1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
4
5impl<B: Backend> Image<B> {
6 pub fn resize(&self, new_width: usize, new_height: usize) -> Result<Self> {
9 let dims = self.tensor.dims();
10 let _c = dims[0];
11 let h = dims[1];
12 let w = dims[2];
13
14 if new_width == 0 || new_height == 0 {
15 return Err(IrisError::InvalidParameter(
16 "Dimensions must be greater than zero".into(),
17 ));
18 }
19
20 let device = &self.tensor.device();
21
22 let y_indices_vec: Vec<i32> = (0..new_height)
24 .map(|y| ((y * h) / new_height) as i32)
25 .collect();
26 let y_indices =
27 Tensor::<B, 1, Int>::from_data(TensorData::new(y_indices_vec, [new_height]), device);
28
29 let x_indices_vec: Vec<i32> = (0..new_width)
31 .map(|x| ((x * w) / new_width) as i32)
32 .collect();
33 let x_indices =
34 Tensor::<B, 1, Int>::from_data(TensorData::new(x_indices_vec, [new_width]), device);
35
36 let resized = self
38 .tensor
39 .clone()
40 .select(1, y_indices)
41 .select(2, x_indices);
42
43 Ok(Image::new(resized))
44 }
45
46 pub fn crop(&self, x: usize, y: usize, width: usize, height: usize) -> Result<Self> {
48 let dims = self.tensor.dims();
49 let c = dims[0];
50 let h = dims[1];
51 let w = dims[2];
52
53 if x + width > w || y + height > h {
54 return Err(IrisError::DimensionMismatch {
55 expected: vec![c, height, width],
56 actual: vec![c, h, w],
57 });
58 }
59
60 let cropped = self
62 .tensor
63 .clone()
64 .slice([0..c, y..(y + height), x..(x + width)]);
65 Ok(Image::new(cropped))
66 }
67
68 pub fn flip(&self, horizontal: bool, vertical: bool) -> Result<Self> {
72 let mut flipped = self.tensor.clone();
73 if vertical {
74 flipped = flipped.flip([1]);
76 }
77 if horizontal {
78 flipped = flipped.flip([2]);
80 }
81 Ok(Image::new(flipped))
82 }
83
84 pub fn rotate(&self, angle_degrees: u32) -> Result<Self> {
86 match angle_degrees {
87 0 | 360 => Ok(self.clone()),
88 90 => {
89 let transposed = self.tensor.clone().swap_dims(1, 2);
91 let rotated = transposed.flip([2]);
92 Ok(Image::new(rotated))
93 }
94 180 => {
95 let rotated = self.tensor.clone().flip([1, 2]);
97 Ok(Image::new(rotated))
98 }
99 270 => {
100 let transposed = self.tensor.clone().swap_dims(1, 2);
102 let rotated = transposed.flip([1]);
103 Ok(Image::new(rotated))
104 }
105 _ => Err(IrisError::InvalidParameter(
106 "Only 90, 180, 270 degrees rotations are supported".into(),
107 )),
108 }
109 }
110
111 pub fn grayscale(&self) -> Result<Self> {
114 let dims = self.tensor.dims();
115 let c = dims[0];
116 let h = dims[1];
117 let w = dims[2];
118
119 if c == 1 {
120 return Ok(self.clone());
121 }
122
123 if c < 3 {
124 return Err(IrisError::Tensor(
125 "Cannot convert image with less than 3 channels to grayscale".into(),
126 ));
127 }
128
129 let r = self.tensor.clone().slice([0..1, 0..h, 0..w]);
131 let g = self.tensor.clone().slice([1..2, 0..h, 0..w]);
132 let b = self.tensor.clone().slice([2..3, 0..h, 0..w]);
133
134 let gray = r
135 .mul_scalar(0.299)
136 .add(g.mul_scalar(0.587))
137 .add(b.mul_scalar(0.114));
138
139 Ok(Image::new(gray))
140 }
141
142 pub fn to_rgb(&self) -> Result<Self> {
144 let dims = self.tensor.dims();
145 let c = dims[0];
146 if c == 3 {
147 return Ok(self.clone());
148 }
149 if c != 1 {
150 return Err(IrisError::Tensor(
151 "Input image must be single-channel to convert to RGB".into(),
152 ));
153 }
154
155 let rgb = Tensor::cat(
157 vec![
158 self.tensor.clone(),
159 self.tensor.clone(),
160 self.tensor.clone(),
161 ],
162 0,
163 );
164 Ok(Image::new(rgb))
165 }
166
167 pub fn gaussian_pyramid(&self, levels: usize) -> Result<Vec<Self>> {
170 let mut pyramid = Vec::with_capacity(levels);
171 pyramid.push(self.clone());
172
173 let mut current = self.clone();
174 for _ in 1..levels {
175 let dims = current.tensor.dims();
176 let h = dims[1];
177 let w = dims[2];
178 let new_h = h / 2;
179 let new_w = w / 2;
180 if new_h == 0 || new_w == 0 {
181 break;
182 }
183 let blurred = current.gaussian_blur(3, 1.0)?;
185 let downsampled = blurred.resize(new_w, new_h)?;
186 pyramid.push(downsampled);
187 current = pyramid.last().cloned().unwrap();
188 }
189
190 Ok(pyramid)
191 }
192
193 pub fn integral_image(&self) -> Result<Image<B>> {
197 let gray = self.grayscale()?;
198 let dims = gray.tensor.dims();
199 let h = dims[1];
200 let w = dims[2];
201
202 let tensor_data = gray.tensor.clone().into_data();
203 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
204
205 let mut integral = vec![0.0f32; (h + 1) * (w + 1)];
206
207 for y in 0..h {
208 let mut row_sum = 0.0f32;
209 for x in 0..w {
210 row_sum += flat_vals[y * w + x];
211 integral[(y + 1) * (w + 1) + (x + 1)] = integral[y * (w + 1) + (x + 1)] + row_sum;
212 }
213 }
214
215 let device = gray.tensor.device();
216 let data = TensorData::new(integral, [1, h + 1, w + 1]);
217 let tensor = Tensor::<B, 3>::from_data(data, &device);
218 Ok(Image::new(tensor))
219 }
220
221 pub fn flood_fill(
225 &self,
226 seed_x: usize,
227 seed_y: usize,
228 fill_value: f32,
229 lo_diff: f32,
230 hi_diff: f32,
231 ) -> Result<Self> {
232 let gray = self.grayscale()?;
233 let dims = gray.tensor.dims();
234 let h = dims[1];
235 let w = dims[2];
236
237 if seed_x >= w || seed_y >= h {
238 return Err(IrisError::InvalidParameter(
239 "Seed point is outside image bounds".into(),
240 ));
241 }
242
243 let tensor_data = gray.tensor.clone().into_data();
244 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
245 let mut out_vals = flat_vals.clone();
246
247 let seed_val = flat_vals[seed_y * w + seed_x];
248 let lo = seed_val - lo_diff;
249 let hi = seed_val + hi_diff;
250
251 let mut visited = vec![false; h * w];
253 let mut queue = std::collections::VecDeque::new();
254 queue.push_back((seed_x, seed_y));
255 visited[seed_y * w + seed_x] = true;
256
257 let dx = [1, 0, -1, 0];
258 let dy = [0, 1, 0, -1];
259
260 while let Some((cx, cy)) = queue.pop_front() {
261 out_vals[cy * w + cx] = fill_value;
262
263 for d in 0..4 {
264 let nx = cx as isize + dx[d];
265 let ny = cy as isize + dy[d];
266 if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
267 let ux = nx as usize;
268 let uy = ny as usize;
269 let idx = uy * w + ux;
270 if !visited[idx] {
271 let pixel = flat_vals[idx];
272 if pixel >= lo && pixel <= hi {
273 visited[idx] = true;
274 queue.push_back((ux, uy));
275 }
276 }
277 }
278 }
279 }
280
281 let device = gray.tensor.device();
282 let data = TensorData::new(out_vals, [1, h, w]);
283 let tensor = Tensor::<B, 3>::from_data(data, &device);
284 Ok(Image::new(tensor))
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::test_helpers::{TestBackend, test_device};
292 use burn::tensor::TensorData;
293
294 #[test]
295 fn test_image_conversions() {
296 let device = test_device();
297 let flat_data = vec![
298 0.1f32, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2,
299 ];
300 let tensor =
301 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 2, 2]), &device);
302 let img = Image::new(tensor);
303
304 let gray = img.grayscale().unwrap();
305 assert_eq!(gray.shape(), [1, 2, 2]);
306
307 let rgb = gray.to_rgb().unwrap();
308 assert_eq!(rgb.shape(), [3, 2, 2]);
309 }
310}