Skip to main content

checkerboard_calibrate/chessboard/
detect.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! End-to-end chessboard detection — wiring stages 1-4 together.
5//!
6//! Mirrors the structure of OpenCV's `findChessboardCorners` with the
7//! `ADAPTIVE_THRESH | NORMALIZE_IMAGE` flags: equalize the image, then for an
8//! increasing number of dilations, binarize, generate quads, link them into a
9//! board graph, and try to extract a board of the requested size. The first
10//! dilation level that yields a complete, monotone board wins.
11//!
12//! Corner *order* matches the lattice row-major readout of [`extract_board`];
13//! canonicalizing to OpenCV's exact start corner/direction is handled by the
14//! caller's cross-check for now.
15
16use super::binarize::{adaptive_threshold_mean, equalize_hist};
17use super::board::extract_board;
18use super::contour::find_contours;
19use super::link::{connected_components, link_quads};
20use super::order::{assign_grid, order_all_corners};
21use super::quad::{Quad, contour_area, find_quads};
22
23/// Maximum number of dilation iterations to try (matches OpenCV's range).
24const MAX_DILATIONS: usize = 7;
25
26/// 3x3 dilation (max filter) of a binary image, out-of-bounds treated as 0,
27/// matching OpenCV's default `dilate` with a 3x3 rectangular kernel.
28fn dilate3x3(src: &[u8], w: usize, h: usize) -> Vec<u8> {
29    let mut dst = vec![0u8; src.len()];
30    for y in 0..h {
31        for x in 0..w {
32            let mut m = 0u8;
33            for dy in -1i32..=1 {
34                for dx in -1i32..=1 {
35                    let nx = x as i32 + dx;
36                    let ny = y as i32 + dy;
37                    if nx >= 0 && ny >= 0 && (nx as usize) < w && (ny as usize) < h {
38                        m = m.max(src[ny as usize * w + nx as usize]);
39                    }
40                }
41            }
42            dst[y * w + x] = m;
43        }
44    }
45    dst
46}
47
48/// Paint a `thickness`-pixel border of `value` around the image, as OpenCV does
49/// to close squares that touch the image edge.
50fn draw_border(img: &mut [u8], w: usize, h: usize, value: u8, thickness: usize) {
51    for y in 0..h {
52        for x in 0..w {
53            if x < thickness || y < thickness || x + thickness >= w || y + thickness >= h {
54                img[y * w + x] = value;
55            }
56        }
57    }
58}
59
60/// Detect a `pattern_w x pattern_h` (inner corners) chessboard in a grayscale
61/// image. Returns the inner corners row-major, or `None` if no board is found.
62///
63/// `pattern_w`/`pattern_h` are the inner-corner counts (e.g. 9x6).
64pub fn find_chessboard_corners(
65    gray: &[u8],
66    w: usize,
67    h: usize,
68    pattern_w: usize,
69    pattern_h: usize,
70) -> Option<Vec<(f32, f32)>> {
71    assert_eq!(gray.len(), w * h);
72    let eq = equalize_hist(gray);
73
74    // Adaptive-threshold block sizes scaled to the image (odd). Several scales
75    // are tried because the right neighborhood depends on the square size and
76    // perspective, as in OpenCV's multi-attempt loop.
77    let smaller = w.min(h);
78    let block_sizes: Vec<usize> = [smaller / 5, smaller / 9, smaller / 15]
79        .iter()
80        .map(|b| (b | 1).max(3))
81        .collect();
82    // Reject tiny noise quads and the whole-image background quad.
83    let min_area = 25.0;
84    let max_area = (w as f64) * (h as f64) * 0.5;
85
86    for dilations in 0..=MAX_DILATIONS {
87        for &block_size in &block_sizes {
88            for &delta in &[0.0f64, 5.0, 9.0] {
89                let mut bin = adaptive_threshold_mean(&eq, w, h, block_size, delta);
90                draw_border(&mut bin, w, h, 255, 1);
91                for _ in 0..dilations {
92                    bin = dilate3x3(&bin, w, h);
93                }
94
95                let contours = find_contours(&bin, w, h);
96                let all_quads = find_quads(&contours, min_area);
97                let quads: Vec<Quad> = all_quads
98                    .into_iter()
99                    .filter(|q| {
100                        let corners = [q.corners[0], q.corners[1], q.corners[2], q.corners[3]];
101                        contour_area(&corners) <= max_area
102                    })
103                    .collect();
104                if quads.len() < pattern_w * pattern_h / 4 {
105                    continue;
106                }
107
108                let mut linked = link_quads(&quads);
109                order_all_corners(&mut linked);
110                for comp in connected_components(&linked) {
111                    let grid = assign_grid(&linked, &comp);
112                    if let Some(corners) = extract_board(&linked, &grid, pattern_w, pattern_h) {
113                        return Some(corners);
114                    }
115                    if let Some(corners) = extract_board(&linked, &grid, pattern_h, pattern_w) {
116                        return Some(corners);
117                    }
118                }
119            }
120        }
121    }
122    None
123}