feat(fe): replace threshold mask with client-side sheet BG removal (flood-fill)
The green/black-sheet luminance threshold could not separate the slab from the 'black' sheet, which in real photos reads at lum ~50 and overlaps the slab's bark/shadow tones (measured on the user's samples: 0% green, 60-80% dark, no brightness gap) - it removed 100% of the image. Replace the mask pass with connectivity-based background removal: sample the sheet tone from the border, flood-fill from the border marking sheet-connected pixels as background, keep the connected remainder as the slab. Robust to tonal overlap. Adds bgRemoval.ts (removeBackground + sampleSheetTone) with unit tests proving a sheet-toned slab pixel survives via enclosure.
This commit is contained in:
@@ -15,7 +15,7 @@ import { get, writable, type Readable } from 'svelte/store';
|
||||
import { calibrationScale, fillFromSetting, FILL_DEFAULT } from '../png/crop';
|
||||
import { measure } from '../measure/measure';
|
||||
import { initialTapeLine, rectAlongLine, slabBounds, minAreaRect } from '../geometry/rect';
|
||||
import { keepForSheet } from '../mask/sheetKeep';
|
||||
import { removeBackground, sampleSheetTone } from '../mask/bgRemoval';
|
||||
import { composeInventoryFrame } from '../png/inventory';
|
||||
import { eighthsToIn, type Axis, type BatchPhotoResult, type Capture, type Mask, type MaskParams, type Measures, type Sheet, type Source } from './capture.types';
|
||||
import { buildPhotosReadyLineup, type PhotosReadyLineupInput, type PhotosReadyLineupItem } from './photosReadyLineup';
|
||||
@@ -99,32 +99,33 @@ export const buildMask = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Default mask pass: keep predicates from `mask/sheetKeep` over the decoded grid. */
|
||||
/** Default mask pass: client-side sheet removal (border flood-fill). */
|
||||
export const defaultMaskRun: MaskRun = async (source, params, decoded) =>
|
||||
buildMask(params, decoded, walkMask(decoded, params));
|
||||
|
||||
/** Walk the decoded RGBA grid through the sheet-keep predicate (CAPTURE-30..60). */
|
||||
/**
|
||||
* Mask pass (CAPTURE-30..60): remove the sheet-connected background by
|
||||
* flood-filling from the image border against the sampled sheet tone, keeping
|
||||
* the connected remainder as the slab. This is robust to sheet/slab luminance
|
||||
* overlap that defeats a global brightness threshold.
|
||||
*
|
||||
* `sheet`: 'auto' (sampled tone) uses pure connectivity; 'green'/'black' bias
|
||||
* the sampled tone toward that sheet so the fill anchors correctly.
|
||||
* `sensitivity` (0–100) maps onto the color distance tolerance.
|
||||
*/
|
||||
function walkMask(decoded: DecodedPhoto, params: MaskParams): Uint8Array {
|
||||
const keep = keepForSheet(params.sheet, params.sensitivity, samplePixels(decoded));
|
||||
const { width, height, rgba } = decoded;
|
||||
const bitset = new Uint8Array(width * height);
|
||||
for (let i = 0; i < width * height; i++) {
|
||||
bitset[i] = keep(rgba[i * 4], rgba[i * 4 + 1], rgba[i * 4 + 2]) ? 1 : 0;
|
||||
}
|
||||
return bitset;
|
||||
const tone = sheetToneFor(params.sheet, decoded);
|
||||
const tolerance = 20 + (params.sensitivity / 100) * 80; // 20..100 distance
|
||||
return removeBackground(width, height, rgba, { tolerance, sheetTone: tone });
|
||||
}
|
||||
|
||||
/** Stride-16 edge-pixel sample for `autoPickSheet` (matches the P1 mask worker). */
|
||||
function samplePixels(decoded: DecodedPhoto): { r: number; g: number; b: number }[] {
|
||||
const { width, height, rgba } = decoded;
|
||||
const samples: { r: number; g: number; b: number }[] = [];
|
||||
for (let y = 0; y < height; y += 16) {
|
||||
for (let x = 0; x < width; x += 16) {
|
||||
const i = (y * width + x) * 4;
|
||||
samples.push({ r: rgba[i], g: rgba[i + 1], b: rgba[i + 2] });
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
/** Sheet tone to anchor the flood fill: sampled border, biased by the choice. */
|
||||
function sheetToneFor(sheet: MaskParams['sheet'], decoded: DecodedPhoto): { r: number; g: number; b: number } {
|
||||
const sampled = sampleSheetTone(decoded.width, decoded.height, decoded.rgba);
|
||||
if (sheet === 'green') return { r: 40, g: 170, b: 60 };
|
||||
if (sheet === 'black') return { r: 30, g: 30, b: 30 };
|
||||
return sampled;
|
||||
}
|
||||
|
||||
/** Default inventory encode: P1 frame composer + canvas → image/png blob. */
|
||||
|
||||
Reference in New Issue
Block a user