(p: Vec2, i: CameraIntrinsics)
| 92 | }, |
| 93 | |
| 94 | inverse(p: Vec2, i: CameraIntrinsics): UndistortResult { |
| 95 | let x = p.x; |
| 96 | let y = p.y; |
| 97 | for (let iter = 0; iter < 20; iter++) { |
| 98 | const r2 = x * x + y * y; |
| 99 | const R = _prFactor(r2, i); |
| 100 | const Rp = _prDeriv(r2, i); |
| 101 | |
| 102 | const dx = x * R + 2 * i.p1 * x * y + i.p2 * (r2 + 2 * x * x); |
| 103 | const dy = y * R + i.p1 * (r2 + 2 * y * y) + 2 * i.p2 * x * y; |
| 104 | |
| 105 | // Jacobian of (u + delta(u)) w.r.t. u. |
| 106 | // Tangential Jacobian terms vanish exactly when p1=p2=0. |
| 107 | const j11 = 1 + R + 2 * x * x * Rp + 2 * i.p1 * y + 6 * i.p2 * x; |
| 108 | const j12 = 2 * x * y * Rp + 2 * i.p1 * x + 2 * i.p2 * y; |
| 109 | const j21 = 2 * x * y * Rp + 2 * i.p1 * x + 2 * i.p2 * y; |
| 110 | const j22 = 1 + R + 2 * y * y * Rp + 6 * i.p1 * y + 2 * i.p2 * x; |
| 111 | |
| 112 | const gx = x + dx - p.x; |
| 113 | const gy = y + dy - p.y; |
| 114 | const det = j11 * j22 - j12 * j21; |
| 115 | if (Math.abs(det) < 1e-15) break; |
| 116 | const stepX = (j22 * gx - j12 * gy) / det; |
| 117 | const stepY = (j11 * gy - j21 * gx) / det; |
| 118 | x -= stepX; |
| 119 | y -= stepY; |
| 120 | if (stepX * stepX + stepY * stepY < 1e-20) break; |
| 121 | } |
| 122 | // Reject non-physical roots: if re-distorting the Newton result doesn't land |
| 123 | // back near the input, the ray has no valid pre-image (folded barrel map). |
| 124 | const check = perspectiveRadialStrategy.forward({ x, y }, i); |
| 125 | const valid = Math.hypot(check.x - p.x, check.y - p.y) < PERSPECTIVE_INVERSE_RESIDUAL_TOL; |
| 126 | return { x, y, valid }; |
| 127 | }, |
| 128 | }; |
| 129 | |
| 130 | // ── 'fov' strategy ──────────────────────────────────────────────────────────── |
nothing calls this directly
no test coverage detected