Skip to main content

trueno_image/
lib.rs

1#![cfg_attr(
2    test,
3    allow(
4        clippy::expect_used,
5        clippy::unwrap_used,
6        clippy::disallowed_methods,
7        clippy::float_cmp,
8        clippy::panic
9    )
10)]
11//! GPU image processing primitives.
12//!
13//! # Contract: image-conv2d-v1.yaml
14//!
15//! Provides convolution, Gaussian blur, Sobel edge detection, and
16//! Canny edge detection with provable properties.
17//!
18//! # Example
19//!
20//! ```
21//! use trueno_image::{conv2d, BorderMode};
22//!
23//! // Identity convolution (delta kernel)
24//! let image = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0_f32];
25//! let delta = [0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0_f32];
26//! let out = conv2d(&image, 3, 3, &delta, 3, 3, BorderMode::Zero).unwrap();
27//! assert!((out[4] - 5.0).abs() < 1e-6); // Center pixel preserved
28//! ```
29
30mod buf;
31mod color;
32mod conv;
33mod error;
34mod histogram;
35mod morphology;
36mod resize;
37
38#[cfg(test)]
39mod tests;
40
41pub use buf::{DType, ImageBuf};
42pub use color::{connected_components, hsv_to_rgb, rgb_to_gray, rgb_to_hsv};
43pub use conv::{
44    canny, canny_rgb, conv2d, gaussian_blur, gradient_magnitude, separable_conv2d, sobel,
45    BorderMode,
46};
47pub use error::ImageError;
48pub use histogram::{cumulative_histogram, equalize, histogram};
49pub use morphology::{closing, dilate, erode, opening};
50pub use resize::{resize, Interpolation};
51
52/// Image operations trait for `ImageBuf` method dispatch.
53///
54/// Provides a unified interface for applying image processing operations
55/// to structured `ImageBuf` instances with automatic dimension handling.
56pub trait ImageOps {
57    /// Apply 2D convolution with given kernel and border mode.
58    fn apply_conv2d(
59        &self,
60        kernel: &[f32],
61        kw: usize,
62        kh: usize,
63        border: BorderMode,
64    ) -> Result<ImageBuf, ImageError>;
65
66    /// Apply Gaussian blur with given sigma.
67    fn blur(&self, sigma: f32) -> Result<ImageBuf, ImageError>;
68
69    /// Compute Sobel gradients (gx, gy).
70    fn sobel_gradients(&self) -> Result<(ImageBuf, ImageBuf), ImageError>;
71
72    /// Apply Canny edge detection (converts multi-channel to grayscale).
73    fn canny_edges(&self, sigma: f32, low: f32, high: f32) -> Result<ImageBuf, ImageError>;
74
75    /// Morphological dilation with structuring element.
76    fn apply_dilate(&self, se: &[f32], sw: usize, sh: usize) -> Result<ImageBuf, ImageError>;
77
78    /// Morphological erosion with structuring element.
79    fn apply_erode(&self, se: &[f32], sw: usize, sh: usize) -> Result<ImageBuf, ImageError>;
80
81    /// Resize image to new dimensions.
82    fn apply_resize(
83        &self,
84        new_w: usize,
85        new_h: usize,
86        interp: Interpolation,
87    ) -> Result<ImageBuf, ImageError>;
88
89    /// Convert to grayscale.
90    fn to_gray(&self) -> Result<ImageBuf, ImageError>;
91
92    /// Convert RGB to HSV color space.
93    fn to_hsv(&self) -> Result<ImageBuf, ImageError>;
94
95    /// Compute histogram with given number of bins.
96    fn compute_histogram(&self, bins: usize) -> Result<Vec<u32>, ImageError>;
97
98    /// Label connected components in binary image.
99    fn label_components(&self) -> Result<(Vec<u32>, u32), ImageError>;
100}
101
102impl ImageOps for ImageBuf {
103    fn apply_conv2d(
104        &self,
105        kernel: &[f32],
106        kw: usize,
107        kh: usize,
108        border: BorderMode,
109    ) -> Result<ImageBuf, ImageError> {
110        if self.channels() == 1 {
111            let data = conv2d(
112                self.data(),
113                self.width(),
114                self.height(),
115                kernel,
116                kw,
117                kh,
118                border,
119            )?;
120            ImageBuf::new(data, self.width(), self.height(), 1)
121        } else {
122            let npix = self.width() * self.height();
123            let mut out = vec![0.0_f32; npix * self.channels()];
124            for c in 0..self.channels() {
125                let ch = self.channel(c)?;
126                let filtered = conv2d(
127                    ch.data(),
128                    self.width(),
129                    self.height(),
130                    kernel,
131                    kw,
132                    kh,
133                    border,
134                )?;
135                for i in 0..npix {
136                    out[i * self.channels() + c] = filtered[i];
137                }
138            }
139            ImageBuf::new(out, self.width(), self.height(), self.channels())
140        }
141    }
142
143    fn blur(&self, sigma: f32) -> Result<ImageBuf, ImageError> {
144        if self.channels() == 1 {
145            let data = gaussian_blur(self.data(), self.width(), self.height(), sigma)?;
146            ImageBuf::new(data, self.width(), self.height(), 1)
147        } else {
148            let npix = self.width() * self.height();
149            let mut out = vec![0.0_f32; npix * self.channels()];
150            for c in 0..self.channels() {
151                let ch = self.channel(c)?;
152                let blurred = gaussian_blur(ch.data(), self.width(), self.height(), sigma)?;
153                for i in 0..npix {
154                    out[i * self.channels() + c] = blurred[i];
155                }
156            }
157            ImageBuf::new(out, self.width(), self.height(), self.channels())
158        }
159    }
160
161    fn sobel_gradients(&self) -> Result<(ImageBuf, ImageBuf), ImageError> {
162        let gray = if self.channels() == 1 {
163            self.data().to_vec()
164        } else {
165            rgb_to_gray(self.data(), self.width(), self.height())?
166        };
167        let (gx, gy) = sobel(&gray, self.width(), self.height())?;
168        Ok((
169            ImageBuf::new(gx, self.width(), self.height(), 1)?,
170            ImageBuf::new(gy, self.width(), self.height(), 1)?,
171        ))
172    }
173
174    fn canny_edges(&self, sigma: f32, low: f32, high: f32) -> Result<ImageBuf, ImageError> {
175        let edges = canny_rgb(
176            self.data(),
177            self.width(),
178            self.height(),
179            self.channels(),
180            sigma,
181            low,
182            high,
183        )?;
184        ImageBuf::new(edges, self.width(), self.height(), 1)
185    }
186
187    fn apply_dilate(&self, se: &[f32], sw: usize, sh: usize) -> Result<ImageBuf, ImageError> {
188        if self.channels() == 1 {
189            let data = dilate(self.data(), self.width(), self.height(), se, sw, sh)?;
190            ImageBuf::new(data, self.width(), self.height(), 1)
191        } else {
192            let npix = self.width() * self.height();
193            let mut out = vec![0.0_f32; npix * self.channels()];
194            for c in 0..self.channels() {
195                let ch = self.channel(c)?;
196                let d = dilate(ch.data(), self.width(), self.height(), se, sw, sh)?;
197                for i in 0..npix {
198                    out[i * self.channels() + c] = d[i];
199                }
200            }
201            ImageBuf::new(out, self.width(), self.height(), self.channels())
202        }
203    }
204
205    fn apply_erode(&self, se: &[f32], sw: usize, sh: usize) -> Result<ImageBuf, ImageError> {
206        if self.channels() == 1 {
207            let data = erode(self.data(), self.width(), self.height(), se, sw, sh)?;
208            ImageBuf::new(data, self.width(), self.height(), 1)
209        } else {
210            let npix = self.width() * self.height();
211            let mut out = vec![0.0_f32; npix * self.channels()];
212            for c in 0..self.channels() {
213                let ch = self.channel(c)?;
214                let e = erode(ch.data(), self.width(), self.height(), se, sw, sh)?;
215                for i in 0..npix {
216                    out[i * self.channels() + c] = e[i];
217                }
218            }
219            ImageBuf::new(out, self.width(), self.height(), self.channels())
220        }
221    }
222
223    fn apply_resize(
224        &self,
225        new_w: usize,
226        new_h: usize,
227        interp: Interpolation,
228    ) -> Result<ImageBuf, ImageError> {
229        if self.channels() == 1 {
230            let data = resize(
231                self.data(),
232                self.width(),
233                self.height(),
234                new_w,
235                new_h,
236                interp,
237            )?;
238            ImageBuf::new(data, new_w, new_h, 1)
239        } else {
240            let new_npix = new_w * new_h;
241            let mut out = vec![0.0_f32; new_npix * self.channels()];
242            for c in 0..self.channels() {
243                let ch = self.channel(c)?;
244                let resized = resize(ch.data(), self.width(), self.height(), new_w, new_h, interp)?;
245                for i in 0..new_npix {
246                    out[i * self.channels() + c] = resized[i];
247                }
248            }
249            ImageBuf::new(out, new_w, new_h, self.channels())
250        }
251    }
252
253    fn to_gray(&self) -> Result<ImageBuf, ImageError> {
254        if self.channels() == 1 {
255            return Ok(self.clone());
256        }
257        let gray = rgb_to_gray(self.data(), self.width(), self.height())?;
258        ImageBuf::new(gray, self.width(), self.height(), 1)
259    }
260
261    fn to_hsv(&self) -> Result<ImageBuf, ImageError> {
262        let hsv = rgb_to_hsv(self.data(), self.width(), self.height())?;
263        ImageBuf::new(hsv, self.width(), self.height(), self.channels())
264    }
265
266    fn compute_histogram(&self, bins: usize) -> Result<Vec<u32>, ImageError> {
267        let gray = if self.channels() == 1 {
268            self.data().to_vec()
269        } else {
270            rgb_to_gray(self.data(), self.width(), self.height())?
271        };
272        histogram(&gray, self.width(), self.height(), bins)
273    }
274
275    fn label_components(&self) -> Result<(Vec<u32>, u32), ImageError> {
276        let gray = if self.channels() == 1 {
277            self.data().to_vec()
278        } else {
279            rgb_to_gray(self.data(), self.width(), self.height())?
280        };
281        let labels = connected_components(&gray, self.width(), self.height())?;
282        let max_label = labels.iter().copied().max().unwrap_or(0);
283        Ok((labels, max_label))
284    }
285}