03d3fcf460
Lower the axial body threshold while retaining fringe rejection, minimize length-only tip inset, and make sheet sensitivity curves testable and conservative at zero.
90 lines
3.0 KiB
TypeScript
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 {};
|