Skip to main content

camcal/
backend.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Backend facade for chessboard detection and camera calibration.
5//!
6//! These route to the pure-Rust [`checkerboard_calibrate`] implementation.
7
8/// A point with a known world (3D) and image (2D) location.
9#[derive(Debug, Clone, Copy)]
10pub struct CorrespondingPoint {
11    pub object_point: (f64, f64, f64),
12    pub image_point: (f64, f64),
13}
14
15/// Result of [`calibrate_camera`].
16#[derive(Debug, Clone)]
17pub struct CalibrationResult {
18    /// Overall reprojection error in pixels (RMS).
19    pub mean_reprojection_distance_pixels: f64,
20    /// Camera matrix `[fx 0 cx; 0 fy cy; 0 0 1]`, row-major.
21    pub camera_matrix: [f64; 9],
22    /// Distortion coefficients `(k1, k2, p1, p2, k3)`.
23    pub distortion_coeffs: [f64; 5],
24    pub image_width: u32,
25    pub image_height: u32,
26}
27
28/// Error from a calibration backend.
29#[derive(Debug)]
30pub enum Error {
31    Calibration(String),
32    Detection(String),
33}
34
35impl std::fmt::Display for Error {
36    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
37        match self {
38            Error::Calibration(s) => write!(f, "calibration error: {s}"),
39            Error::Detection(s) => write!(f, "chessboard detection error: {s}"),
40        }
41    }
42}
43
44impl std::error::Error for Error {}
45
46/// Convert an interleaved RGB buffer to grayscale. The buffer is interpreted as
47/// BGR and reduced as OpenCV's `COLOR_BGR2GRAY` would, so that color inputs are
48/// handled identically to historical behavior. For grayscale inputs (R == G ==
49/// B) this is the identity.
50fn rgb_to_gray(rgb: &[u8], width: u32, height: u32) -> Vec<u8> {
51    let n = (width as usize) * (height as usize);
52    let mut gray = vec![0u8; n];
53    for i in 0..n {
54        let r = rgb[3 * i] as i32;
55        let g = rgb[3 * i + 1] as i32;
56        let b = rgb[3 * i + 2] as i32;
57        // OpenCV BGR2GRAY fixed point (yuv_shift = 14): Y = R*4899 + G*9617 +
58        // B*1868, with the buffer read as B,G,R — i.e. our byte0 is OpenCV's B
59        // and our byte2 is OpenCV's R.
60        gray[i] = ((b * 4899 + g * 9617 + r * 1868 + 8192) >> 14) as u8;
61    }
62    gray
63}
64
65pub fn find_chessboard_corners(
66    rgb: &[u8],
67    width: u32,
68    height: u32,
69    pattern_width: usize,
70    pattern_height: usize,
71) -> Result<Option<Vec<(f32, f32)>>, Error> {
72    use checkerboard_calibrate::{CornerSubPixParams, GrayImageRef, corner_subpix};
73
74    let gray = rgb_to_gray(rgb, width, height);
75    let (w, h) = (width as usize, height as usize);
76    let corners = checkerboard_calibrate::chessboard::find_chessboard_corners(
77        &gray,
78        w,
79        h,
80        pattern_width,
81        pattern_height,
82    );
83    // Sub-pixel refine before returning.
84    Ok(corners.map(|raw| {
85        corner_subpix(
86            GrayImageRef::new(&gray, w, h),
87            &raw,
88            &CornerSubPixParams::default(),
89        )
90    }))
91}
92
93pub fn calibrate_camera(
94    all_pts: &[Vec<CorrespondingPoint>],
95    width: i32,
96    height: i32,
97) -> Result<CalibrationResult, Error> {
98    let views: Vec<Vec<checkerboard_calibrate::calibrate::CorrespondingPoint>> = all_pts
99        .iter()
100        .map(|view| {
101            view.iter()
102                .map(|cp| checkerboard_calibrate::calibrate::CorrespondingPoint {
103                    object_point: cp.object_point,
104                    image_point: cp.image_point,
105                })
106                .collect()
107        })
108        .collect();
109
110    let res =
111        checkerboard_calibrate::calibrate::calibrate_camera(&views, width as u32, height as u32)
112            .map_err(|e| Error::Calibration(e.to_string()))?;
113
114    Ok(CalibrationResult {
115        mean_reprojection_distance_pixels: res.rms_reprojection_error,
116        camera_matrix: res.camera_matrix,
117        distortion_coeffs: res.distortion_coeffs,
118        image_width: res.image_width,
119        image_height: res.image_height,
120    })
121}