1use crate::core::types::Point;
2use crate::error::{IrisError, Result};
3use crate::image::Image;
4use burn::tensor::{Tensor, TensorData, backend::Backend};
5
6impl<B: Backend> Image<B> {
7 pub fn transpose(&self) -> Result<Self> {
9 let transposed = self.tensor.clone().swap_dims(1, 2);
10 Ok(Image::new(transposed))
11 }
12
13 pub fn warp_affine(
15 &self,
16 m: [[f64; 3]; 2],
17 new_width: usize,
18 new_height: usize,
19 ) -> Result<Self> {
20 let dims = self.tensor.dims();
21 let c = dims[0];
22 let h = dims[1];
23 let w = dims[2];
24
25 let device = self.tensor.device();
26 let tensor_data = self.tensor.clone().into_data();
27 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
28 let mut out_vals = vec![0.0f32; c * new_height * new_width];
29
30 let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
32 if det.abs() < 1e-9 {
33 return Err(IrisError::InvalidParameter(
34 "Transformation matrix is singular".into(),
35 ));
36 }
37 let inv_det = 1.0 / det;
38
39 let a_inv = [
41 [m[1][1] * inv_det, -m[0][1] * inv_det],
42 [-m[1][0] * inv_det, m[0][0] * inv_det],
43 ];
44 let tx_inv = -(a_inv[0][0] * m[0][2] + a_inv[0][1] * m[1][2]);
45 let ty_inv = -(a_inv[1][0] * m[0][2] + a_inv[1][1] * m[1][2]);
46
47 {
48 use rayon::prelude::*;
49 out_vals
50 .par_chunks_exact_mut(new_width)
51 .enumerate()
52 .for_each(|(idx, row)| {
53 let ch = idx / new_height;
54 let dy = idx % new_height;
55 for dx in 0..new_width {
56 let sx = a_inv[0][0] * (dx as f64) + a_inv[0][1] * (dy as f64) + tx_inv;
58 let sy = a_inv[1][0] * (dx as f64) + a_inv[1][1] * (dy as f64) + ty_inv;
59
60 let sx_round = sx.round() as isize;
61 let sy_round = sy.round() as isize;
62
63 if sx_round >= 0
64 && sx_round < w as isize
65 && sy_round >= 0
66 && sy_round < h as isize
67 {
68 row[dx] = flat_vals
69 [ch * h * w + (sy_round as usize) * w + (sx_round as usize)];
70 }
71 }
72 });
73 }
74
75 let new_data = TensorData::new(out_vals, [c, new_height, new_width]);
76 let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
77 Ok(Image::new(new_tensor))
78 }
79
80 pub fn warp_perspective(
82 &self,
83 m: [[f64; 3]; 3],
84 new_width: usize,
85 new_height: usize,
86 ) -> Result<Self> {
87 let dims = self.tensor.dims();
88 let c = dims[0];
89 let h = dims[1];
90 let w = dims[2];
91
92 let device = self.tensor.device();
93 let tensor_data = self.tensor.clone().into_data();
94 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
95 let mut out_vals = vec![0.0f32; c * new_height * new_width];
96
97 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
99 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
100 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
101
102 if det.abs() < 1e-9 {
103 return Err(IrisError::InvalidParameter(
104 "Perspective matrix is singular".into(),
105 ));
106 }
107 let inv_det = 1.0 / det;
108
109 let m_inv = [
110 [
111 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
112 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
113 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
114 ],
115 [
116 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
117 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
118 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
119 ],
120 [
121 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
122 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
123 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
124 ],
125 ];
126
127 {
128 use rayon::prelude::*;
129 out_vals
130 .par_chunks_exact_mut(new_width)
131 .enumerate()
132 .for_each(|(idx, row)| {
133 let ch = idx / new_height;
134 let dy = idx % new_height;
135 for dx in 0..new_width {
136 let x_mapped =
137 m_inv[0][0] * (dx as f64) + m_inv[0][1] * (dy as f64) + m_inv[0][2];
138 let y_mapped =
139 m_inv[1][0] * (dx as f64) + m_inv[1][1] * (dy as f64) + m_inv[1][2];
140 let z_mapped =
141 m_inv[2][0] * (dx as f64) + m_inv[2][1] * (dy as f64) + m_inv[2][2];
142
143 if z_mapped.abs() > 1e-9 {
144 let sx = x_mapped / z_mapped;
145 let sy = y_mapped / z_mapped;
146 let sx_round = sx.round() as isize;
147 let sy_round = sy.round() as isize;
148
149 if sx_round >= 0
150 && sx_round < w as isize
151 && sy_round >= 0
152 && sy_round < h as isize
153 {
154 row[dx] = flat_vals
155 [ch * h * w + (sy_round as usize) * w + (sx_round as usize)];
156 }
157 }
158 }
159 });
160 }
161
162 let new_data = TensorData::new(out_vals, [c, new_height, new_width]);
163 let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
164 Ok(Image::new(new_tensor))
165 }
166
167 pub fn remap(&self, map_x: &Tensor<B, 2>, map_y: &Tensor<B, 2>) -> Result<Self> {
169 let dims = self.tensor.dims();
170 let c = dims[0];
171 let h = dims[1];
172 let w = dims[2];
173
174 let map_dims = map_x.dims();
175 let out_h = map_dims[0];
176 let out_w = map_dims[1];
177
178 let device = self.tensor.device();
179 let tensor_data = self.tensor.clone().into_data();
180 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
181
182 let data_map_x = map_x.clone().into_data();
183 let data_map_y = map_y.clone().into_data();
184 let float_map_x: Vec<f32> = data_map_x.iter::<f32>().collect();
185 let float_map_y: Vec<f32> = data_map_y.iter::<f32>().collect();
186
187 let mut out_vals = vec![0.0f32; c * out_h * out_w];
188
189 {
190 use rayon::prelude::*;
191 out_vals
192 .par_chunks_exact_mut(out_w)
193 .enumerate()
194 .for_each(|(idx, row)| {
195 let ch = idx / out_h;
196 let dy = idx % out_h;
197 for dx in 0..out_w {
198 let map_idx = dy * out_w + dx;
199 let sx = float_map_x[map_idx].round() as isize;
200 let sy = float_map_y[map_idx].round() as isize;
201
202 if sx >= 0 && sx < w as isize && sy >= 0 && sy < h as isize {
203 row[dx] = flat_vals[ch * h * w + (sy as usize) * w + (sx as usize)];
204 }
205 }
206 });
207 }
208
209 let new_data = TensorData::new(out_vals, [c, out_h, out_w]);
210 let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
211 Ok(Image::new(new_tensor))
212 }
213
214 pub fn undistort(&self, camera_matrix: &Tensor<B, 2>, dist_coeffs: &[f32]) -> Result<Self> {
225 let dims = self.tensor.dims();
226 let c = dims[0];
227 let h = dims[1];
228 let w = dims[2];
229
230 let cm_data = camera_matrix.clone().into_data();
231 let cm_vals: Vec<f32> = cm_data.iter::<f32>().collect();
232 if cm_vals.len() < 9 {
233 return Err(IrisError::InvalidParameter(
234 "Camera matrix must be 3x3".into(),
235 ));
236 }
237 let fx = cm_vals[0] as f64;
238 let fy = cm_vals[4] as f64;
239 let cx = cm_vals[2] as f64;
240 let cy = cm_vals[5] as f64;
241
242 let k1 = dist_coeffs.first().copied().unwrap_or(0.0) as f64;
243 let k2 = dist_coeffs.get(1).copied().unwrap_or(0.0) as f64;
244 let p1 = dist_coeffs.get(2).copied().unwrap_or(0.0) as f64;
245 let p2 = dist_coeffs.get(3).copied().unwrap_or(0.0) as f64;
246 let k3 = dist_coeffs.get(4).copied().unwrap_or(0.0) as f64;
247
248 let tensor_data = self.tensor.clone().into_data();
249 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
250 let mut out_vals = vec![0.0f32; c * h * w];
251
252 {
253 use rayon::prelude::*;
254 out_vals
255 .par_chunks_exact_mut(w)
256 .enumerate()
257 .for_each(|(idx, row)| {
258 let ch = idx / h;
259 let dy = idx % h;
260 for dx in 0..w {
261 let x_cam = (dx as f64 - cx) / fx;
263 let y_cam = (dy as f64 - cy) / fy;
264
265 let r2 = x_cam * x_cam + y_cam * y_cam;
266 let r4 = r2 * r2;
267 let r6 = r4 * r2;
268
269 let radial = 1.0 + k1 * r2 + k2 * r4 + k3 * r6;
271
272 let x_distorted = x_cam * radial
274 + 2.0 * p1 * x_cam * y_cam
275 + p2 * (r2 + 2.0 * x_cam * x_cam);
276 let y_distorted = y_cam * radial
277 + p1 * (r2 + 2.0 * y_cam * y_cam)
278 + 2.0 * p2 * x_cam * y_cam;
279
280 let sx = (fx * x_distorted + cx).round() as isize;
282 let sy = (fy * y_distorted + cy).round() as isize;
283
284 if sx >= 0 && sx < w as isize && sy >= 0 && sy < h as isize {
285 row[dx] = flat_vals[ch * h * w + (sy as usize) * w + (sx as usize)];
286 }
287 }
288 });
289 }
290
291 let new_data = TensorData::new(out_vals, [c, h, w]);
292 let new_tensor = Tensor::<B, 3>::from_data(new_data, &self.tensor.device());
293 Ok(Image::new(new_tensor))
294 }
295
296 pub fn pyr_down(&self) -> Result<Self> {
302 let dims = self.tensor.dims();
303 let c = dims[0];
304 let h = dims[1];
305 let w = dims[2];
306
307 if h < 2 || w < 2 {
308 return Err(IrisError::InvalidParameter(
309 "Image too small for pyr_down (need at least 2x2)".into(),
310 ));
311 }
312
313 let new_h = h / 2;
314 let new_w = w / 2;
315
316 let tensor_data = self.tensor.clone().into_data();
317 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
318
319 let kernel: [f64; 25] = [
321 1.0, 4.0, 6.0, 4.0, 1.0, 4.0, 16.0, 24.0, 16.0, 4.0, 6.0, 24.0, 36.0, 24.0, 6.0, 4.0,
322 16.0, 24.0, 16.0, 4.0, 1.0, 4.0, 6.0, 4.0, 1.0,
323 ];
324 let ksum: f64 = 256.0;
325
326 let mut out_vals = vec![0.0f32; c * new_h * new_w];
327
328 {
329 use rayon::prelude::*;
330 out_vals
331 .par_chunks_exact_mut(new_w)
332 .enumerate()
333 .for_each(|(idx, row)| {
334 let ch = idx / new_h;
335 let dy = idx % new_h;
336 for dx in 0..new_w {
337 let sx_base = (dx * 2) as isize - 2;
339 let sy_base = (dy * 2) as isize - 2;
340
341 let mut sum = 0.0f64;
342 for ky in 0..5i32 {
343 for kx in 0..5i32 {
344 let px = sx_base + kx as isize;
345 let py = sy_base + ky as isize;
346 let px = px.clamp(0, w as isize - 1) as usize;
347 let py = py.clamp(0, h as isize - 1) as usize;
348 let pixel = flat_vals[ch * h * w + py * w + px] as f64;
349 sum += pixel * kernel[(ky * 5 + kx) as usize];
350 }
351 }
352 row[dx] = (sum / ksum) as f32;
353 }
354 });
355 }
356
357 let new_data = TensorData::new(out_vals, [c, new_h, new_w]);
358 let new_tensor = Tensor::<B, 3>::from_data(new_data, &self.tensor.device());
359 Ok(Image::new(new_tensor))
360 }
361
362 pub fn pyr_up(&self) -> Result<Self> {
369 let dims = self.tensor.dims();
370 let c = dims[0];
371 let h = dims[1];
372 let w = dims[2];
373
374 let new_h = 2 * (h - 1) + 1;
375 let new_w = 2 * (w - 1) + 1;
376
377 let tensor_data = self.tensor.clone().into_data();
378 let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
379
380 let kernel: [f64; 25] = [
382 1.0, 4.0, 6.0, 4.0, 1.0, 4.0, 16.0, 24.0, 16.0, 4.0, 6.0, 24.0, 36.0, 24.0, 6.0, 4.0,
383 16.0, 24.0, 16.0, 4.0, 1.0, 4.0, 6.0, 4.0, 1.0,
384 ];
385 let ksum: f64 = 256.0;
386
387 let mut up_vals = vec![0.0f32; c * new_h * new_w];
389 for ch in 0..c {
390 for sy in 0..h {
391 for sx in 0..w {
392 up_vals[ch * new_h * new_w + sy * 2 * new_w + sx * 2] =
393 flat_vals[ch * h * w + sy * w + sx];
394 }
395 }
396 }
397
398 let mut out_vals = vec![0.0f32; c * new_h * new_w];
400
401 {
402 use rayon::prelude::*;
403 out_vals
404 .par_chunks_exact_mut(new_w)
405 .enumerate()
406 .for_each(|(idx, row)| {
407 let ch = idx / new_h;
408 let dy = idx % new_h;
409 for dx in 0..new_w {
410 let sx_base = dx as isize - 2;
411 let sy_base = dy as isize - 2;
412
413 let mut sum = 0.0f64;
414 for ky in 0..5i32 {
415 for kx in 0..5i32 {
416 let px =
417 (sx_base + kx as isize).clamp(0, new_w as isize - 1) as usize;
418 let py =
419 (sy_base + ky as isize).clamp(0, new_h as isize - 1) as usize;
420 let pixel = up_vals[ch * new_h * new_w + py * new_w + px] as f64;
421 sum += pixel * kernel[(ky * 5 + kx) as usize];
422 }
423 }
424 row[dx] = (sum * 4.0 / ksum) as f32;
425 }
426 });
427 }
428
429 let new_data = TensorData::new(out_vals, [c, new_h, new_w]);
430 let new_tensor = Tensor::<B, 3>::from_data(new_data, &self.tensor.device());
431 Ok(Image::new(new_tensor))
432 }
433}
434
435pub struct GeometricTransform;
437
438impl GeometricTransform {
439 #[must_use]
441 pub fn get_rotation_matrix_2d(
442 center: Point<f64>,
443 angle_degrees: f64,
444 scale: f64,
445 ) -> [[f64; 3]; 2] {
446 let angle_rad = angle_degrees.to_radians();
447 let alpha = scale * angle_rad.cos();
448 let beta = scale * angle_rad.sin();
449
450 [
451 [alpha, beta, (1.0 - alpha) * center.x - beta * center.y],
452 [-beta, alpha, beta * center.x + (1.0 - alpha) * center.y],
453 ]
454 }
455
456 #[must_use]
458 pub fn get_affine_transform(src: &[Point<f64>; 3], dst: &[Point<f64>; 3]) -> [[f64; 3]; 2] {
459 let solve = |pts_d: [f64; 3]| -> [f64; 3] {
463 let a11 = src[0].x;
465 let a12 = src[0].y;
466 let a13 = 1.0;
467 let a21 = src[1].x;
468 let a22 = src[1].y;
469 let a23 = 1.0;
470 let a31 = src[2].x;
471 let a32 = src[2].y;
472 let a33 = 1.0;
473
474 let det = a11 * (a22 * a33 - a23 * a32) - a12 * (a21 * a33 - a23 * a31)
475 + a13 * (a21 * a32 - a22 * a31);
476 if det.abs() < 1e-9 {
477 return [0.0, 0.0, 0.0];
478 }
479 let det_x = pts_d[0] * (a22 * a33 - a23 * a32)
480 - a12 * (pts_d[1] * a33 - a23 * pts_d[2])
481 + a13 * (pts_d[1] * a32 - a22 * pts_d[2]);
482 let det_y = a11 * (pts_d[1] * a33 - a23 * pts_d[2])
483 - pts_d[0] * (a21 * a33 - a23 * a31)
484 + a13 * (a21 * pts_d[2] - pts_d[1] * a31);
485 let det_z = a11 * (a22 * pts_d[2] - pts_d[1] * a32)
486 - a12 * (a21 * pts_d[2] - pts_d[1] * a31)
487 + pts_d[0] * (a21 * a32 - a22 * a31);
488
489 [det_x / det, det_y / det, det_z / det]
490 };
491
492 let row1 = solve([dst[0].x, dst[1].x, dst[2].x]);
493 let row2 = solve([dst[0].y, dst[1].y, dst[2].y]);
494 [row1, row2]
495 }
496
497 #[must_use]
499 pub fn get_perspective_transform(
500 src: &[Point<f64>; 4],
501 dst: &[Point<f64>; 4],
502 ) -> [[f64; 3]; 3] {
503 let mut m = [[0.0; 3]; 3];
506
507 let x0 = src[0].x;
508 let y0 = src[0].y;
509 let x1 = src[1].x;
510 let y1 = src[1].y;
511 let x2 = src[2].x;
512 let y2 = src[2].y;
513 let x3 = src[3].x;
514 let y3 = src[3].y;
515
516 let _u0 = dst[0].x;
517 let _v0 = dst[0].y;
518 let _u1 = dst[1].x;
519 let _v1 = dst[1].y;
520 let _u2 = dst[2].x;
521 let _v2 = dst[2].y;
522 let _u3 = dst[3].x;
523 let _v3 = dst[3].y;
524
525 let dx1 = x1 - x2;
526 let dx2 = x3 - x2;
527 let dy1 = y1 - y2;
528 let dy2 = y3 - y2;
529 let dx3 = x0 - x1 + x2 - x3;
530 let dy3 = y0 - y1 + y2 - y3;
531
532 let det = dx1 * dy2 - dx2 * dy1;
533 if det.abs() < 1e-9 {
534 m[0][0] = 1.0;
535 m[1][1] = 1.0;
536 m[2][2] = 1.0;
537 return m;
538 }
539
540 let g = (dx3 * dy2 - dx2 * dy3) / det;
541 let h = (dx1 * dy3 - dx3 * dy1) / det;
542
543 let a = x1 - x0 + g * x1;
544 let b = x3 - x0 + h * x3;
545 let c = x0;
546 let d = y1 - y0 + g * y1;
547 let e = y3 - y0 + h * y3;
548 let f = y0;
549
550 m[0][0] = a;
552 m[0][1] = b;
553 m[0][2] = c;
554 m[1][0] = d;
555 m[1][1] = e;
556 m[1][2] = f;
557 m[2][0] = g;
558 m[2][1] = h;
559 m[2][2] = 1.0;
560
561 m
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::test_helpers::{TestBackend, test_device};
569 use burn::tensor::TensorData;
570
571 #[test]
572 fn test_geometric_transforms() {
573 let device = test_device();
574 let flat_data = vec![0.5f32; 3 * 10 * 10];
575 let tensor =
576 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 10, 10]), &device);
577 let img = Image::new(tensor);
578
579 let resized = img.resize(20, 20).unwrap();
580 assert_eq!(resized.shape(), [3, 20, 20]);
581
582 let warped_aff = img
583 .warp_affine([[1.0, 0.0, 2.0], [0.0, 1.0, 3.0]], 10, 10)
584 .unwrap();
585 assert_eq!(warped_aff.shape(), [3, 10, 10]);
586
587 let warped_persp = img
588 .warp_perspective([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], 10, 10)
589 .unwrap();
590 assert_eq!(warped_persp.shape(), [3, 10, 10]);
591
592 let map_x = Tensor::<TestBackend, 2>::zeros([10, 10], &device);
593 let map_y = Tensor::<TestBackend, 2>::zeros([10, 10], &device);
594 let remapped = img.remap(&map_x, &map_y).unwrap();
595 assert_eq!(remapped.shape(), [3, 10, 10]);
596
597 let rotated = img.rotate(90).unwrap();
598 assert_eq!(rotated.shape(), [3, 10, 10]);
599 }
600
601 #[test]
602 fn test_undistort_identity() {
603 let device = test_device();
604 let flat_data: Vec<f32> = (0..(3 * 8 * 8)).map(|i| i as f32 / 192.0).collect();
605 let tensor =
606 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
607 let img = Image::new(tensor);
608
609 let cam = Tensor::<TestBackend, 2>::from_data(
611 TensorData::new(vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], [3, 3]),
612 &device,
613 );
614 let dist: [f32; 0] = [];
616
617 let undistorted = img.undistort(&cam, &dist).unwrap();
618 assert_eq!(undistorted.shape(), [3, 8, 8]);
619
620 let dist_zero = [0.0f32; 5];
622 let undistorted2 = img.undistort(&cam, &dist_zero).unwrap();
623 let orig_data: Vec<f32> = img.tensor.clone().into_data().iter::<f32>().collect();
624 let ud_data: Vec<f32> = undistorted2
625 .tensor
626 .clone()
627 .into_data()
628 .iter::<f32>()
629 .collect();
630 for (a, b) in orig_data.iter().zip(ud_data.iter()) {
631 assert!((a - b).abs() < 1e-6, "Mismatch: {a} vs {b}");
632 }
633 }
634
635 #[test]
636 fn test_undistort_with_k1() {
637 let device = test_device();
638 let flat_data: Vec<f32> = (0..(3 * 8 * 8)).map(|i| i as f32 / 192.0).collect();
639 let tensor =
640 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
641 let img = Image::new(tensor);
642
643 let cam = Tensor::<TestBackend, 2>::from_data(
644 TensorData::new(vec![1.0, 0.0, 3.5, 0.0, 1.0, 3.5, 0.0, 0.0, 1.0], [3, 3]),
645 &device,
646 );
647 let dist_coeffs = [0.1, 0.0, 0.0, 0.0, 0.0];
648
649 let undistorted = img.undistort(&cam, &dist_coeffs).unwrap();
650 assert_eq!(undistorted.shape(), [3, 8, 8]);
651
652 let orig_data: Vec<f32> = img.tensor.clone().into_data().iter::<f32>().collect();
654 let ud_data: Vec<f32> = undistorted
655 .tensor
656 .clone()
657 .into_data()
658 .iter::<f32>()
659 .collect();
660 let mut differs = false;
661 for (a, b) in orig_data.iter().zip(ud_data.iter()) {
662 if (a - b).abs() > 1e-6 {
663 differs = true;
664 break;
665 }
666 }
667 assert!(differs, "Undistortion with k1 should change pixel values");
668 }
669
670 #[test]
671 fn test_pyr_down_up_roundtrip() {
672 let device = test_device();
673 let flat_data = vec![0.5f32; 3 * 8 * 8];
674 let tensor =
675 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
676 let img = Image::new(tensor);
677
678 let down = img.pyr_down().unwrap();
679 assert_eq!(down.shape(), [3, 4, 4]);
681
682 let up = down.pyr_up().unwrap();
683 assert_eq!(up.shape(), [3, 7, 7]);
685 }
686
687 #[test]
688 fn test_pyr_down_preserves_energy() {
689 let device = test_device();
690 let mut flat_data = vec![0.0f32; 3 * 16 * 16];
692 for c in 0..3 {
693 for y in 4..12 {
694 for x in 4..12 {
695 flat_data[c * 256 + y * 16 + x] = 1.0;
696 }
697 }
698 }
699 let tensor =
700 Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 16, 16]), &device);
701 let img = Image::new(tensor);
702
703 let down = img.pyr_down().unwrap();
704 assert_eq!(down.shape(), [3, 8, 8]);
705
706 let down_data: Vec<f32> = down.tensor.clone().into_data().iter::<f32>().collect();
708 let max_val = down_data.iter().cloned().fold(0.0f32, f32::max);
709 assert!(
710 max_val > 0.5,
711 "pyr_down should preserve bright region, got max={max_val}"
712 );
713 }
714}