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:
parent
c6daf827d1
commit
9a11eeabc1
7 changed files with 272 additions and 275 deletions
|
|
@ -12,7 +12,7 @@ import { fitSimilarity, applySim, procrustesMean, smoothTransforms } from './mat
|
|||
import { stabilize, toRasterRing, selectKeys, activeKey } from './pipeline.js';
|
||||
import { IndexedRaster, hexToRgb } from './raster.js';
|
||||
import { writeTake } from './take.js';
|
||||
import { clipPolyAbove, otsuForTest } from './interior.js';
|
||||
import { otsuForTest, scaleRing } from './interior.js';
|
||||
import { synthDense } from './synth.js';
|
||||
|
||||
const results = [];
|
||||
|
|
@ -182,25 +182,17 @@ export function run() {
|
|||
ok('palette expansion introduces no intermediate colours',
|
||||
[...seen].every((c) => allowed.has(c)), `${seen.size} distinct colours`);
|
||||
|
||||
// teeth band: the inner ring clipped to a height. The point of doing it this
|
||||
// way is that the silhouette is the mouth's own shape, so it can never
|
||||
// disagree with the lips - assert that rather than the pixel measurement.
|
||||
// scaleRing is what pulls the sampled region in from MediaPipe's inner lip
|
||||
// landmarks, which sit slightly outside the real opening.
|
||||
{
|
||||
const ring = [{ x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }];
|
||||
const half = clipPolyAbove(ring, 5);
|
||||
ok('clip at mid height halves the box',
|
||||
half.length === 4 && Math.max(...half.map((p) => p.y)) === 5,
|
||||
`${half.length} pts, maxY ${Math.max(...half.map((p) => p.y))}`);
|
||||
ok('clip above everything keeps the ring', clipPolyAbove(ring, 99).length === 4);
|
||||
ok('clip below everything empties it', clipPolyAbove(ring, -1).length === 0);
|
||||
const w = clipPolyAbove(ring, 5);
|
||||
ok('clipped band keeps the ring width',
|
||||
Math.min(...w.map((p) => p.x)) === 0 && Math.max(...w.map((p) => p.x)) === 10);
|
||||
// a non-convex ring must not gain or lose x-extent from clipping
|
||||
const tri = [{ x: 0, y: 0 }, { x: 8, y: 2 }, { x: 4, y: 9 }];
|
||||
const ct = clipPolyAbove(tri, 4);
|
||||
ok('clip of a non-convex-ish ring stays within its x range',
|
||||
Math.min(...ct.map((p) => p.x)) >= 0 && Math.max(...ct.map((p) => p.x)) <= 8);
|
||||
const small = scaleRing(ring, 0.5);
|
||||
const w = Math.max(...small.map((p) => p.x)) - Math.min(...small.map((p) => p.x));
|
||||
ok('scaleRing(0.5) halves the extent', Math.abs(w - 5) < 1e-9, `width ${w}`);
|
||||
const same = scaleRing(ring, 1);
|
||||
ok('scaleRing(1) is identity', same.every((p, i) => Math.abs(p.x - ring[i].x) < 1e-9));
|
||||
let cx = 0; for (const p of small) cx += p.x;
|
||||
ok('scaleRing keeps the centroid', Math.abs(cx / 4 - 5) < 1e-9);
|
||||
}
|
||||
|
||||
// Otsu on a uniform region must report near-zero class separation. It will
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue