Skip to main content

calib_targets_core/
image.rs

1/// Borrowed view over an 8-bit grayscale image.
2///
3/// A lightweight, image-crate-free pixel container: the workspace passes
4/// these into detectors so `calib-targets-core` stays decoupled from any
5/// concrete image type.
6#[derive(Clone, Copy, Debug)]
7pub struct GrayImageView<'a> {
8    /// Image width in pixels.
9    pub width: usize,
10    /// Image height in pixels.
11    pub height: usize,
12    /// Row-major pixel buffer; length must equal `width * height`.
13    pub data: &'a [u8],
14}
15
16/// Owned 8-bit grayscale image.
17///
18/// The owning counterpart of [`GrayImageView`]. Call [`GrayImage::view`]
19/// to borrow it for a detector.
20#[derive(Clone, Debug)]
21pub struct GrayImage {
22    /// Image width in pixels.
23    pub width: usize,
24    /// Image height in pixels.
25    pub height: usize,
26    /// Row-major pixel buffer; length must equal `width * height`.
27    pub data: Vec<u8>,
28}
29
30impl GrayImage {
31    /// Borrow this image as a [`GrayImageView`].
32    pub fn view(&self) -> GrayImageView<'_> {
33        GrayImageView {
34            width: self.width,
35            height: self.height,
36            data: &self.data,
37        }
38    }
39}
40
41#[inline]
42fn get_gray(src: &GrayImageView<'_>, x: i32, y: i32) -> u8 {
43    if x < 0 || y < 0 || x >= src.width as i32 || y >= src.height as i32 {
44        return 0;
45    }
46    src.data[y as usize * src.width + x as usize]
47}
48
49/// Bilinearly sample a grayscale image at sub-pixel `(x, y)`.
50///
51/// Coordinates are in pixels; pixel centers sit at integer coordinates.
52/// Samples whose 2×2 footprint falls outside the image treat the missing
53/// pixels as `0` (zero-padding). Returns the interpolated value as `f32`.
54#[inline]
55pub fn sample_bilinear(src: &GrayImageView<'_>, x: f32, y: f32) -> f32 {
56    let x0 = x.floor() as i32;
57    let y0 = y.floor() as i32;
58    let fx = x - x0 as f32;
59    let fy = y - y0 as f32;
60
61    let p00 = get_gray(src, x0, y0) as f32;
62    let p10 = get_gray(src, x0 + 1, y0) as f32;
63    let p01 = get_gray(src, x0, y0 + 1) as f32;
64    let p11 = get_gray(src, x0 + 1, y0 + 1) as f32;
65
66    let a = p00 + fx * (p10 - p00);
67    let b = p01 + fx * (p11 - p01);
68    a + fy * (b - a)
69}
70
71/// Bilinearly sample a grayscale image, skipping bounds checks on the hot
72/// path.
73///
74/// Identical result to [`sample_bilinear`], but when the 2×2 footprint is
75/// fully inside the image it indexes the buffer directly. Out-of-range
76/// footprints fall back to [`sample_bilinear`] (zero-padding semantics).
77#[inline]
78pub fn sample_bilinear_fast(src: &GrayImageView<'_>, x: f32, y: f32) -> f32 {
79    let x0 = x.floor() as i32;
80    let y0 = y.floor() as i32;
81
82    if x0 < 0 || y0 < 0 || x0 + 1 >= src.width as i32 || y0 + 1 >= src.height as i32 {
83        return sample_bilinear(src, x, y);
84    }
85
86    let fx = x - x0 as f32;
87    let fy = y - y0 as f32;
88    let base = y0 as usize * src.width + x0 as usize;
89
90    let p00 = src.data[base] as f32;
91    let p10 = src.data[base + 1] as f32;
92    let p01 = src.data[base + src.width] as f32;
93    let p11 = src.data[base + src.width + 1] as f32;
94
95    let a = p00 + fx * (p10 - p00);
96    let b = p01 + fx * (p11 - p01);
97    a + fy * (b - a)
98}
99
100/// Bilinearly sample a grayscale image and round the result to `u8`.
101///
102/// Wraps [`sample_bilinear`], clamping the interpolated value to
103/// `0..=255` before truncation.
104#[inline]
105pub fn sample_bilinear_u8(src: &GrayImageView<'_>, x: f32, y: f32) -> u8 {
106    sample_bilinear(src, x, y).clamp(0.0, 255.0) as u8
107}