How to detect if 2 instances of one function are the same (aka how to detect a twin)?

First of all, sorry for the confusing title. My sketch will make it clearer.

I have written a function that makes faces that are composed of seperate but overlayed facial elements (which are png’s):
-head shape
-nose
-eyebrows
-eyes
-mouth
-hair

There are 10 of each, so basically with this sketch we can make 10 million faces and I want to find twins. If two faces in a grid of faces are exactly the same, I want to highlight those faces (e.g. if i compare 2 faces, like in the sketch below, i want the BG color to become red if they’re twins).

But I’m breaking my head over how to do this, because I can’t find a way to compare the 2 instances of my drawFace() function. Scoping issue, of course. But I’m too new to P5js to fully wrap my head around this problem.

This is the code of the complete sketch. Maybe it makes more sense to take a look at the full screen version with the PNG’s: p5.js Web Editor

Thanks for the help!

let brow = [];
let eyes = [];
let hair = [];
let head = [];
let mouth = [];
let nose = [];
let names = [];
let lastnames = [];

let firstname;
let lastname;

let cols = 2;
let rows = 1;

//number of variations per facial element
let variations = 10;

function preload() {
  //Preload all variations of facial elements
  for (let i = 1; i <= variations; i++) {
    brow[i] = loadImage("img/brow" + i + ".png");
    eyes[i] = loadImage("img/eyes" + i + ".png");
    hair[i] = loadImage("img/hair" + i + ".png");
    head[i] = loadImage("img/head" + i + ".png");
    mouth[i] = loadImage("img/mouth" + i + ".png");
    nose[i] = loadImage("img/nose" + i + ".png");

    //preload JSON file met 1000 last names en 1000 voornamen
    lastnames = loadJSON("lastnames.json");
    firstnames = loadJSON("names.json");
  }
}

function setup() {
  createCanvas(1080, 1080);
  background(220);
}

function draw() {
  background(230);

  for (let x = 0; x < width; x += width / cols) {
    for (let y = 0; y < height; y += height / rows) {
      drawFace(x, y + height / (cols * 2), width / cols, width / cols);
    }
  }

  frameRate(1);
}

function drawFace(x, y, w, h) {
  let firstname;
  let lastname;

  let brownr = int(random(1, variations + 1));
  let eyesnr = int(random(1, variations + 1));
  let hairnr = int(random(1, variations + 1));
  let headnr = int(random(1, variations + 1));
  let mouthnr = int(random(1, variations + 1));
  let nosenr = int(random(1, variations + 1));
  

  image(brow[brownr], x, y, w, h);
  image(eyes[eyesnr], x, y, w, h);
  image(hair[hairnr], x, y, w, h);
  image(head[headnr], x, y, w, h);
  image(mouth[mouthnr], x, y, w, h);
  image(nose[nosenr], x, y, w, h);

 textAlign(CENTER);

  firstname = firstnames.data[int(random(0, 1000))];
  lastname = lastnames.data[int(random(0, 1000))];

  text(str(firstname + " " + lastname), x + w / 2, y + h + 20);
  

}

function keyPressed() {
  if (key === " ") {
    let code;
    code = split(str(millis()), ",");
    let title;
    title = "Head Nr " + code;
    saveCanvas(title, "png");
  }
}

essentially you have a grid of faces, each of which has 6 numbers.

you can make a 2D grid array (or an array of arrays) and store all numbers

(so like in Excel you have a line for each face and for each face in the columns the variations for nose etc.)

now set the values to the values from drawFace (in drawFace)

compare each face (its 6 numbers) to all others (except itself)

(for loop over all faces then over all columns, when all columns are the same, you have a match)

Chrisir

1 Like

I’m spitballing before having to leave the house soon, so this might not be the most coherent or reasonable approach:

I’d try to codify every face configurations into a “face seed number”. Here’s a shortened Example:

When you generate the head, it randomly picks head version 5.
When you generate the nose, it randomly picks nose version 3.
When you generate the eyes, it randomly picks eyes version 7.
When you generate the ears, it randomly picks ears version 1.
(I’m leaving out the other parts, but you get the point…)
So this particular face would have the seed of “5+3+7+1”.
Now you need to just move through all other faces to find the ones with identical seeds.

One problem: How can you generate a unique seed number which is actually comparable?
Because you can’t just add the numbers together – in this example to 16 – because there’s multiple ways to add numbers up to 16.

The following three face configurations all would add up to 16. No bueno.
5+3+7+1 is 16
4+3+7+2 is 16
10+2+1+3 is 16

What you can instead do, is translate the number via binary:

5+3+7+1 becomes 0101+0011+0111+0001
4+3+7+2 becomes 0100+0011+0111+0010
10+2+1+3 becomes 1010+0010+0001+0011

