A character pipeline I built takes a photograph of a person and produces a 3D model. Somewhere in the middle of it, four separate tools need to answer the same question about a pixel: is this skin?
Each of them answered it the same way, with a fixed floor on linear luminance —
0.35 in two places, 0.40 in the others. Numbers I had typed months earlier,
looking at output, adjusting until it looked right, and never revisited, because
every subject the pipeline had ever processed was pale.
Then I ran it on a subject whose skin tone is a warm dark brown. In linear luminance that colour is 0.075.
All four detectors returned zero skin. The orientation check that decides
which way the head is facing refused to proceed. The seam scanner found no skin
at any of its twenty-one sample heights. The seam quality gate crashed on a
None it had never been able to receive before. The albedo pass died on a
KeyError. A fifth detector, elsewhere in the pipeline, happened to guess right
by luck.
Four errors, in four different tools, each one looking entirely local. Nothing in any traceback said threshold. The pipeline did not report “I cannot see this subject.” It reported four unrelated software faults, and my first instinct was that the input was bad.
The claim
A numeric threshold written while looking at one subject is a description of that subject wearing the costume of a rule.
It will pass every subject that resembles the first one, and it will fail the first one that does not — silently or confidently, depending on which side of the number they land. And the failure will not present as the threshold was wrong. It will present as this subject is broken, because the code is sincere and the code says so.
That last part is what makes the class worth naming. Every other participant in
the failure behaved correctly. The gate was right to refuse a None. The albedo
pass was right to insist on a key it needs. The only thing wrong was a number,
several layers upstream, that had quietly stopped being a general rule at the
moment it was typed.
The half that actually matters
Here is the part I would have got wrong if I had been in a hurry, and the reason this is a post and not a changelog entry.
Lowering the floor did not fix it. It made things worse.
Drop the floor to 0.05 and the detectors now find skin. The orientation check runs, produces an answer, and confidently picks the back of the head. It does not crash. It does not warn. It hands a wrong answer downstream, where everything after it behaves correctly on a bad premise, and the resulting model is wrong in a way that takes twenty minutes of staring to explain.
The reason is that the number was never the assumption. The number was where the assumption had been hiding. Underneath those four floors sat a rule I had never written down and never noticed I believed:
skin is brighter than hair.
Which is true of most pale people with dark hair, and is where the value 0.35 came from. It is false — it inverts — for a dark-skinned subject with mid-brown hair, whose hair is the brighter of the two. Turning the knob kept the inverted rule and removed the crash that was announcing it.
So the crash was the good outcome. It said no. The re-tuned version said yes, and was wrong, and a knob turned until a check passes is not a fix — it is a masked test with extra steps.
The shape of the fix
The repair was not a better constant. There is no better constant; any single number is a claim about one population.
Move the threshold from the tool into the subject. The skin tone is now declared per subject in the recipe that drives the pipeline, alongside the other facts about that person — height, hair colour, vocal register. The detectors read the declaration instead of carrying a number. A threshold that describes the subject belongs with the subject; only a threshold that describes the instrument (a sensor noise floor, a codec’s minimum bitrate) belongs in the tool.
Log the measurement beside the declaration. Each detector now prints what it
measured next to what it was told to expect. That converts the entire class of
future failure from a crash into data: a subject whose actual pixels contradict
their own declared tone gets caught as a mismatch with two numbers attached,
rather than as a KeyError four functions away.
Suspect the floor before you suspect the subject. When a detector returns
zero on a new input, the first question is not “what’s wrong with this input” —
it is what did the first input look like when this number was typed? Run git log -S on the constant. The commit that introduced it will usually be sitting
next to the sample that justified it.
Write ordinal rules with their population named. Any rule of the form “A is
brighter / bigger / redder / longer than B” is a claim about a distribution, not
a law. # skin is brighter than hair is a bug. # skin is brighter than hair for the pale-skinned, dark-haired subjects this was built on is a comment that
tells the next reader exactly when to stop trusting it — and it is not harder to
write, only harder to write honestly, because you have to admit at authoring
time that your rule has a domain.
This is a fairness problem and an engineering problem at the same time, and I don’t think those are two things. The pipeline failed a dark-skinned subject because it encoded its first subject as a constant and never labelled the encoding. There was no malice and no shortcut; there was a number that looked like a rule. Most of them do.
The rule: a threshold that describes the subject belongs in the subject’s declaration — and never re-tune a floor to make a check pass.
sources: concepts/a-hard-coded-floor-is-a-hard-coded-subject,
concepts/a-ledgers-diagnosis-is-a-hypothesis