fix: make calibration resolution-independent (normalized image coords)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1df5509b29
commit
d1577dd569
3 changed files with 19 additions and 7 deletions
|
|
@ -3,6 +3,7 @@ import { estimateHomography, applyHomography } from '../geometry/homography';
|
||||||
|
|
||||||
/** Per-point reprojection error in pixels: |H·machine − image|. */
|
/** Per-point reprojection error in pixels: |H·machine − image|. */
|
||||||
export function residuals(H: Mat3, machine: Vec2[], image: Vec2[]): number[] {
|
export function residuals(H: Mat3, machine: Vec2[], image: Vec2[]): number[] {
|
||||||
|
if (machine.length !== image.length) throw new Error('residuals: machine/image length mismatch');
|
||||||
return machine.map((m, i) => {
|
return machine.map((m, i) => {
|
||||||
const p = applyHomography(H, m);
|
const p = applyHomography(H, m);
|
||||||
return Math.hypot(p[0] - image[i]![0], p[1] - image[i]![1]);
|
return Math.hypot(p[0] - image[i]![0], p[1] - image[i]![1]);
|
||||||
|
|
@ -23,6 +24,10 @@ export interface CalibrationDeps {
|
||||||
/**
|
/**
|
||||||
* Renders the calibration panel. Flow: enter machine X/Y → click "Click point in video" →
|
* 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 ≥4× → "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).
|
||||||
*/
|
*/
|
||||||
export function mountCalibration(deps: CalibrationDeps): void {
|
export function mountCalibration(deps: CalibrationDeps): void {
|
||||||
const { panel, overlay } = deps;
|
const { panel, overlay } = deps;
|
||||||
|
|
@ -53,7 +58,7 @@ export function mountCalibration(deps: CalibrationDeps): void {
|
||||||
|
|
||||||
const renderList = (errs?: number[]) => {
|
const renderList = (errs?: number[]) => {
|
||||||
list.innerHTML = points
|
list.innerHTML = points
|
||||||
.map((p, i) => `<li>(${p.machine[0]}, ${p.machine[1]}) → px(${p.image[0].toFixed(0)}, ${p.image[1].toFixed(0)})${errs ? ` <small>err ${errs[i]!.toFixed(1)}px</small>` : ''}</li>`)
|
.map((p, i) => `<li>(${p.machine[0]}, ${p.machine[1]}) → img(${p.image[0].toFixed(3)}, ${p.image[1].toFixed(3)})${errs ? ` <small>err ${(errs[i]! * overlay.width).toFixed(1)}px</small>` : ''}</li>`)
|
||||||
.join('');
|
.join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -69,10 +74,11 @@ export function mountCalibration(deps: CalibrationDeps): void {
|
||||||
overlay.addEventListener('click', (ev) => {
|
overlay.addEventListener('click', (ev) => {
|
||||||
if (!pendingMachine || !overlay.classList.contains('interactive')) return;
|
if (!pendingMachine || !overlay.classList.contains('interactive')) return;
|
||||||
const rect = overlay.getBoundingClientRect();
|
const rect = overlay.getBoundingClientRect();
|
||||||
// Map click to native image pixels: canvas backing store == displayed media box.
|
// Store normalized [0,1] image coords (fraction of the displayed camera frame).
|
||||||
|
// This makes the calibration independent of display/canvas size.
|
||||||
const px: Vec2 = [
|
const px: Vec2 = [
|
||||||
((ev.clientX - rect.left) / rect.width) * overlay.width,
|
(ev.clientX - rect.left) / rect.width,
|
||||||
((ev.clientY - rect.top) / rect.height) * overlay.height,
|
(ev.clientY - rect.top) / rect.height,
|
||||||
];
|
];
|
||||||
points.push({ machine: pendingMachine, image: px });
|
points.push({ machine: pendingMachine, image: px });
|
||||||
pendingMachine = null;
|
pendingMachine = null;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,13 @@ let style: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth
|
||||||
|
|
||||||
function render(): void {
|
function render(): void {
|
||||||
clearCanvas(ctx, overlay.width, overlay.height);
|
clearCanvas(ctx, overlay.width, overlay.height);
|
||||||
const proj = state.projected();
|
const w = overlay.width;
|
||||||
|
const h = overlay.height;
|
||||||
|
// projected() yields normalized [0,1] image coords; scale to canvas pixels.
|
||||||
|
const proj = state.projected().map((seg) => ({
|
||||||
|
kind: seg.kind,
|
||||||
|
points: seg.points.map((p): [number, number] => [p[0] * w, p[1] * h]),
|
||||||
|
}));
|
||||||
drawOverlay(ctx, proj, style);
|
drawOverlay(ctx, proj, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ export interface Alignment {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Calibration {
|
export interface Calibration {
|
||||||
/** Clicked points in the camera image, pixels. */
|
/** Calibration points in normalized [0,1] camera-frame coordinates. */
|
||||||
imagePoints: Vec2[];
|
imagePoints: Vec2[];
|
||||||
/** Corresponding machine coordinates, mm. */
|
/** Corresponding machine coordinates, mm. */
|
||||||
machinePoints: Vec2[];
|
machinePoints: Vec2[];
|
||||||
/** machine-mm → image-px. */
|
/** machine-mm → normalized [0,1] image coords. */
|
||||||
homography: Mat3;
|
homography: Mat3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue