Files
SlabUploader/frontend/src/worker/mask.worker.ts
T
grokbot_ty 03d3fcf460 fix(photos): preserve tapered tips and dark live edge (#73 #74)
Lower the axial body threshold while retaining fringe rejection, minimize length-only tip inset, and make sheet sensitivity curves testable and conservative at zero.
2026-09-16 15:18:06 +00:00

90 lines
3.0 KiB
TypeScript

import { minAreaRect, slabBounds } from '../geometry/rect';
import type { MaskParams, WorkerIn, WorkerOut } from '../capture/capture.types';
import { decodeOrientedBitmap } from '../decodeOrientedBitmap';
import { greenKeep, blackKeep, autoPickSheet } from '../mask/sheetKeep';
let bitmap: ImageBitmap | null = null;
let data: Uint8ClampedArray | null = null;
let w = 0, h = 0;
let current = 0; // highest job id issued by main; older replies are dropped
function runMask(params: MaskParams, id: number): WorkerOut {
if (!data) return { id, kind: 'failed', reason: 'no source decoded' };
let sheet = params.sheet;
if (sheet === 'auto') {
const samples: { r: number; g: number; b: number }[] = [];
for (let y = 0; y < h; y += 16)
for (let x = 0; x < w; x += 16) {
const i = (y * w + x) * 4;
samples.push({ r: data[i], g: data[i + 1], b: data[i + 2] });
}
sheet = autoPickSheet(samples);
}
const keep = sheet === 'green' ? greenKeep : blackKeep;
const s = Math.max(0, Math.min(100, params.sensitivity));
const bitset = new Uint8Array(w * h);
let slabPx = 0;
for (let i = 0, p = 0; i < data.length; i += 4, p++) {
if (keep(data[i], data[i + 1], data[i + 2], s)) {
bitset[p] = 1;
slabPx++;
}
}
if (slabPx === 0) {
return { id, kind: 'ready', slabPx: 0, bitset, bbox: { corners: [], longSidePx: 0, angleDeg: 0 }, slabBounds: { x: 0, y: 0, w: 0, h: 0 } };
}
const bbox = minAreaRect(bitset, w, h);
if (!bbox) {
return { id, kind: 'ready', slabPx: 0, bitset, bbox: { corners: [], longSidePx: 0, angleDeg: 0 }, slabBounds: { x: 0, y: 0, w: 0, h: 0 } };
}
return { id, kind: 'ready', slabPx, bitset, bbox, slabBounds: slabBounds(bitset, w, h) };
}
self.onmessage = (e: MessageEvent<WorkerIn>) => {
const msg = e.data;
if (msg.id < current) return; // superseded by a newer job
current = msg.id;
if (msg.kind === 'init') {
if (bitmap) {
bitmap.close();
bitmap = null;
}
data = null;
w = h = 0;
decodeOrientedBitmap(msg.blob)
.then((bmp) => {
w = bmp.width;
h = bmp.height;
const canvas = new OffscreenCanvas(w, h);
const ctx = canvas.getContext('2d')!;
ctx.drawImage(bmp, 0, 0);
data = ctx.getImageData(0, 0, w, h).data;
bitmap = bmp;
self.postMessage({ id: msg.id, kind: 'decoded', w, h } as WorkerOut);
})
.catch((err) => self.postMessage({ id: msg.id, kind: 'failed', reason: String(err) } satisfies WorkerOut));
return;
}
if (msg.kind === 'invalidate') {
if (bitmap) {
bitmap.close();
bitmap = null;
}
data = null;
w = h = 0;
return;
}
try {
const out = runMask({ sheet: msg.sheet, sensitivity: msg.sensitivity }, msg.id);
if (out.kind === 'ready') {
self.postMessage(out, [out.bitset.buffer]);
} else {
self.postMessage(out);
}
} catch (err) {
self.postMessage({ id: msg.id, kind: 'failed', reason: String(err) } satisfies WorkerOut);
}
};
export {};