Skip to main content

chess_corners_core/
imageview.rs

1/// Minimal grayscale view for refinement without taking a dependency on `image`.
2#[derive(Copy, Clone, Debug)]
3pub struct ImageView<'a> {
4    pub(crate) data: &'a [u8],
5    pub(crate) width: usize,
6    pub(crate) height: usize,
7    /// Origin of the view in the coordinate system of the response map / base image.
8    ///
9    /// Use [`Self::origin`] to read this value from outside the crate. Setting the
10    /// origin is done through [`Self::with_origin`], which preserves invariants.
11    pub(crate) origin: [i32; 2],
12}
13
14impl<'a> ImageView<'a> {
15    /// Build a view over `data`, treated as `width * height` row-major
16    /// pixels with a zero origin. Returns `None` if `data.len() !=
17    /// width * height`.
18    pub fn from_u8_slice(width: usize, height: usize, data: &'a [u8]) -> Option<Self> {
19        if width.checked_mul(height)? != data.len() {
20            return None;
21        }
22        Some(Self {
23            data,
24            width,
25            height,
26            origin: [0, 0],
27        })
28    }
29
30    /// Like [`Self::from_u8_slice`], but with a non-zero `origin` in
31    /// the coordinate system of the response map / base image.
32    /// Returns `None` under the same condition.
33    pub fn with_origin(
34        width: usize,
35        height: usize,
36        data: &'a [u8],
37        origin: [i32; 2],
38    ) -> Option<Self> {
39        Self::from_u8_slice(width, height, data).map(|mut view| {
40            view.origin = origin;
41            view
42        })
43    }
44
45    /// Return the view's origin in the coordinate system of the response
46    /// map / base image. Use [`Self::with_origin`] to construct a view
47    /// with a non-zero origin.
48    #[inline]
49    pub fn origin(&self) -> [i32; 2] {
50        self.origin
51    }
52
53    /// Return the raw pixel data backing this view.
54    #[inline]
55    pub fn data(&self) -> &'a [u8] {
56        self.data
57    }
58
59    /// Return the view's width in pixels.
60    #[inline]
61    pub fn width(&self) -> usize {
62        self.width
63    }
64
65    /// Return the view's height in pixels.
66    #[inline]
67    pub fn height(&self) -> usize {
68        self.height
69    }
70
71    /// Whether a `radius`-pixel square patch centered at `(cx, cy)` (in
72    /// this view's external, origin-adjusted frame) fits entirely inside
73    /// the view without clamping.
74    #[inline]
75    pub fn supports_patch(&self, cx: i32, cy: i32, radius: i32) -> bool {
76        if self.width == 0 || self.height == 0 {
77            return false;
78        }
79
80        let gx = cx + self.origin[0];
81        let gy = cy + self.origin[1];
82        let min_x = 0;
83        let min_y = 0;
84        let max_x = self.width as i32 - 1;
85        let max_y = self.height as i32 - 1;
86        gx - radius >= min_x && gy - radius >= min_y && gx + radius <= max_x && gy + radius <= max_y
87    }
88
89    /// Nearest-pixel sample at an integer coordinate in the view's
90    /// external frame (`origin` applied, then clamped to the valid
91    /// pixel range). Returns `0.0` for an empty view.
92    #[inline]
93    pub fn sample(&self, gx: i32, gy: i32) -> f32 {
94        if self.width == 0 || self.height == 0 {
95            return 0.0;
96        }
97        let gx = gx + self.origin[0];
98        let gy = gy + self.origin[1];
99        let lx = gx.clamp(0, self.width.saturating_sub(1) as i32) as usize;
100        let ly = gy.clamp(0, self.height.saturating_sub(1) as i32) as usize;
101        self.data[ly * self.width + lx] as f32
102    }
103
104    /// Bilinear sample at subpixel coordinates. Coordinates are in the
105    /// view's external frame (same as [`Self::sample`]): `origin` is
106    /// applied, then the sample is clamped to the valid pixel range.
107    #[inline]
108    pub fn sample_bilinear(&self, gx: f32, gy: f32) -> f32 {
109        if self.width == 0 || self.height == 0 {
110            return 0.0;
111        }
112
113        let fx = gx + self.origin[0] as f32;
114        let fy = gy + self.origin[1] as f32;
115
116        let max_x = self.width.saturating_sub(1) as i32;
117        let max_y = self.height.saturating_sub(1) as i32;
118
119        let x0 = (fx.floor() as i32).clamp(0, max_x);
120        let y0 = (fy.floor() as i32).clamp(0, max_y);
121        let x1 = (x0 + 1).clamp(0, max_x);
122        let y1 = (y0 + 1).clamp(0, max_y);
123
124        // Fractional parts, guarded against samples outside the clamped
125        // range (where we already snapped to the border pixel).
126        let tx = (fx - x0 as f32).clamp(0.0, 1.0);
127        let ty = (fy - y0 as f32).clamp(0.0, 1.0);
128
129        let w = self.width;
130        let i00 = self.data[y0 as usize * w + x0 as usize] as f32;
131        let i10 = self.data[y0 as usize * w + x1 as usize] as f32;
132        let i01 = self.data[y1 as usize * w + x0 as usize] as f32;
133        let i11 = self.data[y1 as usize * w + x1 as usize] as f32;
134
135        let a = i00 + (i10 - i00) * tx;
136        let b = i01 + (i11 - i01) * tx;
137        a + (b - a) * ty
138    }
139}