import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import jpeg from 'jpeg-js'; import { beforeAll, describe, expect, it } from 'vitest'; import { detect, otsuThreshold } from '../src/lib/detect'; import { prepareForExport, shapeSize } from '../src/lib/geometry'; import { loadOpenCV, type CV } from '../src/lib/opencv-loader'; import type { Point, RasterImage } from '../src/lib/types'; type FixtureCase = { file: string; threshold: number | null; seed: Point | null; pxPerMm: number; expected: { widthMm: number; heightMm: number; holes?: number }; toleranceMm: number; }; const dir = fileURLToPath(new URL('./fixtures/', import.meta.url)); const cases: FixtureCase[] = JSON.parse(readFileSync(dir + 'manifest.json', 'utf8')); function readJpeg(path: string): RasterImage { const decoded = jpeg.decode(readFileSync(path), { useTArray: true, formatAsRGBA: true }); return { width: decoded.width, height: decoded.height, data: new Uint8ClampedArray(decoded.data) }; } describe.skipIf(cases.length === 0)('detect on real photos', () => { let cv: CV; beforeAll(async () => { ({ cv } = await loadOpenCV()); }); it.each(cases)('$file', (c) => { const image = readJpeg(dir + c.file); const threshold = c.threshold ?? otsuThreshold(cv, image); const shape = detect(cv, image, { threshold, seed: c.seed, marginPx: 0, minHolePx: 3 * c.pxPerMm }); expect(shape).not.toBeNull(); const mm = prepareForExport(shape!, c.pxPerMm)!; const size = shapeSize(mm); expect(Math.abs(size.width - c.expected.widthMm)).toBeLessThanOrEqual(c.toleranceMm); expect(Math.abs(size.height - c.expected.heightMm)).toBeLessThanOrEqual(c.toleranceMm); if (c.expected.holes !== undefined) expect(mm.holes).toHaveLength(c.expected.holes); }); });