diff --git a/src/app/calibration-ui.ts b/src/app/calibration-ui.ts index b78cf07..12b99af 100644 --- a/src/app/calibration-ui.ts +++ b/src/app/calibration-ui.ts @@ -3,6 +3,7 @@ import { estimateHomography, applyHomography } from '../geometry/homography'; /** Per-point reprojection error in pixels: |H·machine − image|. */ 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) => { const p = applyHomography(H, m); 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" → * 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 { const { panel, overlay } = deps; @@ -53,7 +58,7 @@ export function mountCalibration(deps: CalibrationDeps): void { const renderList = (errs?: number[]) => { list.innerHTML = points - .map((p, i) => `
  • (${p.machine[0]}, ${p.machine[1]}) → px(${p.image[0].toFixed(0)}, ${p.image[1].toFixed(0)})${errs ? ` err ${errs[i]!.toFixed(1)}px` : ''}
  • `) + .map((p, i) => `
  • (${p.machine[0]}, ${p.machine[1]}) → img(${p.image[0].toFixed(3)}, ${p.image[1].toFixed(3)})${errs ? ` err ${(errs[i]! * overlay.width).toFixed(1)}px` : ''}
  • `) .join(''); }; @@ -69,10 +74,11 @@ export function mountCalibration(deps: CalibrationDeps): void { overlay.addEventListener('click', (ev) => { if (!pendingMachine || !overlay.classList.contains('interactive')) return; 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 = [ - ((ev.clientX - rect.left) / rect.width) * overlay.width, - ((ev.clientY - rect.top) / rect.height) * overlay.height, + (ev.clientX - rect.left) / rect.width, + (ev.clientY - rect.top) / rect.height, ]; points.push({ machine: pendingMachine, image: px }); pendingMachine = null; diff --git a/src/main.ts b/src/main.ts index f3fadbb..1fd429e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,13 @@ let style: RenderStyle = { cutColor: '#00e5ff', rapidColor: '#ff9800', lineWidth function render(): void { 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); } diff --git a/src/types.ts b/src/types.ts index 5224bca..666b06a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,11 +19,11 @@ export interface Alignment { } export interface Calibration { - /** Clicked points in the camera image, pixels. */ + /** Calibration points in normalized [0,1] camera-frame coordinates. */ imagePoints: Vec2[]; /** Corresponding machine coordinates, mm. */ machinePoints: Vec2[]; - /** machine-mm → image-px. */ + /** machine-mm → normalized [0,1] image coords. */ homography: Mat3; }