perf: prepare the lighting-corrected image once per photo

Threshold, click, scale and margin changes now reuse it, so each update
skips the ~200 ms grayscale and lighting fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
immortaly007andClaude Opus 5.5 committed 2026-10-15 15:22:36 +02:00
1 parent 8ea525a1e8
commit 077c3cc31d
3 files changed
+60 -15

No files matched your search

+16 -6
View File
@@ -1,4 +1,4 @@
import { detect, otsuThreshold } from './detect';
import { detectPrepared, prepareImage, preparedThreshold, type PreparedImage } from './detect';
import { prepareForExport } from './geometry';
import { loadPhoto } from './load-image';
import { shapeFromPolygons } from './margin';
@@ -16,6 +16,8 @@ export class AppState {
cv = $state.raw<CV | null>(null);
cvError = $state<string | null>(null);
image = $state.raw<RasterImage | null>(null);
/** Lighting-corrected grayscale of `image`, computed once per photo; owns WASM memory. */
prepared = $state.raw<PreparedImage | null>(null);
loadError = $state<string | null>(null);
tool = $state<Tool>('none');
@@ -38,12 +40,13 @@ export class AppState {
marginPx = $derived(this.pxPerMm ? this.marginMm * this.pxPerMm : 0);
shapePx = $derived.by((): Shape | null => {
const { cv, image } = this;
const { cv, image, prepared } = 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);
}
return detect(cv, image, {
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,
});
@@ -53,7 +56,7 @@ export class AppState {
setCv(cv: CV) {
this.cv = cv;
if (this.image) this.threshold = otsuThreshold(cv, this.image);
this.prepare();
}
setImage(image: RasterImage) {
@@ -62,7 +65,14 @@ export class AppState {
this.scalePoints = [];
this.seed = null;
this.clearManual();
this.threshold = this.cv ? otsuThreshold(this.cv, image) : DEFAULT_THRESHOLD;
this.prepare();
}
private prepare() {
this.prepared?.gray.delete();
const { cv, image } = this;
this.prepared = cv && image ? prepareImage(cv, image) : null;
this.threshold = cv && this.prepared ? preparedThreshold(cv, this.prepared) : DEFAULT_THRESHOLD;
}
async loadFile(file: Blob) {
@@ -75,7 +85,7 @@ export class AppState {
}
autoThreshold() {
if (this.cv && this.image) this.threshold = otsuThreshold(this.cv, this.image);
if (this.cv && this.prepared) this.threshold = preparedThreshold(this.cv, this.prepared);
}
handleImageClick(p: Point) {