But, don’t add these binary numbers together, instead concatenate them:

0101+0011+0111+0001 turns into 0101001101110001
0100+0011+0111+0010 turns into 0100001101110010
1010+0010+0001+0011 turns into 1010001000010011

And in the final step, turn binary to decimal again:

0101001101110001 is 21361
0100001101110010 is 17266
1010001000010011 is 41491

Voilà, the three configurations now all have unique seeds which you now can compare against each other. Every number derived via this binary transformation detour can absolutely only be created by that very specific configuration.

Hope this gives you some food for thought.

2 Likes

Or more simply, think of each attribute as a digit in a number. If you are set on 10 choices per attribute then it’s trivial to think of each attribute as a digit of your base-10 number. But you can also create unique numbers for different sized attributes.

If you had 4 attributes with 6, 8, 7, and 5 choices for each and a given face had choices v1, v2, v3, v4, then its value would be ((v1 * 8 + v2) * 7 + v3) * 5 + v4.

For each face you generate, store its value as well, then you can just compare those to find duplicates.

1 Like

Thanks everyone for the insightful hints on how to solve this! I finally managed to solve it, but it’s not at all an elegant solution. I managed only to do it for 2 faces next to each other.

I ended up making 2 “faceseeds” (instead of doing the binary conversion, i just made string variables out of them, makes it easier to compare imo). But this made me split the function drawFace() into two separate functions drawFace1() and drawFace2(). I also made 2 arrays of random integers that pick a random nose, hair, mouth, etc.

I’m certain that I could have done this a lot more elegantly, but it works now. I still can’t figure out how I can do this with only 1 function, because I feed the random integer arrays into the function, it gets drawn in my draw loop and then the next face gets drawn out of the same function, but there is no way to compare the 2 “seeds” of those faces.

But it works. If you want to take a chance in 10 million: the sketch will save the frame if you happen to stumble upon twins! Here it is: p5.js Web Editor

Anyway… Here’s the updated code. Thanks again.
Still curious if someone can make this in less code.

let brow = [];
let eyes = [];
let hair = [];
let head = [];
let mouth = [];
let nose = [];
let names = [];
let lastnames = [];

//array of arrays of elements of face
let faceArray1 = [];
let faceArray2 = [];

let faceSeed1 = 1;
let faceSeed2 = 2;

let firstname;
let lastname;

let cols = 2;
let rows = 1;

//instellen hoeveel variaties we hebben van de elementen
let variations = 10;

function preload() {
  //Preload alle variaties van alle gezichtselementen. Aantal variaties instellen in global var
  for (let i = 1; i <= variations; i++) {
    brow[i] = loadImage("img/brow" + i + ".png");
    eyes[i] = loadImage("img/eyes" + i + ".png");
    hair[i] = loadImage("img/hair" + i + ".png");
    head[i] = loadImage("img/head" + i + ".png");
    mouth[i] = loadImage("img/mouth" + i + ".png");
    nose[i] = loadImage("img/nose" + i + ".png");

    //preload JSON file met 1000 last names en 1000 voornamen
    lastnames = loadJSON("lastnames.json");
    firstnames = loadJSON("names.json");
  }
}

function setup() {
  createCanvas(1000, 1000);
  background(220);
  frameRate(60);
}

function draw() {
  background(230);

  //we'll make 7 arrays of nose, eyes, hair, etc. each with 10 variations. These numbers will inform the drawFace function

  faceArray1 = [
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
  ];

  faceArray2 = [
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
  ];

  //piece of code if you use 2 cols , 1 row
  drawFace1(0, 250, 500, 500);
  drawFace2(500, 250, 500, 500);

  //If they are twins, color the BG red and save the png
  if (faceSeed1 == faceSeed2) {
    background(255, 0, 0);
    drawFace1(0, 250, 500, 500);
    drawFace2(500, 250, 500, 500);
    saveCanvas("YOU FOUND A TWIN", "png");
    //reset faceSeed
    faceSeed1 = 0;
    faceSeed2 = 1;
  }

  //piece of code for larger grids
  //     drawFace(x, y, width/cols, width/cols);
}

