calib-targets-print 0.13.0

Printable calibration target generation for chessboard, ChArUco, PuzzleBoard, and marker boards
Documentation
//! Printable marker-board (checkerboard + 3-circle) target specification.

use calib_targets_marker::{CirclePolarity, MarkerBoardSpec};
use serde::{Deserialize, Serialize};

use super::chessboard::validate_inner_corner_grid;
use super::error::PrintableTargetError;

pub(super) fn default_circle_diameter_rel() -> f64 {
    0.5
}

/// One circle in the printable marker board layout.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MarkerCircleSpec {
    /// Cell column index of the circle.
    pub i: u32,
    /// Cell row index of the circle.
    pub j: u32,
    /// Whether the circle is a white or black disk.
    pub polarity: CirclePolarity,
}

impl MarkerCircleSpec {
    /// Build a printable marker-circle spec at cell `(i, j)` with the given
    /// polarity.
    pub fn new(i: u32, j: u32, polarity: CirclePolarity) -> Self {
        Self { i, j, polarity }
    }

    /// Convert to the detector `MarkerCircleSpec`.
    pub fn to_detector_spec(self) -> calib_targets_marker::MarkerCircleSpec {
        calib_targets_marker::MarkerCircleSpec::new(
            calib_targets_marker::CellCoords {
                i: self.i as i32,
                j: self.j as i32,
            },
            self.polarity,
        )
    }
}

/// Printable marker-board (checkerboard + coloured circle overlay) target.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MarkerBoardTargetSpec {
    /// Number of inner corner-intersection rows.
    pub inner_rows: u32,
    /// Number of inner corner-intersection columns.
    pub inner_cols: u32,
    /// Side length of one square in millimeters.
    pub square_size_mm: f64,
    /// The three marker circles overlaid on the board.
    pub circles: [MarkerCircleSpec; 3],
    /// Circle diameter as a fraction of the square side.
    #[serde(default = "default_circle_diameter_rel")]
    pub circle_diameter_rel: f64,
}

impl MarkerBoardTargetSpec {
    /// Build a printable marker-board target from its inner-corner grid size,
    /// square size (mm), and the three overlaid circles. The circle diameter
    /// defaults; override it with
    /// [`MarkerBoardTargetSpec::with_circle_diameter_rel`].
    pub fn new(
        inner_rows: u32,
        inner_cols: u32,
        square_size_mm: f64,
        circles: [MarkerCircleSpec; 3],
    ) -> Self {
        Self {
            inner_rows,
            inner_cols,
            square_size_mm,
            circles,
            circle_diameter_rel: default_circle_diameter_rel(),
        }
    }

    /// Override the circle diameter as a fraction of the square side.
    #[must_use]
    pub fn with_circle_diameter_rel(mut self, circle_diameter_rel: f64) -> Self {
        self.circle_diameter_rel = circle_diameter_rel;
        self
    }

    /// Compute a centred default 3-circle layout for the given board size.
    pub fn default_circles(inner_rows: u32, inner_cols: u32) -> [MarkerCircleSpec; 3] {
        let squares_x = inner_cols + 1;
        let squares_y = inner_rows + 1;
        let cx = squares_x / 2;
        let cy = squares_y / 2;
        [
            MarkerCircleSpec {
                i: cx.saturating_sub(1),
                j: cy.saturating_sub(1),
                polarity: CirclePolarity::White,
            },
            MarkerCircleSpec {
                i: cx,
                j: cy.saturating_sub(1),
                polarity: CirclePolarity::Black,
            },
            MarkerCircleSpec {
                i: cx,
                j: cy,
                polarity: CirclePolarity::White,
            },
        ]
    }

    /// Build a printable marker-board target from a detector layout whose
    /// `cell_size` is already expressed in millimeters.
    pub fn try_from_layout_mm(layout: &MarkerBoardSpec) -> Result<Self, PrintableTargetError> {
        let square_size_mm = layout
            .cell_size
            .map(f64::from)
            .ok_or(PrintableTargetError::MissingMarkerBoardCellSize)?;
        let [circle0, circle1, circle2] = layout.circles;
        Ok(Self {
            inner_rows: layout.rows,
            inner_cols: layout.cols,
            square_size_mm,
            circles: [
                try_printable_circle_from_detector_spec(circle0)?,
                try_printable_circle_from_detector_spec(circle1)?,
                try_printable_circle_from_detector_spec(circle2)?,
            ],
            circle_diameter_rel: default_circle_diameter_rel(),
        })
    }
}

pub(crate) fn validate_marker_board_spec(
    spec: &MarkerBoardTargetSpec,
) -> Result<(), PrintableTargetError> {
    validate_inner_corner_grid(spec.inner_rows, spec.inner_cols, spec.square_size_mm)?;
    if !spec.circle_diameter_rel.is_finite()
        || spec.circle_diameter_rel <= 0.0
        || spec.circle_diameter_rel > 1.0
    {
        return Err(PrintableTargetError::InvalidCircleDiameter);
    }
    let squares_x = spec.inner_cols + 1;
    let squares_y = spec.inner_rows + 1;
    let mut seen = std::collections::BTreeSet::new();
    for circle in spec.circles {
        if circle.i >= squares_x || circle.j >= squares_y {
            return Err(PrintableTargetError::InvalidCircleCell);
        }
        if !seen.insert((circle.i, circle.j)) {
            return Err(PrintableTargetError::DuplicateCircleCells);
        }
    }
    Ok(())
}

pub(crate) fn try_printable_circle_from_detector_spec(
    circle: calib_targets_marker::MarkerCircleSpec,
) -> Result<MarkerCircleSpec, PrintableTargetError> {
    Ok(MarkerCircleSpec {
        i: u32::try_from(circle.cell.i).map_err(|_| PrintableTargetError::InvalidCircleCell)?,
        j: u32::try_from(circle.cell.j).map_err(|_| PrintableTargetError::InvalidCircleCell)?,
        polarity: circle.polarity,
    })
}