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 contours = new cv.MatVector();
|
||||
const hierarchy = new cv.Mat();
|
||||
let kernel: Mat | null = null;
|
||||
try {
|
||||
cv.copyMakeBorder(mask, padded, pad, pad, pad, pad, cv.BORDER_CONSTANT, new cv.Scalar(0));
|
||||
if (radius > 0) {
|
||||
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);
|
||||
}
|
||||
if (radius > 0) offsetMask(cv, padded, marginPx < 0 ? -radius : radius);
|
||||
cv.findContours(padded, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
const count = contours.size();
|
||||
@@ -146,7 +141,27 @@ export function traceMask(cv: CV, mask: Mat, marginPx: number): Shape | null {
|
||||
padded.delete();
|
||||
contours.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', () => {
|
||||
it('lands between background and piece brightness', () => {
|
||||
const image = grayImage(200, 160, (x, y) => (inRect(x, y, 40, 30, 160, 130) ? 220 : 30));
|
||||
|
||||
Reference in new issue
Block a user