function drawFace1(x, y, w, h) {
  let firstname;
  let lastname;

  image(brow[faceArray1[0]], x, y, w, h);
  image(eyes[faceArray1[1]], x, y, w, h);
  image(hair[faceArray1[2]], x, y, w, h);
  image(head[faceArray1[3]], x, y, w, h);
  image(mouth[faceArray1[4]], x, y, w, h);
  image(nose[faceArray1[5]], x, y, w, h);

  faceSeed1 = str(
    str(faceArray1[0]) +
      str(faceArray1[1]) +
      str(faceArray1[2]) +
      str(faceArray1[3]) +
      str(faceArray1[4]) +
      str(faceArray1[5])
  );

  print(faceSeed1);

  textAlign(CENTER);

  firstname = firstnames.data[int(random(0, 1000))];
  lastname = lastnames.data[int(random(0, 1000))];

  //code for 2 x 1
  text(str(firstname + " " + lastname), x + w / 2, y + h + 20);

  //textSize(20);
//  text(str(firstname + " " + lastname), x + w / 2, 1060);
}

function drawFace2(x, y, w, h) {
  let firstname;
  let lastname;

  image(brow[faceArray2[0]], x, y, w, h);
  image(eyes[faceArray2[1]], x, y, w, h);
  image(hair[faceArray2[2]], x, y, w, h);
  image(head[faceArray2[3]], x, y, w, h);
  image(mouth[faceArray2[4]], x, y, w, h);
  image(nose[faceArray2[5]], x, y, w, h);

  faceSeed2 = str(
    str(faceArray2[0]) +
      str(faceArray2[1]) +
      str(faceArray2[2]) +
      str(faceArray2[3]) +
      str(faceArray2[4]) +
      str(faceArray2[5])
  );

  print(faceSeed2);

  textAlign(CENTER);

  firstname = firstnames.data[int(random(0, 1000))];
  lastname = lastnames.data[int(random(0, 1000))];

  //code for 2 x 1
  text(str(firstname + " " + lastname), x + w / 2, y + h + 20);

  // textSize(20);
//  text(str(firstname + " " + lastname), x + w / 2, 1060);
}

function keyPressed() {
  if (key === " ") {
    let code;
    code = split(str(millis()), ",");
    let title;
    title = "Head Nr " + code;
    saveFrames(title, "png", 15, 1);
  }
}

Sooo… I’ve just returned back home and I feel compelled to add some input here. But since I had a real good evening out, my answers might still not be the most coherent or reasonable approach. Possibly even less so…

I’ll also caveat everything by saying that I’m not well versed with p5.js, so I may not be able to fully switch over my understanding of Processing correctly. Here goes:

You can harness the power of function returns to achieve what you want.

The first time you can do it is here:

faceArray1 = [
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
  ];

faceArray2 = [
   int(random(1, variations + 1)),
   int(random(1, variations + 1)),
   int(random(1, variations + 1)),
   int(random(1, variations + 1)),
   int(random(1, variations + 1)),
   int(random(1, variations + 1)),
 ];

Those two identical array generation parts scream to be wrapped into a dedicated function. Something like this:

function generateFace() {
  return [
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
    int(random(1, variations + 1)),
  ]
}

Basically you’re calling a function which immediately returns a generated array.

Now call this function as follows and assign the returned value to the two faceArrays.

  faceArray1 = generateFace();
  faceArray2 = generateFace();

Nice! The same function generates two random face configurations.

This was the easy and preliminary part. On to the main course.

This is your current solution:

  drawFace1(0, 250, 500, 500);
  drawFace2(500, 250, 500, 500);

drawFace1() and drawFace2() are near-identical, except that within the respective functions you manually specify which faceArray needs to be used for the calculations.

This is my proposed approach:

faceSeed1 = drawFace(faceArray1, 0, 250, 500, 500);
faceSeed2 = drawFace(faceArray2, 500, 250, 500, 500);

You can call just one drawFace() function, but pass into it the faceArray it should use for calculations.

The function then looks something like this:

function drawFace(faceArray, x, y, w, h) {
  let firstname;
  let lastname;

  image(brow[faceArray[0]], x, y, w, h);
  image(eyes[faceArray[1]], x, y, w, h);
  image(hair[faceArray[2]], x, y, w, h);
  image(head[faceArray[3]], x, y, w, h);
  image(mouth[faceArray[4]], x, y, w, h);
  image(nose[faceArray[5]], x, y, w, h);

/* I'm leaving out a lot of the code here */

 return faceSeed;
}

You should be able to generalise the function by yourself. No need to differentiate between faceArray1 and faceArray2, because that info is contained within the passed faceArray parameter.

Also, very important, that final return value of “faceSeed” so that you then can compare the two seeds with each other.

