fix: apply the safety margin with a distance transform
An elliptical erode kernel costs radius² per pixel. While typing a length, an intermediate value like "3" mm made the margin hundreds of pixels and froze the page for minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55a25ba978
commit
8ea525a1e8
2 files changed
+33
-7
No files matched your search
+22
-7
@@ -103,14 +103,9 @@ export function traceMask(cv: CV, mask: Mat, marginPx: number): Shape | null {
|
|||||||
const padded = new cv.Mat();
|
const padded = new cv.Mat();
|
||||||
const contours = new cv.MatVector();
|
const contours = new cv.MatVector();
|
||||||
const hierarchy = new cv.Mat();
|
const hierarchy = new cv.Mat();
|
||||||
let kernel: Mat | null = null;
|
|
||||||
try {
|
try {
|
||||||
cv.copyMakeBorder(mask, padded, pad, pad, pad, pad, cv.BORDER_CONSTANT, new cv.Scalar(0));
|
cv.copyMakeBorder(mask, padded, pad, pad, pad, pad, cv.BORDER_CONSTANT, new cv.Scalar(0));
|
||||||
if (radius > 0) {
|
if (radius > 0) offsetMask(cv, padded, marginPx < 0 ? -radius : radius);
|
||||||
kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, new cv.Size(2 * radius + 1, 2 * radius + 1));
|
|
||||||
if (marginPx < 0) cv.erode(padded, padded, kernel);
|
|
||||||
else cv.dilate(padded, padded, kernel);
|
|
||||||
}
|
|
||||||
cv.findContours(padded, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
|
cv.findContours(padded, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
|
||||||
|
|
||||||
const count = contours.size();
|
const count = contours.size();
|
||||||
@@ -146,7 +141,27 @@ export function traceMask(cv: CV, mask: Mat, marginPx: number): Shape | null {
|
|||||||
padded.delete();
|
padded.delete();
|
||||||
contours.delete();
|
contours.delete();
|
||||||
hierarchy.delete();
|
hierarchy.delete();
|
||||||
kernel?.delete();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erodes (negative) or dilates (positive) a mask by a radius, in place. Uses a distance transform:
|
||||||
|
* an elliptical kernel costs radius² per pixel and froze the UI for large margins.
|
||||||
|
*/
|
||||||
|
function offsetMask(cv: CV, mask: Mat, radius: number) {
|
||||||
|
const source = new cv.Mat();
|
||||||
|
const dist = new cv.Mat();
|
||||||
|
try {
|
||||||
|
// Dilating the piece is eroding the background, so measure from the other side.
|
||||||
|
if (radius > 0) cv.bitwise_not(mask, source);
|
||||||
|
else mask.copyTo(source);
|
||||||
|
cv.distanceTransform(source, dist, cv.DIST_L2, cv.DIST_MASK_PRECISE);
|
||||||
|
const r = Math.abs(radius);
|
||||||
|
cv.threshold(dist, dist, r, 255, radius > 0 ? cv.THRESH_BINARY_INV : cv.THRESH_BINARY);
|
||||||
|
dist.convertTo(mask, cv.CV_8U);
|
||||||
|
} finally {
|
||||||
|
source.delete();
|
||||||
|
dist.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ describe('traceMask', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('traceMask performance', () => {
|
||||||
|
it('handles a huge margin quickly (e.g. while a length is half-typed)', () => {
|
||||||
|
const m = maskFrom(cv, 2000, 1500, (x, y) => inRect(x, y, 200, 200, 1800, 1300));
|
||||||
|
mats.push(m);
|
||||||
|
const start = performance.now();
|
||||||
|
const shape = traceMask(cv, m, -300)!;
|
||||||
|
expect(performance.now() - start).toBeLessThan(1500);
|
||||||
|
expect(Math.abs(bounds(shape.outer).minX - 500)).toBeLessThanOrEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('otsuThreshold', () => {
|
describe('otsuThreshold', () => {
|
||||||
it('lands between background and piece brightness', () => {
|
it('lands between background and piece brightness', () => {
|
||||||
const image = grayImage(200, 160, (x, y) => (inRect(x, y, 40, 30, 160, 130) ? 220 : 30));
|
const image = grayImage(200, 160, (x, y) => (inRect(x, y, 40, 30, 160, 130) ? 220 : 30));
|
||||||
|
|||||||
Reference in new issue
Block a user