Skip to main content

cumulus3d_core/
camera.rs

1/*
2 * camera.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * Third-party notices: parts of the algorithms, default parameters and data
26 * formats in this file follow other open-source projects. Their copyright
27 * notices and licenses are reproduced in THIRD_PARTY_NOTICES.md.
28 *
29 * SPDX-License-Identifier: Apache-2.0
30 */
31
32//! Camera models.
33//!
34//! Coordinate convention: camera coordinates (X, Y, Z) -> normalized plane (u, v) = (X/Z, Y/Z) -> distortion -> pixel.
35//! The pixel origin is the top-left corner of the image; the top-left pixel center = (0.5, 0.5).
36
37use crate::error::{Error, Result};
38use crate::geometry::{Mat3, Vec2, Vec3};
39use crate::ids::{CameraId, INVALID_CAMERA_ID};
40use nalgebra::{Matrix2, Matrix2x3};
41
42/// Supported camera models. The numeric id is the model number in the model file format.
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub enum CameraModelKind {
45    /// Single-focal pinhole (f, cx, cy).
46    SingleFocalPinhole = 0,
47    /// Pinhole (fx, fy, cx, cy).
48    Pinhole = 1,
49    /// Single focal + 1 radial distortion term (f, cx, cy, k).
50    SingleFocalRadial = 2,
51    /// Single focal + 2 radial distortion terms (f, cx, cy, k1, k2).
52    Radial = 3,
53    /// 2 radial terms + 2 tangential terms (fx, fy, cx, cy, k1, k2, p1, p2).
54    OpenCv = 4,
55}
56
57/// Maximum number of Newton iterations for undistortion.
58pub const UNDISTORT_MAX_ITERS: usize = 100;
59/// Convergence test: ‖step‖² < 1e−10.
60pub const UNDISTORT_STEP_SQ_TOL: f64 = 1e-10;
61
62/// File-format names of the supported models (in id order).
63const MODEL_NAMES: [&str; 5] = ["SIMPLE_PINHOLE", "PINHOLE", "SIMPLE_RADIAL", "RADIAL", "OPENCV"];
64
65impl CameraModelKind {
66    /// Model number -> kind.
67    pub fn from_id(id: i32) -> Result<Self> {
68        match id {
69            0 => Ok(Self::SingleFocalPinhole),
70            1 => Ok(Self::Pinhole),
71            2 => Ok(Self::SingleFocalRadial),
72            3 => Ok(Self::Radial),
73            4 => Ok(Self::OpenCv),
74            _ => Err(Error::Unsupported(format!("카메라 모델 id {id}"))),
75        }
76    }
77    /// Model number.
78    pub fn id(self) -> i32 {
79        self as i32
80    }
81    /// File-format model name (e.g. `PINHOLE`).
82    pub fn name(self) -> &'static str {
83        MODEL_NAMES[self as usize]
84    }
85    /// File-format model name -> kind.
86    pub fn from_name(name: &str) -> Result<Self> {
87        match MODEL_NAMES.iter().position(|n| *n == name) {
88            Some(i) => Self::from_id(i as i32),
89            None => Err(Error::Format(format!("알 수 없는 카메라 모델 이름 {name}"))),
90        }
91    }
92    /// Number of parameters.
93    pub fn num_params(self) -> usize {
94        match self {
95            Self::SingleFocalPinhole => 3,
96            Self::Pinhole => 4,
97            Self::SingleFocalRadial => 4,
98            Self::Radial => 5,
99            Self::OpenCv => 8,
100        }
101    }
102    /// Parameter names (comma-separated).
103    pub fn params_info(self) -> &'static str {
104        match self {
105            Self::SingleFocalPinhole => "f, cx, cy",
106            Self::Pinhole => "fx, fy, cx, cy",
107            Self::SingleFocalRadial => "f, cx, cy, k",
108            Self::Radial => "f, cx, cy, k1, k2",
109            Self::OpenCv => "fx, fy, cx, cy, k1, k2, p1, p2",
110        }
111    }
112    /// Position of the focal parameters within `params`.
113    pub fn focal_slots(self) -> &'static [usize] {
114        match self {
115            Self::SingleFocalPinhole | Self::SingleFocalRadial | Self::Radial => &[0],
116            Self::Pinhole | Self::OpenCv => &[0, 1],
117        }
118    }
119    /// Position of the principal point parameters within `params`.
120    pub fn pp_slots(self) -> &'static [usize] {
121        match self {
122            Self::SingleFocalPinhole | Self::SingleFocalRadial | Self::Radial => &[1, 2],
123            Self::Pinhole | Self::OpenCv => &[2, 3],
124        }
125    }
126    /// Position of the distortion (extra) parameters within `params`.
127    pub fn extra_param_slots(self) -> &'static [usize] {
128        match self {
129            Self::SingleFocalPinhole | Self::Pinhole => &[],
130            Self::SingleFocalRadial => &[3],
131            Self::Radial => &[3, 4],
132            Self::OpenCv => &[4, 5, 6, 7],
133        }
134    }
135}
136
137/// Camera (intrinsic parameters). `focal_from_prior` is memory-only (not stored in the model file).
138#[derive(Clone, Debug, PartialEq)]
139pub struct Camera {
140    /// Camera id.
141    pub camera_id: CameraId,
142    /// Camera model.
143    pub model: CameraModelKind,
144    /// Image width (pixels).
145    pub width: u64,
146    /// Image height (pixels).
147    pub height: u64,
148    /// Model parameters (order per `CameraModelKind::params_info`).
149    pub params: Vec<f64>,
150    /// Whether the focal length came from prior information such as EXIF (for choosing the calibration path).
151    pub focal_from_prior: bool,
152}
153
154impl Camera {
155    /// Creates after validating the parameter count.
156    pub fn new(camera_id: CameraId, model: CameraModelKind, width: u64, height: u64, params: Vec<f64>) -> Result<Self> {
157        if params.len() != model.num_params() {
158            return Err(Error::Format(format!(
159                "카메라 {camera_id}: 모델 {} 의 파라미터 개수 {} != {}",
160                model.name(),
161                params.len(),
162                model.num_params()
163            )));
164        }
165        Ok(Self { camera_id, model, width, height, params, focal_from_prior: false })
166    }
167
168    /// Initializes with focal length f: focal = f, principal point = (W/2, H/2), distortion 0.
169    pub fn from_focal(model: CameraModelKind, focal: f64, width: u64, height: u64) -> Self {
170        let mut params = vec![0.0; model.num_params()];
171        for &i in model.focal_slots() {
172            params[i] = focal;
173        }
174        let pp = model.pp_slots();
175        params[pp[0]] = width as f64 / 2.0;
176        params[pp[1]] = height as f64 / 2.0;
177        Self { camera_id: INVALID_CAMERA_ID, model, width, height, params, focal_from_prior: false }
178    }
179
180    /// Whether the parameter count matches the model.
181    pub fn verify_params(&self) -> bool {
182        self.params.len() == self.model.num_params()
183    }
184
185    /// Mean focal length.
186    pub fn mean_focal(&self) -> f64 {
187        let idx = self.model.focal_slots();
188        idx.iter().map(|&i| self.params[i]).sum::<f64>() / idx.len() as f64
189    }
190    /// x focal length.
191    pub fn focal_length_x(&self) -> f64 {
192        self.params[self.model.focal_slots()[0]]
193    }
194    /// y focal length (same as x for single-focal models).
195    pub fn focal_length_y(&self) -> f64 {
196        let idx = self.model.focal_slots();
197        self.params[idx[idx.len() - 1]]
198    }
199    /// Sets all focal parameters to f.
200    pub fn set_focal_length(&mut self, f: f64) {
201        for &i in self.model.focal_slots() {
202            self.params[i] = f;
203        }
204    }
205    /// Principal point x.
206    pub fn principal_point_x(&self) -> f64 {
207        self.params[self.model.pp_slots()[0]]
208    }
209    /// Principal point y.
210    pub fn principal_point_y(&self) -> f64 {
211        self.params[self.model.pp_slots()[1]]
212    }
213    /// Sets the principal point.
214    pub fn set_principal_point(&mut self, cx: f64, cy: f64) {
215        let pp = self.model.pp_slots();
216        self.params[pp[0]] = cx;
217        self.params[pp[1]] = cy;
218    }
219    /// Copy of the distortion (extra) parameters.
220    pub fn extra_params(&self) -> Vec<f64> {
221        self.model.extra_param_slots().iter().map(|&i| self.params[i]).collect()
222    }
223    /// No-distortion test: all extra parameters are 0.
224    pub fn is_undistorted(&self) -> bool {
225        self.model.extra_param_slots().iter().all(|&i| self.params[i] == 0.0)
226    }
227    /// Calibration matrix K (distortion ignored).
228    pub fn calibration_matrix(&self) -> Mat3 {
229        Mat3::new(self.focal_length_x(), 0.0, self.principal_point_x(), 0.0, self.focal_length_y(), self.principal_point_y(), 0.0, 0.0, 1.0)
230    }
231    /// Abnormal-parameter test.
232    pub fn has_implausible_params(&self, min_focal_ratio: f64, max_focal_ratio: f64, max_extra_param: f64) -> bool {
233        let (w, h) = (self.width as f64, self.height as f64);
234        let (cx, cy) = (self.principal_point_x(), self.principal_point_y());
235        if cx < 0.0 || cx > w || cy < 0.0 || cy > h {
236            return true;
237        }
238        let m = w.max(h);
239        for &i in self.model.focal_slots() {
240            let r = self.params[i] / m;
241            if r < min_focal_ratio || r > max_focal_ratio {
242                return true;
243            }
244        }
245        self.model.extra_param_slots().iter().any(|&i| self.params[i].abs() > max_extra_param)
246    }
247
248    /// Uniform resize: multiplies focal and principal point by s, size is rounded. Distortion coefficients unchanged.
249    pub fn rescale(&mut self, scale: f64) {
250        let nw = (self.width as f64 * scale).round().max(1.0) as u64;
251        let nh = (self.height as f64 * scale).round().max(1.0) as u64;
252        self.scale_params(scale, scale);
253        self.width = nw;
254        self.height = nh;
255    }
256
257    /// Resize to a new size: multiplies focal and principal point by per-axis sx = W'/W, sy = H'/H.
258    pub fn rescale_to(&mut self, new_width: u64, new_height: u64) {
259        let sx = new_width as f64 / self.width as f64;
260        let sy = new_height as f64 / self.height as f64;
261        self.scale_params(sx, sy);
262        self.width = new_width;
263        self.height = new_height;
264    }
265
266    fn scale_params(&mut self, sx: f64, sy: f64) {
267        let f = self.model.focal_slots();
268        if f.len() == 1 {
269            // Design decision: for single-focal models, the anisotropic scale uses the mean of the two axes.
270            self.params[f[0]] *= 0.5 * (sx + sy);
271        } else {
272            self.params[f[0]] *= sx;
273            self.params[f[1]] *= sy;
274        }
275        let pp = self.model.pp_slots();
276        self.params[pp[0]] *= sx;
277        self.params[pp[1]] *= sy;
278    }
279
280    /// Converts a pixel error threshold to a normalized-plane threshold: τ / mean focal length.
281    pub fn img_to_cam_threshold(&self, threshold_px: f64) -> f64 {
282        threshold_px / self.mean_focal()
283    }
284
285    /// Normalized-plane distortion Δ(u,v) and ∂Δ/∂(u,v).
286    fn distortion(&self, u: f64, v: f64) -> (f64, f64, Matrix2<f64>) {
287        let p = &self.params;
288        match self.model {
289            CameraModelKind::SingleFocalPinhole | CameraModelKind::Pinhole => (0.0, 0.0, Matrix2::zeros()),
290            CameraModelKind::SingleFocalRadial | CameraModelKind::Radial => {
291                let (k1, k2) = if self.model == CameraModelKind::SingleFocalRadial { (p[3], 0.0) } else { (p[3], p[4]) };
292                let r2 = u * u + v * v;
293                let radial = k1 * r2 + k2 * r2 * r2;
294                let g = 2.0 * k1 + 4.0 * k2 * r2; // ∂radial/∂u = g·u
295                let du = u * radial;
296                let dv = v * radial;
297                let j = Matrix2::new(radial + u * g * u, u * g * v, v * g * u, radial + v * g * v);
298                (du, dv, j)
299            }
300            CameraModelKind::OpenCv => {
301                let (k1, k2, p1, p2) = (p[4], p[5], p[6], p[7]);
302                let r2 = u * u + v * v;
303                let radial = k1 * r2 + k2 * r2 * r2;
304                let du = u * radial + 2.0 * p1 * u * v + p2 * (r2 + 2.0 * u * u);
305                let dv = v * radial + 2.0 * p2 * u * v + p1 * (r2 + 2.0 * v * v);
306                let du_du = radial + u * (2.0 * k1 * u + 4.0 * k2 * r2 * u) + 2.0 * p1 * v + 6.0 * p2 * u;
307                let du_dv = u * (2.0 * k1 * v + 4.0 * k2 * r2 * v) + 2.0 * p1 * u + 2.0 * p2 * v;
308                let dv_du = v * (2.0 * k1 * u + 4.0 * k2 * r2 * u) + 2.0 * p2 * v + 2.0 * p1 * u;
309                let dv_dv = radial + v * (2.0 * k1 * v + 4.0 * k2 * r2 * v) + 2.0 * p2 * u + 6.0 * p1 * v;
310                (du, dv, Matrix2::new(du_du, du_dv, dv_du, dv_dv))
311            }
312        }
313    }
314
315    fn fxfycxcy(&self) -> (f64, f64, f64, f64) {
316        (self.focal_length_x(), self.focal_length_y(), self.principal_point_x(), self.principal_point_y())
317    }
318
319    /// Normalized plane (u, v) -> pixel (with distortion).
320    pub fn normalized_to_img(&self, uv: &Vec2) -> Vec2 {
321        let (fx, fy, cx, cy) = self.fxfycxcy();
322        let (du, dv, _) = self.distortion(uv.x, uv.y);
323        Vec2::new(fx * (uv.x + du) + cx, fy * (uv.y + dv) + cy)
324    }
325
326    /// 3D point in camera coordinates -> pixel. None if Z < f64 ε.
327    pub fn cam_to_img(&self, xc: &Vec3) -> Option<Vec2> {
328        if xc.z < f64::EPSILON {
329            return None;
330        }
331        Some(self.normalized_to_img(&Vec2::new(xc.x / xc.z, xc.y / xc.z)))
332    }
333
334    /// Pixel -> normalized plane (undistortion). None if Newton does not converge within 100 iterations.
335    pub fn img_to_normalized(&self, xy: &Vec2) -> Option<Vec2> {
336        let (fx, fy, cx, cy) = self.fxfycxcy();
337        let p0 = Vec2::new((xy.x - cx) / fx, (xy.y - cy) / fy);
338        if self.model.extra_param_slots().is_empty() || self.is_undistorted() {
339            return if p0.x.is_finite() && p0.y.is_finite() { Some(p0) } else { None };
340        }
341        let mut p = p0;
342        for _ in 0..UNDISTORT_MAX_ITERS {
343            let (du, dv, jd) = self.distortion(p.x, p.y);
344            let j = Matrix2::identity() + jd;
345            let res = Vec2::new(p.x + du - p0.x, p.y + dv - p0.y);
346            let mut step = j.lu().solve(&res)?;
347            if !step.x.is_finite() || !step.y.is_finite() {
348                return None;
349            }
350            let rho2 = (p.norm_squared() * 0.01).max(0.01);
351            let s2 = step.norm_squared();
352            if s2 > rho2 {
353                step *= (rho2 / s2).sqrt();
354            }
355            p -= step;
356            if step.norm_squared() < UNDISTORT_STEP_SQ_TOL {
357                return Some(p);
358            }
359        }
360        None
361    }
362
363    /// Pixel -> unit ray (u, v, 1)/‖·‖.
364    pub fn img_to_ray(&self, xy: &Vec2) -> Option<Vec3> {
365        self.img_to_normalized(xy).map(|uv| Vec3::new(uv.x, uv.y, 1.0).normalize())
366    }
367
368    /// Projection and analytic Jacobian.
369    ///
370    /// Returns: (pixel, ∂pixel/∂camera-coordinates 2×3). If `d_params` is given, fills ∂pixel/∂parameters as row-major 2×num_params
371    /// (first row = x, second row = y, columns = parameter order).
372    /// None if Z < ε (BA then sets the residual and Jacobian to 0).
373    pub fn cam_to_img_with_jacobian(&self, xc: &Vec3, d_params: Option<&mut [f64]>) -> Option<(Vec2, Matrix2x3<f64>)> {
374        if xc.z < f64::EPSILON {
375            return None;
376        }
377        let iz = 1.0 / xc.z;
378        let u = xc.x * iz;
379        let v = xc.y * iz;
380        let (fx, fy, cx, cy) = self.fxfycxcy();
381        let (du, dv, jd) = self.distortion(u, v);
382        let xd = u + du;
383        let yd = v + dv;
384        let pix = Vec2::new(fx * xd + cx, fy * yd + cy);
385        let d = Matrix2::identity() + jd;
386        let nm = Matrix2x3::new(iz, 0.0, -xc.x * iz * iz, 0.0, iz, -xc.y * iz * iz);
387        let fd = Matrix2::new(fx, 0.0, 0.0, fy);
388        let j_point = fd * d * nm;
389        if let Some(out) = d_params {
390            let n = self.model.num_params();
391            assert!(out.len() >= 2 * n, "d_params 길이 부족");
392            out[..2 * n].iter_mut().for_each(|x| *x = 0.0);
393            let r2 = u * u + v * v;
394            let (rx, ry) = out.split_at_mut(n);
395            match self.model {
396                CameraModelKind::SingleFocalPinhole => {
397                    rx[0] = u;
398                    rx[1] = 1.0;
399                    ry[0] = v;
400                    ry[2] = 1.0;
401                }
402                CameraModelKind::Pinhole => {
403                    rx[0] = u;
404                    rx[2] = 1.0;
405                    ry[1] = v;
406                    ry[3] = 1.0;
407                }
408                CameraModelKind::SingleFocalRadial | CameraModelKind::Radial => {
409                    let f = fx;
410                    rx[0] = xd;
411                    rx[1] = 1.0;
412                    rx[3] = f * u * r2;
413                    ry[0] = yd;
414                    ry[2] = 1.0;
415                    ry[3] = f * v * r2;
416                    if self.model == CameraModelKind::Radial {
417                        rx[4] = f * u * r2 * r2;
418                        ry[4] = f * v * r2 * r2;
419                    }
420                }
421                CameraModelKind::OpenCv => {
422                    rx[0] = xd;
423                    rx[2] = 1.0;
424                    rx[4] = fx * u * r2;
425                    rx[5] = fx * u * r2 * r2;
426                    rx[6] = fx * 2.0 * u * v;
427                    rx[7] = fx * (r2 + 2.0 * u * u);
428                    ry[1] = yd;
429                    ry[3] = 1.0;
430                    ry[4] = fy * v * r2;
431                    ry[5] = fy * v * r2 * r2;
432                    ry[6] = fy * (r2 + 2.0 * v * v);
433                    ry[7] = fy * 2.0 * u * v;
434                }
435            }
436        }
437        Some((pix, j_point))
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use rand::{RngExt, SeedableRng};
445    use rand_pcg::Pcg64;
446
447    fn opencv(params: [f64; 8]) -> Camera {
448        Camera::new(1, CameraModelKind::OpenCv, 1000, 800, params.to_vec()).unwrap()
449    }
450
451    #[test]
452    fn opencv_hand_computed() {
453        let cam = opencv([1000.0, 1000.0, 500.0, 400.0, 0.1, 0.01, 0.001, -0.002]);
454        let p = cam.cam_to_img(&Vec3::new(0.2, -0.1, 1.0)).unwrap();
455        assert!((p.x - 700.705).abs() < 1e-9, "{}", p.x);
456        assert!((p.y - 299.6475).abs() < 1e-9, "{}", p.y);
457        assert!(cam.cam_to_img(&Vec3::new(0.0, 0.0, 0.0)).is_none());
458        assert!(cam.cam_to_img(&Vec3::new(0.0, 0.0, -1.0)).is_none());
459    }
460
461    #[test]
462    fn roundtrip_all_models() {
463        let mut rng = Pcg64::seed_from_u64(42);
464        for model in [
465            CameraModelKind::SingleFocalPinhole,
466            CameraModelKind::Pinhole,
467            CameraModelKind::SingleFocalRadial,
468            CameraModelKind::Radial,
469            CameraModelKind::OpenCv,
470        ] {
471            for _ in 0..300 {
472                let mut cam = Camera::from_focal(model, 900.0 + rng.random_range(0.0..200.0), 1000, 800);
473                let extra = model.extra_param_slots();
474                let ranges = [0.3, 0.1, 0.01, 0.01];
475                for (k, &i) in extra.iter().enumerate() {
476                    cam.params[i] = rng.random_range(-ranges[k]..ranges[k]);
477                }
478                if model == CameraModelKind::OpenCv || model == CameraModelKind::Pinhole {
479                    cam.params[1] = cam.params[0] * 1.01;
480                }
481                let uv = Vec2::new(rng.random_range(-0.8..0.8), rng.random_range(-0.8..0.8));
482                // Samples where the distortion map is not locally invertible (Jacobian determinant ≤ 0.2) are excluded
483                // since the solution is not unique.
484                if (Matrix2::identity() + cam.distortion(uv.x, uv.y).2).determinant() <= 0.2 {
485                    continue;
486                }
487                let px = cam.normalized_to_img(&uv);
488                let uv2 = cam.img_to_normalized(&px).expect("수렴해야 함");
489                assert!((uv2 - uv).norm() <= 1e-8, "{model:?} {uv:?} {uv2:?}");
490                let px2 = cam.normalized_to_img(&uv2);
491                assert!((px2 - px).norm() <= 1e-6);
492            }
493        }
494    }
495
496    #[test]
497    fn undistort_failure_no_nan() {
498        let cam = opencv([1000.0, 1000.0, 500.0, 400.0, -5.0, 0.0, 0.0, 0.0]);
499        // Large radius: with k1 = −5, a p0 beyond the maximum of F(p) (~0.17 at r≈0.26) has no solution.
500        let r = cam.img_to_normalized(&Vec2::new(500.0 + 900.0, 400.0 + 900.0));
501        assert!(r.is_none());
502    }
503
504    #[test]
505    fn distortion_jacobian_numeric() {
506        let mut rng = Pcg64::seed_from_u64(7);
507        for _ in 0..100 {
508            let cam = opencv([
509                1000.0,
510                1000.0,
511                500.0,
512                400.0,
513                rng.random_range(-0.3..0.3),
514                rng.random_range(-0.1..0.1),
515                rng.random_range(-0.01..0.01),
516                rng.random_range(-0.01..0.01),
517            ]);
518            let (u, v) = (rng.random_range(-0.8..0.8), rng.random_range(-0.8..0.8));
519            let (_, _, j) = cam.distortion(u, v);
520            let h = 1e-7;
521            let (a1, b1, _) = cam.distortion(u + h, v);
522            let (a0, b0, _) = cam.distortion(u - h, v);
523            let (c1, d1, _) = cam.distortion(u, v + h);
524            let (c0, d0, _) = cam.distortion(u, v - h);
525            let num = Matrix2::new((a1 - a0) / (2.0 * h), (c1 - c0) / (2.0 * h), (b1 - b0) / (2.0 * h), (d1 - d0) / (2.0 * h));
526            assert!((num - j).abs().max() <= 1e-6);
527        }
528    }
529
530    #[test]
531    fn projection_jacobian_numeric() {
532        let mut rng = Pcg64::seed_from_u64(9);
533        for model in [
534            CameraModelKind::SingleFocalPinhole,
535            CameraModelKind::Pinhole,
536            CameraModelKind::SingleFocalRadial,
537            CameraModelKind::Radial,
538            CameraModelKind::OpenCv,
539        ] {
540            for _ in 0..50 {
541                let mut cam = Camera::from_focal(model, 1200.0, 2000, 1500);
542                for &i in model.extra_param_slots() {
543                    cam.params[i] = rng.random_range(-0.2..0.2);
544                }
545                if model == CameraModelKind::OpenCv {
546                    cam.params[6] = rng.random_range(-0.01..0.01);
547                    cam.params[7] = rng.random_range(-0.01..0.01);
548                }
549                let xc = Vec3::new(rng.random_range(-1.0..1.0), rng.random_range(-1.0..1.0), rng.random_range(2.0..5.0));
550                let n = model.num_params();
551                let mut dp = vec![0.0; 2 * n];
552                let (pix, jp) = cam.cam_to_img_with_jacobian(&xc, Some(&mut dp)).unwrap();
553                assert!((pix - cam.cam_to_img(&xc).unwrap()).norm() < 1e-12);
554                // point
555                for k in 0..3 {
556                    let h = 1e-6 * xc[k].abs().max(1.0);
557                    let mut a = xc;
558                    a[k] += h;
559                    let mut b = xc;
560                    b[k] -= h;
561                    let num = (cam.cam_to_img(&a).unwrap() - cam.cam_to_img(&b).unwrap()) / (2.0 * h);
562                    for r in 0..2 {
563                        let an = jp[(r, k)];
564                        assert!((num[r] - an).abs() <= 1e-6 * an.abs().max(1.0), "{model:?} pt {r},{k}: {} vs {}", num[r], an);
565                    }
566                }
567                // params
568                for k in 0..n {
569                    let h = 1e-6 * cam.params[k].abs().max(1.0);
570                    let mut a = cam.clone();
571                    a.params[k] += h;
572                    let mut b = cam.clone();
573                    b.params[k] -= h;
574                    let num = (a.cam_to_img(&xc).unwrap() - b.cam_to_img(&xc).unwrap()) / (2.0 * h);
575                    for r in 0..2 {
576                        let an = dp[r * n + k];
577                        assert!((num[r] - an).abs() <= 1e-6 * an.abs().max(1.0), "{model:?} param {r},{k}: {} vs {}", num[r], an);
578                    }
579                }
580            }
581        }
582        let cam = Camera::from_focal(CameraModelKind::OpenCv, 1000.0, 100, 100);
583        assert!(cam.cam_to_img_with_jacobian(&Vec3::new(0.0, 0.0, -1.0), None).is_none());
584    }
585
586    #[test]
587    fn model_tables_and_rescale() {
588        let cam = Camera::from_focal(CameraModelKind::OpenCv, 4800.0, 4000, 3000);
589        assert_eq!(cam.params, vec![4800.0, 4800.0, 2000.0, 1500.0, 0.0, 0.0, 0.0, 0.0]);
590        assert_eq!(CameraModelKind::from_name("OPENCV").unwrap(), CameraModelKind::OpenCv);
591        assert!(CameraModelKind::from_id(6).is_err());
592        assert!(Camera::new(1, CameraModelKind::OpenCv, 1, 1, vec![0.0; 7]).is_err());
593        let mut c = cam.clone();
594        c.rescale_to(960, 720);
595        assert!((c.params[0] - 4800.0 * 0.24).abs() < 1e-9);
596        assert!((c.params[3] - 1500.0 * 0.24).abs() < 1e-9);
597        assert!((cam.img_to_cam_threshold(4.0) - 4.0 / 4800.0).abs() < 1e-15);
598        assert!(!cam.has_implausible_params(0.1, 10.0, 1.0));
599        let mut b = cam.clone();
600        b.params[4] = 1.5;
601        assert!(b.has_implausible_params(0.1, 10.0, 1.0));
602    }
603}