Teeth as an extracted blob contour, not a clipped band

The band filled the mouth because a band is the wrong reduction: the bright
region is a blob, and reading it as "everything above a line" throws the shape
away.

Extracting a contour reintroduces the vertex-correspondence problem that made
me avoid it, but for a blob there is a way out. Radial sampling from the
centroid along N fixed directions makes vertex k always mean "the extent in
direction k": correspondence holds by construction, the count is fixed, and
temporal smoothing cannot reorder anything. It also yields a star-shaped
reduction, which suits flat colour.

Tongue rejection, which the band had no way to express:
- pixels red relative to their own brightness are dropped (teeth are neutral)
- component choice is biased toward the top of the cavity, since area alone
  picks the tongue when the mouth is wide
- separate inner and outer controls: cavity erode pulls the sampled region off
  the lip edge, blob grow/erode resizes the found blob

Also: a knob wired in app.js but missing from index.html threw during wiring
and left a blank page with nothing useful in the console - which is exactly
what happened to teethDwell in the previous commit. el() now names the missing
id, and window.onerror surfaces it in the status line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-24 15:24:52 -04:00
parent c6daf827d1
commit 9a11eeabc1
7 changed files with 272 additions and 275 deletions

View file

@ -65,25 +65,33 @@ is hand-drawn head plates, which this tool does not yet do.
## Teeth
MediaPipe has no landmarks inside the lips: the inner ring bounds the cavity and
everything within it is just pixels. So teeth come from the image — but tracing
the bright blob would produce a new contour every frame with no vertex
correspondence, which is precisely the boil the design exists to avoid.
everything within it is just pixels. So teeth come from the image.
So the extraction yields a **scalar, not a shape**. The teeth polygon is the inner
lip ring clipped to a horizontal line, and only that line's height is measured
(Otsu threshold within the cavity, scanned from the top). The silhouette is
therefore always the mouth's own shape — stable by construction — and the only
per-frame variable is one number, which smooths trivially. It is also how the
shape gets drawn by hand: a band bounded by the lip.
The hazard is vertex correspondence — a traced contour reorders between frames
and boils. The way out for a blob specifically is **radial sampling**: march
outward from the blob's centroid along N fixed directions and take the last pixel
inside. Vertex *k* is then always "the extent in direction *k*", so correspondence
holds by construction, the vertex count is fixed, and temporal smoothing is well
defined with no reordering possible. It also produces a star-shaped reduction,
which is what flat blocks of colour want.
Presence uses hysteresis plus a minimum dwell, the same treatment plate selection
gets, because a teeth block that blinks on and off for single frames is worse
than one that is simply absent.
The **teeth measurement** panel shows exactly what is sampled: region dimmed,
kept pixels green, extracted contour amber. Tune against that, not the numbers.
**Tongue** would work the same way — a shape filling the lower cavity, gated on a
redness rather than a brightness statistic. Not implemented, because it is not
visible in the test footage: the cavity reads as dark with a bright upper-teeth
band and nothing else.
| Knob | What it does |
| --- | --- |
| teeth contrast | Gate on the separation between the cavity's dark and bright class means. Otsu always returns *some* threshold, so this is what stops it inventing teeth in a dark mouth. |
| cavity erode | Pulls the sampled region in from the lip edge — MediaPipe's inner lip landmarks sit slightly outside the real opening, and lips are bright. |
| blob grow/erode | Resizes the found blob. An open pass always runs first to despeckle. |
| tongue reject | Drops pixels red relative to their own brightness. Teeth are near-neutral; tongue is not. |
| prefer upper | Biases component choice toward the top of the cavity. Area alone picks the tongue when the mouth is wide. |
| teeth vertices | Radial sample count. |
| teeth avg ±f | Temporal average over the contour. |
| teeth dwell | Frames a presence change must persist before it takes effect. |
**Tongue** as its own part would work the same way, gated on redness instead of
brightness and biased low rather than high. Not implemented: it is not visible in
the test footage, which reads as a dark cavity with a bright upper-teeth band.
## The plate is reference, not art