Some comments:

  • I’ve tested these changes in the Web Editor and it seems to work.
  • I really don’t like that the drawFace() function still unifies a drawing component with a calculation and value return component. It does work fine, but I feel that more capable programmers will be sadly shaking their head at this setup. I don’t know a better way around this though.
  • The sketch as a whole works because you’re strict about the number of variations each face component can have – exactly 10, as specified in “variations”. As soon as you want to introduce varying numbers of individual components, things will require some adjusting.
  • Contrary to what I just said two points higher up, inelegant code oftentimes is actually good enough. I’ve sweated over many sketches trying to make them elegant, robust, safe and performant. But in truth, duct taping code fragments together to generate fun output is often fine, as long as it works. If you’re pushing for a computer science title, then sure, all of the mentioned considerations are required. But for messing around and generating fun prototypes it’s not. So don’t fret too much about writing things correctly all the time.
  • Though, it’s never wrong to aim at becoming better and learn new stuff. :wink:
  • Greeting from neighbouring Germany. Time for bed.
1 Like

Thanks so much for the insights and the time you took to review my sloppy program.
I’m putting aside my shame and admit that the Power of the Return was unknown to me until today. That might actually make a lot of my earlier sketches a bit more streamlined.

I’ll also add that, although I’m not making a life goal out of it, making capable programmers sadly shake their head at my code is an oddly satisfying thought. As a designer, I’m in for the duct taping. Greetings back from Belgium (which is, come to think of it, also kind of duct taped together!).

Happy to help, I’ve had some amazing support from this community throughout the years, and now that I actually know a solution to a question myself, I’m paying it forward.

Also don’t feel bad about not knowing about returns. It’s an essential part of functions, but often taught as an afterthought, in non-CS environments.

One takeaway you should have from all of this is that it’s absolutely possible to pass into and return from more complex data types with functions. Everybody knows that you can have int or float variables as function attributes, e.g. to define position and size of a shape.

But there’s no stopping you from passing in arrays (as I did in my example). Or vectors. Or objects.

Alas, depending on your level of programming and your willingness to actually go deeper, these latter two data types may be a bit too complex for your needs.

Though if you think return values in functions are powerful, your brain will explode when you understand what objects can do.

2 Likes

https://editor.p5js.org/scudly/sketches/0zsd51dny has some simpler code that spits out a 10x10 array of faces. Click with the mouse to make a new set. The code can handle a variable number of each attribute if you adjust the loading to not be a fixed number of variations.

let attribs = [[],[],[],[],[],[]];
let nPossible = 1;
let makeNew = true;
let uniqueVals = [];

let names = [];
let lastnames = [];

let firstname;
let lastname;

//instellen hoeveel variaties we hebben van de elementen
let variations = 10;

function preload() {
  //Preload alle variaties van alle gezichtselementen. Aantal variaties instellen in global var
  for (let i = 1; i <= variations; i++) {
    attribs[0][i-1] = loadImage("img/brow" + i + ".png");
    attribs[1][i-1] = loadImage("img/eyes" + i + ".png");
    attribs[2][i-1] = loadImage("img/hair" + i + ".png");
    attribs[3][i-1] = loadImage("img/head" + i + ".png");
    attribs[4][i-1] = loadImage("img/mouth" + i + ".png");
    attribs[5][i-1] = loadImage("img/nose" + i + ".png");
  }

  //preload JSON file met 1000 last names en 1000 voornamen
  lastnames = loadJSON("lastnames.json");
  firstnames = loadJSON("names.json");
}

function setup() {
  createCanvas(1000, 1000);
  for( let i=0; i<attribs.length; i++ ) {
    nPossible *= attribs[i].length;
  }
  noLoop();
}


function getUniqueInts( N, nRange ) {
  let nums = [];
  for( let i=0; i<N; i++ ) {
    nums[i] = int(random( nRange ));
    for( let j=0; j<i; j++ ) {
      if( nums[i] == nums[j] ) { 
        nums[i] = int(random( nRange ));
        j = 0;
      }
    }
  }
  return nums;
}

function draw() {
  if( makeNew ) {
    uniqueVals = getUniqueInts( 100, nPossible );
    makeNew = false;
  }
  background( 220 );
  textAlign(CENTER);
  for( let x=0, iu=0; x<10; x++ ) {
    for( let y=0; y<10; y++, iu++ ) {
      let idx = uniqueVals[iu];
      push();
      translate( x*width/10, y*height/10 );
      text( idx, 50, 95 );
      for( let i=0; i<attribs.length; i++ ) {
        let j = idx % attribs[i].length;
        image( attribs[i][j], 5, 0, 90, 90 );
        idx = int( idx / attribs[i].length );
      }
      pop();
    }
  }
}

function mousePressed() {
  makeNew = true;
  redraw();
}

function keyPressed() {
  if (key === " ") {
    let code;
    code = split(str(millis()), ",");
    let title;
    title = "Head Nr " + code;
    saveFrames(title, "png", 15, 1);
  }
}

1 Like

for testing purposes you can just work with 2 variations

2 Likes

This is some awesome stuff. Thanks for this!!