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 { prepareForExport } from './geometry';
|
||||||
import { loadPhoto } from './load-image';
|
import { loadPhoto } from './load-image';
|
||||||
import { shapeFromPolygons } from './margin';
|
import { shapeFromPolygons } from './margin';
|
||||||
@@ -39,17 +40,30 @@ export class AppState {
|
|||||||
scaleInvalid = $derived(this.scalePoints.length === 2 && this.lengthMm !== null && this.pxPerMm === null);
|
scaleInvalid = $derived(this.scalePoints.length === 2 && this.lengthMm !== null && this.pxPerMm === null);
|
||||||
marginPx = $derived(this.pxPerMm ? this.marginMm * this.pxPerMm : 0);
|
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 => {
|
shapePx = $derived.by((): Shape | null => {
|
||||||
const { cv, image, prepared } = this;
|
const { cv, image } = this;
|
||||||
if (!cv || !image) return null;
|
if (!cv || !image) return null;
|
||||||
if (this.mode === 'manual') {
|
if (this.mode === 'manual') {
|
||||||
return shapeFromPolygons(cv, image.width, image.height, { outer: this.manualOuter, holes: this.manualHoles }, this.marginPx);
|
return shapeFromPolygons(cv, image.width, image.height, { outer: this.manualOuter, holes: this.manualHoles }, this.marginPx);
|
||||||
}
|
}
|
||||||
if (!prepared) return null;
|
return this.pieceMask ? traceMask(cv, this.pieceMask, this.marginPx) : null;
|
||||||
return detectPrepared(cv, prepared, {
|
|
||||||
threshold: this.threshold, seed: this.seed, marginPx: this.marginPx,
|
|
||||||
minHolePx: this.pxPerMm ? MIN_HOLE_MM * this.pxPerMm : 0,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
exportShape = $derived(this.shapePx && this.pxPerMm ? prepareForExport(this.shapePx, this.pxPerMm) : 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 {
|
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 { gray, background } = prepared;
|
||||||
const width = gray.cols;
|
const width = gray.cols;
|
||||||
const height = gray.rows;
|
const height = gray.rows;
|
||||||
@@ -226,6 +240,7 @@ export function detectPrepared(cv: CV, prepared: PreparedImage, params: DetectPa
|
|||||||
const labels = new cv.Mat();
|
const labels = new cv.Mat();
|
||||||
const stats = new cv.Mat();
|
const stats = new cv.Mat();
|
||||||
const centroids = new cv.Mat();
|
const centroids = new cv.Mat();
|
||||||
|
let found = false;
|
||||||
try {
|
try {
|
||||||
const segment = (dark: boolean, seed: Point | null) => {
|
const segment = (dark: boolean, seed: Point | null) => {
|
||||||
cv.threshold(gray, mask, params.threshold, 255, dark ? cv.THRESH_BINARY_INV : cv.THRESH_BINARY);
|
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;
|
for (let i = 0; i < lab.length; i++) m[i] = lab[i] === label ? 255 : 0;
|
||||||
trimTexturedEdges(cv, mask, prepared.texture, prepared.textureRadius);
|
trimTexturedEdges(cv, mask, prepared.texture, prepared.textureRadius);
|
||||||
fillSmallHoles(cv, mask, params.minHolePx);
|
fillSmallHoles(cv, mask, params.minHolePx);
|
||||||
return traceMask(cv, mask, params.marginPx);
|
found = true;
|
||||||
|
return mask;
|
||||||
} finally {
|
} finally {
|
||||||
mask.delete();
|
if (!found) mask.delete();
|
||||||
kernel.delete();
|
kernel.delete();
|
||||||
labels.delete();
|
labels.delete();
|
||||||
stats.delete();
|
stats.delete();
|
||||||
|
|||||||
+24
-1
@@ -1,5 +1,5 @@
|
|||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
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 { bounds } from '../src/lib/geometry';
|
||||||
import { loadOpenCV, type CV } from '../src/lib/opencv-loader';
|
import { loadOpenCV, type CV } from '../src/lib/opencv-loader';
|
||||||
import type { DetectParams } from '../src/lib/types';
|
import type { DetectParams } from '../src/lib/types';
|
||||||
@@ -149,4 +149,27 @@ describe('detect', () => {
|
|||||||
const b = bounds(detect(cv, plywood, params({ threshold: 140 }))!.outer);
|
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);
|
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