feat: add OpenCV loader, Otsu threshold and margin-aware mask tracing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8d597e856e
commit
5e4eebaaa2
5 files changed
+231
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import type { Mat } from '@techstark/opencv-js';
|
||||
import type { CV } from './opencv-loader';
|
||||
import type { Point, RasterImage, Shape } from './types';
|
||||
|
||||
/** Blurred grayscale; shared by thresholding and Otsu so both see the same pixels. */
|
||||
function toGray(cv: CV, image: RasterImage): Mat {
|
||||
const src = cv.matFromImageData(image as unknown as ImageData);
|
||||
const gray = new cv.Mat();
|
||||
try {
|
||||
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
|
||||
cv.GaussianBlur(gray, gray, new cv.Size(5, 5), 0);
|
||||
return gray;
|
||||
} finally {
|
||||
src.delete();
|
||||
}
|
||||
}
|
||||
|
||||
export function otsuThreshold(cv: CV, image: RasterImage): number {
|
||||
const gray = toGray(cv, image);
|
||||
const out = new cv.Mat();
|
||||
try {
|
||||
return Math.round(cv.threshold(gray, out, 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU));
|
||||
} finally {
|
||||
gray.delete();
|
||||
out.delete();
|
||||
}
|
||||
}
|
||||
|
||||
export function traceMask(cv: CV, mask: Mat, marginPx: number): Shape | null {
|
||||
const radius = Math.round(Math.abs(marginPx));
|
||||
// Padding keeps dilation from being clipped and contours off the image edge.
|
||||
const pad = radius + 2;
|
||||
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);
|
||||
}
|
||||
cv.findContours(padded, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE);
|
||||
|
||||
const count = contours.size();
|
||||
const h = hierarchy.data32S; // per contour: [next, prev, firstChild, parent]
|
||||
let best = -1;
|
||||
let bestArea = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (h[i * 4 + 3] !== -1) continue;
|
||||
const c = contours.get(i);
|
||||
const area = cv.contourArea(c);
|
||||
c.delete();
|
||||
if (area > bestArea) {
|
||||
bestArea = area;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
if (best === -1) return null;
|
||||
|
||||
const ring = (i: number): Point[] => {
|
||||
const c = contours.get(i);
|
||||
const d = c.data32S;
|
||||
const pts: Point[] = [];
|
||||
for (let k = 0; k < d.length; k += 2) pts.push({ x: d[k] - pad, y: d[k + 1] - pad });
|
||||
c.delete();
|
||||
return pts;
|
||||
};
|
||||
const holes: Point[][] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (h[i * 4 + 3] === best) holes.push(ring(i));
|
||||
}
|
||||
return { outer: ring(best), holes };
|
||||
} finally {
|
||||
padded.delete();
|
||||
contours.delete();
|
||||
hierarchy.delete();
|
||||
kernel?.delete();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user