From 83171fab6e39c4199b51b14a0e95a32865118f5e Mon Sep 17 00:00:00 2001 From: grokbot_ty Date: Tue, 8 Sep 2026 00:30:24 +0000 Subject: [PATCH] =?UTF-8?q?fix(gate-b):=20drawMaskOverlay=20=E2=80=94=20cu?= =?UTF-8?q?tout=20on=20checkerboard,=20no=20green=20overlay=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace green overlay rendering with cutout-on-checkerboard preview: - Slab pixels show actual photo RGB with mask-based alpha - Removed background area shows 10px checkerboard (224/192 gray) - Knob debounce from a1d4a96 preserved unchanged Update Playwright test to assert checkerboard + slab pixels visible, not green overlay avgAlpha. --- frontend/src/routes/+page.svelte | 45 ++++++++---- frontend/test/bvt/live-mask.spec.ts | 102 ++++++++++++++++------------ 2 files changed, 92 insertions(+), 55 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 80c8c13..6fb7a44 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -337,21 +337,40 @@ img.onload = () => { canvas.width = photo.rgba!.width; canvas.height = photo.rgba!.height; - ctx.drawImage(img, 0, 0); - const imgData = ctx.createImageData(canvas.width, canvas.height); + const w = canvas.width; + const h = canvas.height; + const srcData = photo.rgba!.data; const mask = photo.maskResult!.mask; - // Translucent light green overlay — photo remains clearly visible. - // Max alpha 80 (~31% opacity) on full-slab pixels; feathered edge blends smoothly. - for (let i = 0; i < mask.length; i++) { - const alpha = Math.round((mask[i]! / 255) * 80); - const pixelIdx = i * 4; - imgData.data[pixelIdx] = 60; - imgData.data[pixelIdx + 1] = 220; - imgData.data[pixelIdx + 2] = 60; - imgData.data[pixelIdx + 3] = alpha; + const CHECKER_SIZE = 10; + + // Build ImageData: slab pixels pass through; background = checkerboard. + const outData = ctx.createImageData(w, h); + const out = outData.data; + for (let i = 0; i < w * h; i++) { + const srcIdx = i * 4; + const outIdx = i * 4; + const maskVal = mask[i]!; + + if (maskVal > 0) { + // Slab pixel — show actual photo RGB, alpha from mask. + out[outIdx] = srcData[srcIdx]!; + out[outIdx + 1] = srcData[srcIdx + 1]!; + out[outIdx + 2] = srcData[srcIdx + 2]!; + out[outIdx + 3] = maskVal; + } else { + // Background removed — checkerboard. + const px = i % w; + const py = Math.floor(i / w); + const isLight = (Math.floor(px / CHECKER_SIZE) + Math.floor(py / CHECKER_SIZE)) % 2 === 0; + const v = isLight ? 224 : 192; + out[outIdx] = v; + out[outIdx + 1] = v; + out[outIdx + 2] = v; + out[outIdx + 3] = 255; + } } - ctx.putImageData(imgData, 0, 0); + ctx.putImageData(outData, 0, 0); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(photo.blob); @@ -520,7 +539,7 @@ {#if state.step === 'mask'}

Mask & Knobs

-

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,