feat: detect piece outline with threshold, seed selection and margin
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e4eebaaa2
commit
9e48c5d762
2 files changed
+156
-1
No files matched your search
+69
-1
@@ -1,6 +1,6 @@
|
||||
import type { Mat } from '@techstark/opencv-js';
|
||||
import type { CV } from './opencv-loader';
|
||||
import type { Point, RasterImage, Shape } from './types';
|
||||
import type { DetectParams, 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 {
|
||||
@@ -79,3 +79,71 @@ export function traceMask(cv: CV, mask: Mat, marginPx: number): Shape | null {
|
||||
kernel?.delete();
|
||||
}
|
||||
}
|
||||
|
||||
export function detect(cv: CV, image: RasterImage, params: DetectParams): Shape | null {
|
||||
const gray = toGray(cv, image);
|
||||
const mask = new cv.Mat();
|
||||
const kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, new cv.Size(5, 5));
|
||||
const labels = new cv.Mat();
|
||||
const stats = new cv.Mat();
|
||||
const centroids = new cv.Mat();
|
||||
try {
|
||||
const type = params.invert ? cv.THRESH_BINARY_INV : cv.THRESH_BINARY;
|
||||
cv.threshold(gray, mask, params.threshold, 255, type);
|
||||
cv.morphologyEx(mask, mask, cv.MORPH_OPEN, kernel);
|
||||
cv.morphologyEx(mask, mask, cv.MORPH_CLOSE, kernel);
|
||||
const count = cv.connectedComponentsWithStats(mask, labels, stats, centroids, 8, cv.CV_32S);
|
||||
|
||||
const label = pickComponent(cv, labels, stats, count, image.width, image.height, params.seed);
|
||||
if (label === null) return null;
|
||||
|
||||
const lab = labels.data32S;
|
||||
const m = mask.data;
|
||||
for (let i = 0; i < lab.length; i++) m[i] = lab[i] === label ? 255 : 0;
|
||||
return traceMask(cv, mask, params.marginPx);
|
||||
} finally {
|
||||
gray.delete();
|
||||
mask.delete();
|
||||
kernel.delete();
|
||||
labels.delete();
|
||||
stats.delete();
|
||||
centroids.delete();
|
||||
}
|
||||
}
|
||||
|
||||
function pickComponent(
|
||||
cv: CV, labels: Mat, stats: Mat, count: number, width: number, height: number, seed: Point | null,
|
||||
): number | null {
|
||||
const stat = (label: number, field: number) => stats.intAt(label, field);
|
||||
const box = (label: number) => {
|
||||
const left = stat(label, cv.CC_STAT_LEFT);
|
||||
const top = stat(label, cv.CC_STAT_TOP);
|
||||
return { left, top, right: left + stat(label, cv.CC_STAT_WIDTH), bottom: top + stat(label, cv.CC_STAT_HEIGHT) };
|
||||
};
|
||||
|
||||
if (seed) {
|
||||
const x = Math.round(seed.x);
|
||||
const y = Math.round(seed.y);
|
||||
if (x >= 0 && y >= 0 && x < width && y < height) {
|
||||
const label = labels.intAt(y, x);
|
||||
if (label !== 0) {
|
||||
const b = box(label);
|
||||
const coversImage = b.left === 0 && b.top === 0 && b.right === width && b.bottom === height;
|
||||
return coversImage ? null : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let best: number | null = null;
|
||||
let bestArea = 0;
|
||||
for (let label = 1; label < count; label++) {
|
||||
const b = box(label);
|
||||
if (b.left === 0 || b.top === 0 || b.right === width || b.bottom === height) continue;
|
||||
const area = stat(label, cv.CC_STAT_AREA);
|
||||
if (area > bestArea) {
|
||||
bestArea = area;
|
||||
best = label;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
Reference in new issue
Block a user