feat(#21): Gate B green/black sheet mask (TV-1, TV-2)
Pure client modules for TECH-SPEC sheet detect, chroma-key / black threshold with border flood-fill, four knobs (live re-run), and mask confirm hooks. Synthetic fixtures. U2Net never invoked. TDD: red evidence in test-results/tv1-tv2-red.txt, then minimum impl. Results: TV-1 | pass | test-results/tv1-tv2-green.txt TV-2 | pass | test-results/tv1-tv2-green.txt
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
confirmMask,
|
||||
createMaskSession,
|
||||
defaultKnobs,
|
||||
removeSheetBackground,
|
||||
reRunMask,
|
||||
requestRetake,
|
||||
} from "../lib/pipeline/mask.js";
|
||||
import {
|
||||
blackSheetFixture,
|
||||
greenSheetFixture,
|
||||
maskAt,
|
||||
} from "./fixtures/synthetic.js";
|
||||
|
||||
describe("TV-1 green-sheet chroma-key", () => {
|
||||
it("keeps interior green-ish patch that does not touch the border", () => {
|
||||
const { image, interiorGreen, woodSample, borderSample } = greenSheetFixture();
|
||||
const knobs = { ...defaultKnobs(), sheet: "green" as const };
|
||||
const result = removeSheetBackground(image, knobs);
|
||||
|
||||
expect(result.u2netInvoked).toBe(false);
|
||||
expect(result.sheetUsed).toBe("green");
|
||||
expect(maskAt(result.mask, result.width, borderSample.x, borderSample.y)).toBe(0);
|
||||
expect(maskAt(result.mask, result.width, woodSample.x, woodSample.y)).toBeGreaterThan(0);
|
||||
expect(
|
||||
maskAt(result.mask, result.width, interiorGreen.x, interiorGreen.y),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("re-runs live when knobs change and still skips U2Net", () => {
|
||||
const { image } = greenSheetFixture();
|
||||
const first = removeSheetBackground(image, { ...defaultKnobs(), sheet: "green" });
|
||||
const second = reRunMask(image, {
|
||||
...defaultKnobs(),
|
||||
sheet: "green",
|
||||
sensitivity: 0.7,
|
||||
edgeOffset: 1,
|
||||
feather: 1,
|
||||
});
|
||||
expect(first.u2netInvoked).toBe(false);
|
||||
expect(second.u2netInvoked).toBe(false);
|
||||
expect(second.knobs.sensitivity).toBe(0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TV-2 black-sheet threshold + border flood-fill", () => {
|
||||
it("keeps dark interior wood that does not touch the border", () => {
|
||||
const { image, darkInterior, woodSample, borderSample } = blackSheetFixture();
|
||||
const knobs = { ...defaultKnobs(), sheet: "black" as const };
|
||||
const result = removeSheetBackground(image, knobs);
|
||||
|
||||
expect(result.u2netInvoked).toBe(false);
|
||||
expect(result.sheetUsed).toBe("black");
|
||||
expect(maskAt(result.mask, result.width, borderSample.x, borderSample.y)).toBe(0);
|
||||
expect(maskAt(result.mask, result.width, woodSample.x, woodSample.y)).toBeGreaterThan(0);
|
||||
expect(
|
||||
maskAt(result.mask, result.width, darkInterior.x, darkInterior.y),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("flood-fill removes only border-connected dark pixels", () => {
|
||||
const { image, darkInterior, borderSample } = blackSheetFixture();
|
||||
const result = removeSheetBackground(image, { ...defaultKnobs(), sheet: "black" });
|
||||
// Border sheet gone; isolated dark island kept.
|
||||
expect(maskAt(result.mask, result.width, borderSample.x, borderSample.y)).toBe(0);
|
||||
expect(
|
||||
maskAt(result.mask, result.width, darkInterior.x, darkInterior.y),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mask confirm hooks (pure)", () => {
|
||||
it("starts pending, confirm and retake transition without DOM", () => {
|
||||
const { image } = greenSheetFixture();
|
||||
const result = removeSheetBackground(image, { ...defaultKnobs(), sheet: "green" });
|
||||
const session = createMaskSession(result);
|
||||
expect(session.state).toBe("pending");
|
||||
expect(confirmMask(session).state).toBe("confirmed");
|
||||
expect(requestRetake(session).state).toBe("retake");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user