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 { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
import { loadConfig, DEFAULT_RENDER } from './config';
|
import { loadConfig, DEFAULT_RENDER } from './config';
|
||||||
|
import { estimatePolyWarp } from './geometry/polywarp';
|
||||||
|
|
||||||
function mockFetch(body: unknown, ok = true) {
|
function mockFetch(body: unknown, ok = true) {
|
||||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||||
|
|
@ -20,18 +21,16 @@ describe('loadConfig', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a valid calibration', async () => {
|
it('keeps a valid calibration', async () => {
|
||||||
const calibration = {
|
const square = [[0, 0], [10, 0], [10, 10], [0, 10]] as [number, number][];
|
||||||
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
const warp = estimatePolyWarp(square, square, 1);
|
||||||
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
const calibration = { imagePoints: square, machinePoints: square, warp };
|
||||||
homography: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
|
||||||
};
|
|
||||||
mockFetch({ streamUrl: 'x', calibration });
|
mockFetch({ streamUrl: 'x', calibration });
|
||||||
const cfg = await loadConfig('config.json');
|
const cfg = await loadConfig('config.json');
|
||||||
expect(cfg.calibration).toEqual(calibration);
|
expect(cfg.calibration).toEqual(calibration);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops a malformed calibration to null', async () => {
|
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');
|
const cfg = await loadConfig('config.json');
|
||||||
expect(cfg.calibration).toBeNull();
|
expect(cfg.calibration).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
@ -50,12 +49,11 @@ describe('loadConfig', () => {
|
||||||
expect(cfg.renderDefaults).not.toHaveProperty('bogus'); // extra key dropped
|
expect(cfg.renderDefaults).not.toHaveProperty('bogus'); // extra key dropped
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects a calibration whose homography contains NaN', async () => {
|
it('rejects a calibration whose warp coefficients contain NaN', async () => {
|
||||||
const calibration = {
|
const square = [[0, 0], [10, 0], [10, 10], [0, 10]] as [number, number][];
|
||||||
imagePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
const warp = estimatePolyWarp(square, square, 1);
|
||||||
machinePoints: [[0, 0], [1, 0], [1, 1], [0, 1]],
|
warp.fwd.cu[0] = NaN;
|
||||||
homography: [1, 0, 0, 0, 1, 0, 0, 0, NaN],
|
const calibration = { imagePoints: square, machinePoints: square, warp };
|
||||||
};
|
|
||||||
mockFetch({ streamUrl: 'x', calibration });
|
mockFetch({ streamUrl: 'x', calibration });
|
||||||
const cfg = await loadConfig('config.json');
|
const cfg = await loadConfig('config.json');
|
||||||
expect(cfg.calibration).toBeNull();
|
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 };
|
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));
|
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 {
|
function parseCalibration(c: unknown): Calibration | null {
|
||||||
if (!c || typeof c !== 'object') return null;
|
if (!c || typeof c !== 'object') return null;
|
||||||
const obj = c as Record<string, unknown>;
|
const obj = c as Record<string, unknown>;
|
||||||
if (!isVec2Array(obj.imagePoints) || !isVec2Array(obj.machinePoints)) return null;
|
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;
|
if (obj.imagePoints.length !== obj.machinePoints.length || obj.imagePoints.length < 4) return null;
|
||||||
return {
|
const warp = parseWarp(obj.warp);
|
||||||
imagePoints: obj.imagePoints,
|
if (!warp) return null;
|
||||||
machinePoints: obj.machinePoints,
|
return { imagePoints: obj.imagePoints, machinePoints: obj.machinePoints, warp };
|
||||||
homography: obj.homography as Mat3,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRenderStyle(r: unknown): RenderStyle {
|
function parseRenderStyle(r: unknown): RenderStyle {
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@ export interface Calibration {
|
||||||
imagePoints: Vec2[];
|
imagePoints: Vec2[];
|
||||||
/** Corresponding machine coordinates, mm. */
|
/** Corresponding machine coordinates, mm. */
|
||||||
machinePoints: Vec2[];
|
machinePoints: Vec2[];
|
||||||
/** machine-mm → normalized [0,1] image coords. */
|
/** Bidirectional polynomial warp (machine-mm ↔ normalized image). */
|
||||||
homography: Mat3;
|
warp: PolyWarp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderStyle {
|
export interface RenderStyle {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue