refactor: config validates PolyWarp calibration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0130188416
commit
3e9878ac42
3 changed files with 48 additions and 22 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { loadConfig, DEFAULT_RENDER } from './config';
|
||||
import { estimatePolyWarp } from './geometry/polywarp';
|
||||
|
||||
function mockFetch(body: unknown, ok = true) {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
|
|
@ -20,18 +21,16 @@ describe('loadConfig', () => {
|
|||
});
|
||||
|
||||
it('keeps a valid calibration', async () => {
|
||||
const calibration = {
|
||||
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
homography: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
||||
};
|
||||
const square = [[0, 0], [10, 0], [10, 10], [0, 10]] as [number, number][];
|
||||
const warp = estimatePolyWarp(square, square, 1);
|
||||
const calibration = { imagePoints: square, machinePoints: square, warp };
|
||||
mockFetch({ streamUrl: 'x', calibration });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.calibration).toEqual(calibration);
|
||||
});
|
||||
|
||||
it('drops a malformed calibration to null', async () => {
|
||||
mockFetch({ streamUrl: 'x', calibration: { homography: [1, 2, 3] } });
|
||||
mockFetch({ streamUrl: 'x', calibration: { warp: { degree: 2 } } });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.calibration).toBeNull();
|
||||
});
|
||||
|
|
@ -50,12 +49,11 @@ describe('loadConfig', () => {
|
|||
expect(cfg.renderDefaults).not.toHaveProperty('bogus'); // extra key dropped
|
||||
});
|
||||
|
||||
it('rejects a calibration whose homography contains NaN', async () => {
|
||||
const calibration = {
|
||||
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
||||
homography: [1, 0, 0, 0, 1, 0, 0, 0, NaN],
|
||||
};
|
||||
it('rejects a calibration whose warp coefficients contain NaN', async () => {
|
||||
const square = [[0, 0], [10, 0], [10, 10], [0, 10]] as [number, number][];
|
||||
const warp = estimatePolyWarp(square, square, 1);
|
||||
warp.fwd.cu[0] = NaN;
|
||||
const calibration = { imagePoints: square, machinePoints: square, warp };
|
||||
mockFetch({ streamUrl: 'x', calibration });
|
||||
const cfg = await loadConfig('config.json');
|
||||
expect(cfg.calibration).toBeNull();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AppConfig, Calibration, Mat3, Vec2, RenderStyle } from './types';
|
||||
import type { AppConfig, Calibration, PolyMap, PolyWarp, Vec2, RenderStyle } from './types';
|
||||
|
||||
export const DEFAULT_RENDER: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth: 1.5 };
|
||||
|
||||
|
|
@ -10,18 +10,46 @@ function isVec2Array(v: unknown): v is Vec2[] {
|
|||
return Array.isArray(v) && v.every((p) => Array.isArray(p) && p.length === 2 && p.every(isFiniteNumber));
|
||||
}
|
||||
|
||||
function isNumArray(v: unknown, len: number): v is number[] {
|
||||
return Array.isArray(v) && v.length === len && v.every(isFiniteNumber);
|
||||
}
|
||||
|
||||
function isVec2(v: unknown): v is Vec2 {
|
||||
return Array.isArray(v) && v.length === 2 && v.every(isFiniteNumber);
|
||||
}
|
||||
|
||||
function termCount(degree: number): number {
|
||||
return ((degree + 1) * (degree + 2)) / 2;
|
||||
}
|
||||
|
||||
function parsePolyMap(m: unknown, degree: number): PolyMap | null {
|
||||
if (!m || typeof m !== 'object') return null;
|
||||
const o = m as Record<string, unknown>;
|
||||
const norm = o.norm as Record<string, unknown> | undefined;
|
||||
if (!norm || !isVec2(norm.off) || !isVec2(norm.scl)) return null;
|
||||
const n = termCount(degree);
|
||||
if (!isNumArray(o.cu, n) || !isNumArray(o.cv, n)) return null;
|
||||
return { degree, norm: { off: norm.off as Vec2, scl: norm.scl as Vec2 }, cu: o.cu as number[], cv: o.cv as number[] };
|
||||
}
|
||||
|
||||
function parseWarp(w: unknown): PolyWarp | null {
|
||||
if (!w || typeof w !== 'object') return null;
|
||||
const o = w as Record<string, unknown>;
|
||||
if (!isFiniteNumber(o.degree)) return null;
|
||||
const fwd = parsePolyMap(o.fwd, o.degree);
|
||||
const inv = parsePolyMap(o.inv, o.degree);
|
||||
if (!fwd || !inv) return null;
|
||||
return { degree: o.degree, fwd, inv };
|
||||
}
|
||||
|
||||
function parseCalibration(c: unknown): Calibration | null {
|
||||
if (!c || typeof c !== 'object') return null;
|
||||
const obj = c as Record<string, unknown>;
|
||||
if (!isVec2Array(obj.imagePoints) || !isVec2Array(obj.machinePoints)) return null;
|
||||
if (!Array.isArray(obj.homography) || obj.homography.length !== 9) return null;
|
||||
if (!obj.homography.every(isFiniteNumber)) return null;
|
||||
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
||||
return {
|
||||
imagePoints: obj.imagePoints,
|
||||
machinePoints: obj.machinePoints,
|
||||
homography: obj.homography as Mat3,
|
||||
};
|
||||
const warp = parseWarp(obj.warp);
|
||||
if (!warp) return null;
|
||||
return { imagePoints: obj.imagePoints, machinePoints: obj.machinePoints, warp };
|
||||
}
|
||||
|
||||
function parseRenderStyle(r: unknown): RenderStyle {
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ export interface Calibration {
|
|||
imagePoints: Vec2[];
|
||||
/** Corresponding machine coordinates, mm. */
|
||||
machinePoints: Vec2[];
|
||||
/** machine-mm → normalized [0,1] image coords. */
|
||||
homography: Mat3;
|
||||
/** Bidirectional polynomial warp (machine-mm ↔ normalized image). */
|
||||
warp: PolyWarp;
|
||||
}
|
||||
|
||||
export interface RenderStyle {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue