diff --git a/src/app/calibration-ui.test.ts b/src/app/calibration-ui.test.ts index 0f42a38..bae2934 100644 --- a/src/app/calibration-ui.test.ts +++ b/src/app/calibration-ui.test.ts @@ -1,22 +1,22 @@ import { describe, it, expect } from 'vitest'; import { residuals } from './calibration-ui'; -import { estimateHomography } from '../geometry/homography'; +import { estimatePolyWarp } from '../geometry/polywarp'; import type { Vec2 } from '../types'; describe('calibration residuals', () => { - it('reports near-zero error for a clean fit', () => { + it('reports near-zero error for a clean degree-1 fit', () => { const machine: Vec2[] = [[0, 0], [100, 0], [100, 100], [0, 100]]; - const image: Vec2[] = [[10, 10], [210, 12], [208, 210], [8, 208]]; - const H = estimateHomography(machine, image); - const errs = residuals(H, machine, image); - expect(Math.max(...errs)).toBeLessThan(1e-6); + const image: Vec2[] = machine.map((p): Vec2 => [p[0] * 0.5 + 5, p[1] * 0.5 + 5]); + const w = estimatePolyWarp(machine, image, 1); + const errs = residuals(w, machine, image); + expect(Math.max(...errs)).toBeLessThan(1e-4); }); it('flags a mismatched point with a large residual', () => { - const machine: Vec2[] = [[0, 0], [100, 0], [100, 100], [0, 100], [50, 50]]; - const image: Vec2[] = [[0, 0], [100, 0], [100, 100], [0, 100], [80, 20]]; // last is wrong - const H = estimateHomography(machine, image); - const errs = residuals(H, machine, image); - expect(errs[4]!).toBeGreaterThan(5); + const machine: Vec2[] = [[0, 0], [100, 0], [100, 100], [0, 100], [50, 50], [25, 75], [75, 25]]; + const image: Vec2[] = [[0, 0], [50, 0], [50, 50], [0, 50], [25, 25], [12.5, 37.5], [90, 5]]; // last is wrong + const w = estimatePolyWarp(machine, image, 1); + const errs = residuals(w, machine, image); + expect(errs[6]!).toBeGreaterThan(5); }); }); diff --git a/src/app/calibration-ui.ts b/src/app/calibration-ui.ts index 68ed23a..554974b 100644 --- a/src/app/calibration-ui.ts +++ b/src/app/calibration-ui.ts @@ -1,11 +1,11 @@ -import type { Vec2, Mat3 } from '../types'; -import { estimateHomography, applyHomography } from '../geometry/homography'; +import type { Vec2, PolyWarp } from '../types'; +import { estimatePolyWarp, applyPolyWarp } from '../geometry/polywarp'; -/** Per-point reprojection error in pixels: |H·machine − image|. */ -export function residuals(H: Mat3, machine: Vec2[], image: Vec2[]): number[] { +/** Per-point reprojection error in normalized units: |warp(machine) − image|. */ +export function residuals(warp: PolyWarp, machine: Vec2[], image: Vec2[]): number[] { if (machine.length !== image.length) throw new Error('residuals: machine/image length mismatch'); return machine.map((m, i) => { - const p = applyHomography(H, m); + const p = applyPolyWarp(warp, m); return Math.hypot(p[0] - image[i]![0], p[1] - image[i]![1]); }); } @@ -18,16 +18,15 @@ interface CalibPoint { export interface CalibrationDeps { panel: HTMLElement; overlay: HTMLCanvasElement; - onHomography: (H: Mat3, points: CalibPoint[]) => void; + onWarp: (w: PolyWarp, points: CalibPoint[]) => void; } /** * Renders the calibration panel. Flow: enter machine X/Y → click "Click point in video" → - * click the spindle tip in the video → repeat ≥4× → "Compute". + * click the spindle tip in the video → repeat ≥10× → choose degree → "Compute". * - * Captured image points and the resulting homography are in normalized [0,1] camera-frame - * coordinates (machine-mm → normalized image), so they are display-size independent. - * The JSON output structure stays the same (imagePoints now hold normalized values). + * Image points and the resulting warp are in normalized [0,1] camera-frame coordinates + * (machine-mm ↔ normalized image), so they are display-size independent. */ export function mountCalibration(deps: CalibrationDeps): void { const { panel, overlay } = deps; @@ -36,7 +35,7 @@ export function mountCalibration(deps: CalibrationDeps): void { panel.innerHTML = `