NOTES — 01 · ANIMAL LOOKALIKE

Matching a face to twelve animals

A human face and an animal face do not live on the same scale, so a distance between them is meaningless until you change what you are measuring. This is how that page works, what broke in the first version, and what the fix cost.

ENGLISH ONLY · ~10 MIN · ALL FIGURES MEASURED, NOT ESTIMATED

01The problem is the scale, not the maths

The obvious way to build this is to measure some facial proportions, measure the same proportions on animal photos, and pick whichever animal is closest. That approach fails immediately, and it fails for a reason worth stating precisely: humans occupy a tiny corner of the space that animals spread across.

Here are the six proportions the page uses, with the range covered by 34 human reference faces next to the range covered by the 12 animal references.

FeatureHumans (n=34)Animals (n=12)Animals inside the human range
Face width ÷ length0.760 – 0.9150.713 – 1.5423 / 12
Eye spacing0.213 – 0.2950.269 – 0.8383 / 12
Eye size0.163 – 0.2210.055 – 0.2514 / 12
Nose ratio0.221 – 0.3040.203 – 0.4475 / 12
Mouth width0.305 – 0.4520.228 – 0.7266 / 12
Eye tilt (degrees)0.34 – 11.74−13.99 – 28.842 / 12
Every ratio is normalised by face width or face length, so image size and camera distance drop out. Eye tilt is an angle, which is why it is the one feature that is not a ratio.

Look at the last row. Human eyes tilt between 0.3° and 11.7° in this sample; the animal references run from −14° to +29°. A raw distance in that space does not ask "whose eyes are more like yours" — it asks "whose eyes are least far from the middle of a range you were never in." The answer is the same animal for almost everybody, which is exactly what happened.

THE FIX

Rank first, then compare. Convert your measurement into a percentile within the human sample, convert each animal into a percentile within the animal sample, and compare those two numbers instead. The question stops being "is your face 0.83 wide" and becomes "you are wider-faced than 78% of people — which animal is wider-faced than 78% of animals?"

That single change is the whole idea of the page. Everything after it is bookkeeping.

02What is actually measured

MediaPipe's FaceLandmarker returns 468 points. The page uses twelve of them:

face outline 234 left · 454 right · 10 top · 152 chin nose 168 bridge · 1 tip mouth 61 left · 291 right eyes 33 / 133 outer-inner · 362 / 263 inner-outer

Before anything is computed, the photo has to pass a frontality gate. The distance from the nose tip to the left edge of the face is compared with the distance to the right edge:

frontal = min(d_left, d_right) / max(d_left, d_right) reject if < 0.80

A face turned to the side compresses one half in the image, and every ratio downstream inherits that compression. Rejecting the photo with a visible score is more honest than silently returning a confident answer computed from a bad measurement.

03Percentile matching, in full

Both sides are converted with the same mid-rank formula, which is just the fraction of the sample lying below the value, with ties split in half:

pctRank(v, S) = ( |{x ∈ S : x < v}| + 0.5·|{x ∈ S : x = v}| ) / |S|

Your six measurements go through the human sample; the twelve animals go through the animal sample. Both sides now live in the unit cube, and the comparison is a weighted root-mean-square distance across the six axes:

d(you, animal) = √( Σ wk·(pk − ak)² / Σ wk ) weights face shape 1.2 · eye spacing 1.1 · eye tilt 1.1 eye size 1.0 · nose 0.9 · mouth 0.5

The weights are a judgement call, not a fitted parameter — face shape and eye placement carry more of what people read as resemblance than mouth width does. I would not defend the exact values; I would defend the ordering.

04The deer collapse

The first working version was tested and it kept returning deer. Not always, but far more often than one animal out of twelve should. It was easy to dismiss as a coincidence of the faces being tested, so I measured it instead.

The test runs 20,000 synthetic people through the matcher. Each synthetic person is built by drawing each of the six percentiles independently from the 34-person sample, which is the most neutral population I can construct from the data I have. With twelve animals, a well-behaved matcher should hand out roughly 8.3% to each.

AnimalWin rate, no correctionvs 8.3% baseline
Deer21.7%2.6×
Otter15.6%1.9×
Rabbit11.3%1.4×
Dog10.4%1.2×
Capybara9.7%1.2×
Pig7.4%0.9×
Harbor seal7.1%0.9×
Cat6.9%0.8×
Fox4.1%0.5×
Beluga2.6%0.3×
Wolf1.8%0.2×
Quokka1.6%0.2×
20,000 simulated people, seeded. Most frequent animal ÷ least frequent = 13.7×.

A 13.7× spread between the most and least reachable animal is not a quirk. Two of the twelve are effectively unreachable: a wolf or a quokka result was something almost nobody would ever see.

Why deer, specifically

Deer is not "more human-like" in any meaningful sense. Deer simply sits closest to the middle of the animal percentile cube, and the middle is where most people land once you rank them. I made this explicit by computing, for each animal, its mean distance to the whole human reference set:

Deer0.277 — nearest
Rabbit0.326
Otter0.350
Capybara0.369
Pig0.370
Dog0.391
Fox0.396
Harbor seal0.398
Cat0.431
Wolf0.436
Quokka0.437
Beluga0.480 — farthest

The ordering of that chart is almost exactly the ordering of the win-rate table, upside down. The winner was being decided by the geometry of the reference set rather than by the face in the photo.

The correction

