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 data: &'a [u8],
5    pub width: usize,
6    pub 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    pub fn from_u8_slice(width: usize, height: usize, data: &'a [u8]) -> Option<Self> {
16        if width.checked_mul(height)? != data.len() {
17            return None;
18        }
19        Some(Self {
20            data,
21            width,
22            height,
23            origin: [0, 0],
24        })
25    }
26
27    pub fn with_origin(
28        width: usize,
29        height: usize,
30        data: &'a [u8],
31        origin: [i32; 2],
32    ) -> Option<Self> {
33        Self::from_u8_slice(width, height, data).map(|mut view| {
34            view.origin = origin;
35            view
36        })
37    }
38
39    /// Return the view's origin in the coordinate system of the response
40    /// map / base image. Use [`Self::with_origin`] to construct a view
41    /// with a non-zero origin.
42    #[inline]
43    pub fn origin(&self) -> [i32; 2] {
44        self.origin
45    }
46
47    #[inline]
48    pub fn supports_patch(&self, cx: i32, cy: i32, radius: i32) -> bool {
49        if self.width == 0 || self.height == 0 {
50            return false;
51        }
52
53        let gx = cx + self.origin[0];
54        let gy = cy + self.origin[1];
55        let min_x = 0;
56        let min_y = 0;
57        let max_x = self.width as i32 - 1;
58        let max_y = self.height as i32 - 1;
59        gx - radius >= min_x && gy - radius >= min_y && gx + radius <= max_x && gy + radius <= max_y
60    }
61
62    #[inline]
63    pub fn sample(&self, gx: i32, gy: i32) -> f32 {
64        if self.width == 0 || self.height == 0 {
65            return 0.0;
66        }
67        let gx = gx + self.origin[0];
68        let gy = gy + self.origin[1];
69        let lx = gx.clamp(0, self.width.saturating_sub(1) as i32) as usize;
70        let ly = gy.clamp(0, self.height.saturating_sub(1) as i32) as usize;
71        self.data[ly * self.width + lx] as f32
72    }
73
74    /// Bilinear sample at subpixel coordinates. Coordinates are in the
75    /// view's external frame (same as [`Self::sample`]): `origin` is
76    /// applied, then the sample is clamped to the valid pixel range.
77    #[inline]
78    pub fn sample_bilinear(&self, gx: f32, gy: f32) -> f32 {
79        if self.width == 0 || self.height == 0 {
80            return 0.0;
81        }
82
83        let fx = gx + self.origin[0] as f32;
84        let fy = gy + self.origin[1] as f32;
85
86        let max_x = self.width.saturating_sub(1) as i32;
87        let max_y = self.height.saturating_sub(1) as i32;
88
89        let x0 = (fx.floor() as i32).clamp(0, max_x);
90        let y0 = (fy.floor() as i32).clamp(0, max_y);
91        let x1 = (x0 + 1).clamp(0, max_x);
92        let y1 = (y0 + 1).clamp(0, max_y);
93
94        // Fractional parts, guarded against samples outside the clamped
95        // range (where we already snapped to the border pixel).
96        let tx = (fx - x0 as f32).clamp(0.0, 1.0);
97        let ty = (fy - y0 as f32).clamp(0.0, 1.0);
98
99        let w = self.width;
100        let i00 = self.data[y0 as usize * w + x0 as usize] as f32;
101        let i10 = self.data[y0 as usize * w + x1 as usize] as f32;
102        let i01 = self.data[y1 as usize * w + x0 as usize] as f32;
103        let i11 = self.data[y1 as usize * w + x1 as usize] as f32;
104
105        let a = i00 + (i10 - i00) * tx;
106        let b = i01 + (i11 - i01) * tx;
107        a + (b - a) * ty
108    }
109}