perf: cache the detected piece so margin changes only re-trace it
Margin slider steps drop from ~800 ms to under 70 ms in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e925ce66d5
commit
353577d5ec
3 files changed
+63
-10
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { detectPrepared, disposePrepared, prepareImage, preparedThreshold, type PreparedImage } from './detect';
|
||||
import type { Mat } from '@techstark/opencv-js';
|
||||
import { disposePrepared, prepareImage, preparedThreshold, segmentPiece, traceMask, type PreparedImage } from './detect';
|
||||
import { prepareForExport } from './geometry';
|
||||
import { loadPhoto } from './load-image';
|
||||
import { shapeFromPolygons } from './margin';
|
||||
@@ -39,17 +40,30 @@ export class AppState {
|
||||
scaleInvalid = $derived(this.scalePoints.length === 2 && this.lengthMm !== null && this.pxPerMm === null);
|
||||
marginPx = $derived(this.pxPerMm ? this.marginMm * this.pxPerMm : 0);
|
||||
|
||||
private lastPieceMask: Mat | null = null;
|
||||
|
||||
/** The detected piece before the margin; cached so margin changes only re-trace it. */
|
||||
pieceMask = $derived.by((): Mat | null => {
|
||||
const { cv, prepared } = this;
|
||||
const next = cv && prepared && this.mode === 'auto'
|
||||
? segmentPiece(cv, prepared, {
|
||||
threshold: this.threshold, seed: this.seed,
|
||||
minHolePx: this.pxPerMm ? MIN_HOLE_MM * this.pxPerMm : 0,
|
||||
})
|
||||
: null;
|
||||
// The previous mask lives in WASM memory and is unreachable once replaced.
|
||||
this.lastPieceMask?.delete();
|
||||
this.lastPieceMask = next;
|
||||
return next;
|
||||
});
|
||||
|
||||
shapePx = $derived.by((): Shape | null => {
|
||||
const { cv, image, prepared } = this;
|
||||
const { cv, image } = this;
|
||||
if (!cv || !image) return null;
|
||||
if (this.mode === 'manual') {
|
||||
return shapeFromPolygons(cv, image.width, image.height, { outer: this.manualOuter, holes: this.manualHoles }, this.marginPx);
|
||||
}
|
||||
if (!prepared) return null;
|
||||
return detectPrepared(cv, prepared, {
|
||||
threshold: this.threshold, seed: this.seed, marginPx: this.marginPx,
|
||||
minHolePx: this.pxPerMm ? MIN_HOLE_MM * this.pxPerMm : 0,
|
||||
});
|
||||
return this.pieceMask ? traceMask(cv, this.pieceMask, this.marginPx) : null;
|
||||
});
|
||||
|
||||
exportShape = $derived(this.shapePx && this.pxPerMm ? prepareForExport(this.shapePx, this.pxPerMm) : null);
|
||||
|
||||
+18
-2
@@ -218,6 +218,20 @@ export function detect(cv: CV, image: RasterImage, params: DetectParams): Shape
|
||||
}
|
||||
|
||||
export function detectPrepared(cv: CV, prepared: PreparedImage, params: DetectParams): Shape | null {
|
||||
const piece = segmentPiece(cv, prepared, params);
|
||||
if (!piece) return null;
|
||||
try {
|
||||
return traceMask(cv, piece, params.marginPx);
|
||||
} finally {
|
||||
piece.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The selected piece as a mask (255 = piece), before the margin. Cache it: margin changes then only
|
||||
* need `traceMask`. Caller deletes the returned Mat.
|
||||
*/
|
||||
export function segmentPiece(cv: CV, prepared: PreparedImage, params: Omit<DetectParams, 'marginPx'>): Mat | null {
|
||||
const { gray, background } = prepared;
|
||||
const width = gray.cols;
|
||||
const height = gray.rows;
|
||||
@@ -226,6 +240,7 @@ export function detectPrepared(cv: CV, prepared: PreparedImage, params: DetectPa
|
||||
const labels = new cv.Mat();
|
||||
const stats = new cv.Mat();
|
||||
const centroids = new cv.Mat();
|
||||
let found = false;
|
||||
try {
|
||||
const segment = (dark: boolean, seed: Point | null) => {
|
||||
cv.threshold(gray, mask, params.threshold, 255, dark ? cv.THRESH_BINARY_INV : cv.THRESH_BINARY);
|
||||
@@ -245,9 +260,10 @@ export function detectPrepared(cv: CV, prepared: PreparedImage, params: DetectPa
|
||||
for (let i = 0; i < lab.length; i++) m[i] = lab[i] === label ? 255 : 0;
|
||||
trimTexturedEdges(cv, mask, prepared.texture, prepared.textureRadius);
|
||||
fillSmallHoles(cv, mask, params.minHolePx);
|
||||
return traceMask(cv, mask, params.marginPx);
|
||||
found = true;
|
||||
return mask;
|
||||
} finally {
|
||||
mask.delete();
|
||||
if (!found) mask.delete();
|
||||
kernel.delete();
|
||||
labels.delete();
|
||||
stats.delete();
|
||||
|
||||
+24
-1
@@ -1,5 +1,5 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { detect, detectPrepared, disposePrepared, otsuThreshold, prepareImage, preparedThreshold } from '../src/lib/detect';
|
||||
import { detect, detectPrepared, disposePrepared, otsuThreshold, prepareImage, preparedThreshold, segmentPiece, traceMask } from '../src/lib/detect';
|
||||
import { bounds } from '../src/lib/geometry';
|
||||
import { loadOpenCV, type CV } from '../src/lib/opencv-loader';
|
||||
import type { DetectParams } from '../src/lib/types';
|
||||
@@ -149,4 +149,27 @@ describe('detect', () => {
|
||||
const b = bounds(detect(cv, plywood, params({ threshold: 140 }))!.outer);
|
||||
near(b.minX, 150); near(b.maxX, 449); near(b.minY, 100); near(b.maxY, 299);
|
||||
});
|
||||
|
||||
it('segments once and applies different margins to the same piece mask', () => {
|
||||
const prepared = prepareImage(cv, plateScene);
|
||||
const piece = segmentPiece(cv, prepared, { threshold: 128, seed: null, minHolePx: 0 })!;
|
||||
try {
|
||||
for (const marginPx of [0, -5, 3]) {
|
||||
const direct = detectPrepared(cv, prepared, params({ marginPx }))!;
|
||||
expect(bounds(traceMask(cv, piece, marginPx)!.outer)).toEqual(bounds(direct.outer));
|
||||
}
|
||||
} finally {
|
||||
piece.delete();
|
||||
disposePrepared(prepared);
|
||||
}
|
||||
});
|
||||
|
||||
it('segmentPiece returns null when nothing is found', () => {
|
||||
const prepared = prepareImage(cv, grayImage(100, 100, () => 30));
|
||||
try {
|
||||
expect(segmentPiece(cv, prepared, { threshold: 128, seed: null, minHolePx: 0 })).toBeNull();
|
||||
} finally {
|
||||
disposePrepared(prepared);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user