Ranking divides each raw distance by the animal's own average distance, raised to a power:

adjusted = raw / avgDistanceα α = 0.5

An animal that is close to everybody has a small average distance, so dividing by it inflates its distance and it stops winning by default. α controls the strength: 0 is no correction, 1 is full normalisation. I swept it.

αMost frequentLeast frequentSpreadEntropy (max 3.58)
022.0%1.6%13.7×3.247
0.215.3%2.2%7.0×3.378
0.3514.2%2.7%5.2×3.442
0.513.0%3.3%3.9×3.478
0.6513.0%4.1%3.2×3.495
0.813.1%3.7%3.6×3.496
1.014.0%2.3%6.1×3.480
Entropy of the winner distribution in bits; 3.58 would be a perfectly uniform twelve-way split. Note that α=1 is worse than α=0.65 — over-correcting starts handing the game to the outliers instead.

The shipped value is α = 0.5. It captures most of the available improvement — spread 13.7× → 3.9×, entropy 3.247 → 3.478 out of 3.58 — while changing the top answer for 22.9% of faces rather than the 38.1% that α=1 would rewrite. Past about 0.65 the curve is flat and the only thing still moving is how much the correction overrides the actual measurement.

05What the fix cost

Every correction buys something with something. This one has a visible price, and it is visible on the results page itself.

The ranking uses the adjusted distance, but the percentage shown next to each animal is computed from the raw distance — deliberately, because the percentage should describe how similar you actually are, not how similar you are after a diversity thumb on the scale. The consequence is unavoidable:

KNOWN ARTEFACT

In 16.6% of simulated faces, the runner-up displays a higher percentage than the winner. The average size of that inversion is 1.9 percentage points.

It is real, it is on screen, and no reasonable reader would guess the cause. The alternatives were to show the adjusted number — which would be a number that means nothing outside this page — or to rank by the raw distance and accept the deer collapse. I chose the visible oddity over the invisible bias.

06How much should you trust the answer

Landmark positions move between photos of the same person — different lighting, expression, angle. To put a number on what that does, I perturbed each percentile with Gaussian noise and measured how often the winner changed.

Percentile jitter (σ)Top answer changesFalls out of the original top 3
±2 points6.9%0.1%
±5 points16.0%0.8%
±10 points29.3%4.7%
20,000 simulated faces per row, α = 0.5. This is a proxy for photo-to-photo variation, not a measurement of it — I do not have repeat photographs of the same faces.

The honest reading: the top three are stable, the single winner is not. At a jitter that is easy to imagine between two photos of the same person, the headline answer changes one time in six, while the correct answer stays inside the displayed shortlist better than 99 times in 100. That is why the page shows four runners-up instead of one verdict.

And the percentage itself

The displayed number is an exponential decay of the distance, clamped:

shown % = clamp( 100·e−d/0.62, 42, 97 )

Across 20,000 simulated faces the winner's percentage ran from 46.8% to 94.5%, median 68.4%, with the middle half between 63.9% and 73.2%. Neither clamp ever fired — 0.0% of runs hit either bound, so those two constants are dead code that survives only as a guard against a pathological input. The number is a monotone restatement of the distance dressed up to read like a score; it is not a probability of anything.

07What is still wrong with it

Four things I know about and have not fixed.

The human reference set is stored as six sorted lists, not 34 people. Only the marginal distribution of each feature survives; the pairing between features is gone. The balance correction rebuilds "34 people" out of those lists, which produces 34 profiles that sit on the diagonal of the percentile cube — every feature at the same percentile. Real faces are not like that. Rerunning the collapse measurement against that diagonal population instead of the independent one puts deer at 47.1% rather than 21.7%, so the diagonal exaggerates the problem it is used to correct. The correction is calibrated against a population that does not exist. It still helps, and the ordering it produces is right, but the constant it produces is not trustworthy to two decimal places.

34 humans and 12 animals are small. A percentile computed against 34 values moves in steps of about three points, which is the same order as the jitter that flips one answer in six.

The animal references are hand-labelled from photographs. One representative image per species, landmarked by hand. "The average deer face" is doing a lot of work in that sentence, and it is not doing it very well.

There is no ground truth. There is no correct answer to "which animal does this face resemble", so nothing here is an accuracy figure. Everything measured in this write-up is a property of the algorithm — how it distributes its answers, how stable they are — not evidence that the answers are right. I think that distinction is the most important sentence on this page.

08Why write this down

The first version of this page worked. It loaded a model, found a face, computed six numbers and printed an animal with a confident percentage next to it. Nobody using it would have known that one answer in five was the same animal, or that two of the twelve were unreachable. The failure was completely invisible from the outside, and it was only found because the output looked boring rather than wrong.

That is the part worth keeping: the bug was not in the code, it was in the assumption that a distance in a space I had built was a distance that meant something. The fix was not a better algorithm but a measurement of the one I had — and then a second measurement of what the fix itself cost.

Reproducing the numbers

Every figure above comes from a harness that imports the exact constants shipped on the live page — the twelve animal profiles, the six sorted human lists, the weights — and re-implements the matcher around them, so it cannot drift from what the page actually does. Populations are seeded, N = 20,000 unless stated. If a constant on the page changes, the harness reruns and this write-up is wrong until it is updated.

Try it. The photo is processed entirely in your browser; nothing is uploaded and nothing is stored.

Open the Animal Lookalike →