feat: run manual polygons through the shared margin pipeline
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e48c5d762
commit
fad7deed5d
2 files changed
+63
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import type { Mat } from '@techstark/opencv-js';
|
||||
import { traceMask } from './detect';
|
||||
import type { CV } from './opencv-loader';
|
||||
import type { Point, Shape } from './types';
|
||||
|
||||
export function shapeFromPolygons(cv: CV, width: number, height: number, polygons: Shape, marginPx: number): Shape | null {
|
||||
if (polygons.outer.length < 3) return null;
|
||||
const mask = cv.Mat.zeros(height, width, cv.CV_8UC1) as Mat;
|
||||
try {
|
||||
fill(cv, mask, [polygons.outer], 255);
|
||||
fill(cv, mask, polygons.holes.filter((h) => h.length >= 3), 0);
|
||||
return traceMask(cv, mask, marginPx);
|
||||
} finally {
|
||||
mask.delete();
|
||||
}
|
||||
}
|
||||
|
||||
function fill(cv: CV, mask: Mat, rings: Point[][], value: number) {
|
||||
if (rings.length === 0) return;
|
||||
const mats = rings.map((ring) =>
|
||||
cv.matFromArray(ring.length, 1, cv.CV_32SC2, ring.flatMap((p) => [Math.round(p.x), Math.round(p.y)])),
|
||||
);
|
||||
const vector = new cv.MatVector();
|
||||
try {
|
||||
mats.forEach((m) => vector.push_back(m));
|
||||
cv.fillPoly(mask, vector, new cv.Scalar(value));
|
||||
} finally {
|
||||
vector.delete();
|
||||
mats.forEach((m) => m.delete());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { bounds } from '../src/lib/geometry';
|
||||
import { shapeFromPolygons } from '../src/lib/margin';
|
||||
import { loadOpenCV, type CV } from '../src/lib/opencv-loader';
|
||||
|
||||
let cv: CV;
|
||||
beforeAll(async () => {
|
||||
({ cv } = await loadOpenCV());
|
||||
});
|
||||
|
||||
const outer = [{ x: 40, y: 30 }, { x: 160, y: 30 }, { x: 160, y: 130 }, { x: 40, y: 130 }];
|
||||
const hole = [{ x: 90, y: 70 }, { x: 110, y: 70 }, { x: 110, y: 90 }, { x: 90, y: 90 }];
|
||||
const near = (actual: number, expected: number) => expect(Math.abs(actual - expected)).toBeLessThanOrEqual(2);
|
||||
|
||||
describe('shapeFromPolygons', () => {
|
||||
it('traces the drawn outline and hole', () => {
|
||||
const shape = shapeFromPolygons(cv, 200, 160, { outer, holes: [hole] }, 0)!;
|
||||
const b = bounds(shape.outer);
|
||||
near(b.minX, 40); near(b.maxX, 160); near(b.minY, 30); near(b.maxY, 130);
|
||||
expect(shape.holes).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('applies the margin to outline and hole', () => {
|
||||
const shape = shapeFromPolygons(cv, 200, 160, { outer, holes: [hole] }, -5)!;
|
||||
near(bounds(shape.outer).minX, 45);
|
||||
near(bounds(shape.holes[0]).minX, 84);
|
||||
});
|
||||
|
||||
it('returns null for an outline with fewer than 3 points', () => {
|
||||
expect(shapeFromPolygons(cv, 200, 160, { outer: outer.slice(0, 2), holes: [] }, 0)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user