Adjust knobs to refine the mask. Green overlay shows the detected slab.
Adjust knobs to refine the mask. Detected slab shows in foreground; checkerboard = removed background.
diff --git a/frontend/test/bvt/live-mask.spec.ts b/frontend/test/bvt/live-mask.spec.ts
index 7f5e5b8..60ae279 100644
--- a/frontend/test/bvt/live-mask.spec.ts
+++ b/frontend/test/bvt/live-mask.spec.ts
@@ -2,7 +2,7 @@
* LIVE mask verification — issue #55, Gate B.
*
* Tests three fundamentals against the deployed host:
- * 1. Overlay translucent — photo visible, not solid green washout.
+ * 1. Cutout preview — slab pixels visible, checkerboard where background removed.
* 2. Knobs respond fast — debounce working, no multi-second lag.
* 3. Auto/green/black sheet does not mark entire frame as slab.
*
@@ -34,7 +34,7 @@ async function loadToMaskStep(page: import('@playwright/test').Page) {
}
test.describe('LIVE mask fundamentals (issue #55)', () => {
- test('1. Overlay translucent — photo visible under green mask, not solid green washout', async ({ page }) => {
+ test('1. Cutout preview — slab pixels visible on checkerboard, no green overlay', async ({ page }) => {
await page.goto('/');
await loadToMaskStep(page);
@@ -44,7 +44,8 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
path: resolve(evidenceDir, 'mask-overlay-live.png'),
});
- // Read canvas pixel data to assert overlay is translucent.
+ // Read canvas pixel data: slab pixels show actual photo RGB,
+ // background shows checkerboard (224/192 gray, 255 alpha), no green.
const stats = await page.evaluate(() => {
const canvas = document.querySelector('canvas.mask-canvas') as HTMLCanvasElement;
if (!canvas) return { width: 0, height: 0 };
@@ -52,47 +53,62 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
if (!ctx) return { width: 0, height: 0 };
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { data } = imgData;
- const len = data.length;
- let totalAlpha = 0;
- let greenPixelCount = 0;
- let maxAlpha = 0;
- for (let i = 0; i < len; i += 4) {
+ let slabPixels = 0;
+ let checkerPixels = 0;
+ let greenTintPixels = 0;
+ let hasColorVariance = false;
+ const totalPixels = canvas.width * canvas.height;
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i];
+ const g = data[i + 1];
+ const b = data[i + 2];
const a = data[i + 3];
- totalAlpha += a;
- if (a > 0) greenPixelCount++;
- if (a > maxAlpha) maxAlpha = a;
+
+ // Checkerboard pixel: gray (224 or 192), full alpha, no color.
+ if (a === 255 && (r === 224 || r === 192) && r === g && g === b) {
+ checkerPixels++;
+ }
+ // Slab pixel: not checkerboard, alpha from mask (may vary).
+ else if (a > 0) {
+ slabPixels++;
+ // Check if any slab pixel has color variance (not monochrome gray).
+ const maxC = Math.max(r, g, b);
+ const minC = Math.min(r, g, b);
+ if (maxC - minC > 5) hasColorVariance = true;
+ }
+ // Green-tint detection: green channel significantly > red and blue.
+ if (g > 180 && r < 100 && b < 100) {
+ greenTintPixels++;
+ }
}
- const avgAlpha = totalAlpha / (canvas.width * canvas.height);
return {
width: canvas.width,
height: canvas.height,
- avgAlpha,
- maxAlpha,
- greenPixelCount,
- totalPixels: canvas.width * canvas.height,
+ slabPixels,
+ checkerPixels,
+ greenTintPixels,
+ hasColorVariance,
+ totalPixels,
};
});
- console.log(`Canvas: ${stats.width}x${stats.height}, avgAlpha=${stats.avgAlpha.toFixed(1)}, maxAlpha=${stats.maxAlpha}, greenPixels=${stats.greenPixelCount}/${stats.totalPixels} (${(stats.greenPixelCount / stats.totalPixels * 100).toFixed(1)}%)`);
+ console.log(`Canvas: ${stats.width}x${stats.height}, slab=${stats.slabPixels}, checker=${stats.checkerPixels}, greenTint=${stats.greenTintPixels}`);
- // Check 1 assertions:
- // a) Canvas has dimensions (not blank)
+ // a) Canvas has dimensions
expect(stats.width).toBeGreaterThan(0);
expect(stats.height).toBeGreaterThan(0);
- // b) Average alpha must be well below 255 — the overlay is translucent.
- // Max alpha is 80 (~31% of 255), so average should be < 128 (50% opacity).
- expect(stats.avgAlpha).toBeLessThan(128);
- expect(stats.avgAlpha).toBeGreaterThan(0); // Must have some overlay at all.
+ // b) Must have checkerboard pixels (removed background)
+ expect(stats.checkerPixels).toBeGreaterThan(0);
- // c) Max alpha must be 80 or close (the fix set max alpha to 80).
- expect(stats.maxAlpha).toBeLessThan(170); // Well below full opacity.
+ // c) Must have slab pixels (the wood showing through)
+ expect(stats.slabPixels).toBeGreaterThan(0);
- // d) Green pixel fraction should not be ~100% — the overlay should NOT cover
- // the entire canvas with green washout. There should be visible photo areas.
- const greenFraction = stats.greenPixelCount / stats.totalPixels;
- expect(greenFraction).toBeLessThan(0.95);
- console.log(`Green overlay fraction: ${greenFraction.toFixed(2)} (must be <0.95)`);
+ // d) Slab pixels must have color variance — real wood, not green wash
+ expect(stats.hasColorVariance).toBe(true);
+
+ // e) No green tint pixels — the green overlay is gone
+ expect(stats.greenTintPixels).toBe(0);
});
test('2. Knobs respond fast on real photo — debounce working, no multi-second lag', async ({ page }) => {
@@ -143,11 +159,9 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
await loadToMaskStep(page);
// Read the canvas pixel data and compute slab fraction.
- // The canvas shows: photo + green overlay on slab pixels.
- // Green overlay = (60, 220, 60) with alpha proportional to mask value.
- // Sheet pixels have alpha=0 (fully transparent).
- // So: count pixels where the green channel is > 150 AND red channel < 100
- // to detect the green overlay, which indicates slab pixels.
+ // Canvas now shows: slab pixels (actual photo RGB, mask alpha) +
+ // checkerboard (224/192 gray, 255 alpha) where background was removed.
+ // Slab pixels are any non-checkerboard pixels with alpha > 0.
const slabStats = await page.evaluate(() => {
const canvas = document.querySelector('canvas.mask-canvas') as HTMLCanvasElement;
if (!canvas) return { width: 0, height: 0 };
@@ -162,12 +176,9 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
- // Green overlay is (60, 220, 60). Slab pixels have alpha > 0.
- // At max alpha (80), the blended pixel on white (240,240,240) would be:
- // R: 240*(1-0.31) + 60*0.31 ≈ 184
- // G: 240*(1-0.31) + 220*0.31 ≈ 234
- // So slab pixels have R < 240, G > 200, B < 240, a > 0
- if (a > 0 && g > 150 && r < 230 && b < 230) {
+ // Checkerboard: r==g==b and r is 224 or 192, alpha 255.
+ const isChecker = a === 255 && r === g && g === b && (r === 224 || r === 192);
+ if (!isChecker && a > 0) {
slabPixels++;
}
}
@@ -214,8 +225,15 @@ test.describe('LIVE mask fundamentals (issue #55)', () => {
const { data } = imgData;
let slabPixels = 0;
for (let i = 0; i < data.length; i += 4) {
+ const r = data[i];
+ const g = data[i + 1];
+ const b = data[i + 2];
const a = data[i + 3];
- if (a > 0) slabPixels++;
+ // Checkerboard: r==g==b and r is 224 or 192, alpha 255.
+ const isChecker = a === 255 && r === g && g === b && (r === 224 || r === 192);
+ if (!isChecker && a > 0) {
+ slabPixels++;
+ }
}
resolve({
width: canvas.width,