Skip to main content

iris/filters/
mod.rs

1pub mod bilateral;
2
3use crate::error::{IrisError, Result};
4use crate::image::Image;
5use burn::tensor::{Tensor, TensorData, backend::Backend};
6
7/// Square root of 2, used for diagonal distance in distance transform.
8const SQRT_2: f32 = std::f32::consts::SQRT_2;
9
10impl<B: Backend> Image<B> {
11    /// Applies a box filter to blur the image with the specified kernel size.
12    pub fn box_blur(self, kernel_size: usize) -> Result<Self> {
13        if kernel_size.is_multiple_of(2) {
14            return Err(IrisError::InvalidParameter(
15                "Kernel size must be odd".into(),
16            ));
17        }
18
19        let dims = self.tensor.dims();
20        let c = dims[0];
21        let h = dims[1];
22        let w = dims[2];
23
24        let device = self.tensor.device();
25        let tensor_data = self.tensor.into_data();
26        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
27        let mut out_vals = vec![0.0f32; c * h * w];
28
29        let rad = (kernel_size / 2) as isize;
30
31        {
32            use rayon::prelude::*;
33            out_vals
34                .par_chunks_exact_mut(w)
35                .enumerate()
36                .for_each(|(idx, row)| {
37                    let ch = idx / h;
38                    let y = idx % h;
39                    for x in 0..w {
40                        let mut sum = 0.0f32;
41                        let mut count = 0.0f32;
42
43                        for ky in -rad..=rad {
44                            let py = y as isize + ky;
45                            if py >= 0 && py < h as isize {
46                                for kx in -rad..=rad {
47                                    let px = x as isize + kx;
48                                    if px >= 0 && px < w as isize {
49                                        sum += flat_vals
50                                            [ch * h * w + (py as usize) * w + (px as usize)];
51                                        count += 1.0;
52                                    }
53                                }
54                            }
55                        }
56                        row[x] = sum / count;
57                    }
58                });
59        }
60
61        let new_data = TensorData::new(out_vals, [c, h, w]);
62        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
63        Ok(Image::new(new_tensor))
64    }
65
66    /// Applies a Gaussian blur filter to the image.
67    pub fn gaussian_blur(self, kernel_size: usize, sigma: f64) -> Result<Self> {
68        if kernel_size.is_multiple_of(2) {
69            return Err(IrisError::InvalidParameter(
70                "Kernel size must be odd".into(),
71            ));
72        }
73
74        let dims = self.tensor.dims();
75        let c = dims[0];
76        let h = dims[1];
77        let w = dims[2];
78
79        let device = self.tensor.device();
80        let tensor_data = self.tensor.into_data();
81        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
82        let mut out_vals = vec![0.0f32; c * h * w];
83
84        let rad = (kernel_size / 2) as isize;
85
86        // Generate Gaussian kernel
87        let mut kernel = vec![vec![0.0f64; kernel_size]; kernel_size];
88        let mut sum = 0.0f64;
89
90        let s2 = 2.0 * sigma * sigma;
91
92        for ky in -rad..=rad {
93            for kx in -rad..=rad {
94                let r = (kx * kx + ky * ky) as f64;
95                let val = (-r / s2).exp();
96                kernel[(ky + rad) as usize][(kx + rad) as usize] = val;
97                sum += val;
98            }
99        }
100
101        // Normalize kernel
102        for row in &mut kernel {
103            for val in row {
104                *val /= sum;
105            }
106        }
107
108        {
109            use rayon::prelude::*;
110            out_vals
111                .par_chunks_exact_mut(w)
112                .enumerate()
113                .for_each(|(idx, row)| {
114                    let ch = idx / h;
115                    let y = idx % h;
116                    for x in 0..w {
117                        let mut blur_sum = 0.0f64;
118                        for ky in -rad..=rad {
119                            let py = y as isize + ky;
120                            let py_clamped = py.clamp(0, h as isize - 1) as usize;
121                            for kx in -rad..=rad {
122                                let px = x as isize + kx;
123                                let px_clamped = px.clamp(0, w as isize - 1) as usize;
124                                let weight = kernel[(ky + rad) as usize][(kx + rad) as usize];
125                                blur_sum +=
126                                    f64::from(flat_vals[ch * h * w + py_clamped * w + px_clamped])
127                                        * weight;
128                            }
129                        }
130                        row[x] = blur_sum as f32;
131                    }
132                });
133        }
134
135        let new_data = TensorData::new(out_vals, [c, h, w]);
136        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
137        Ok(Image::new(new_tensor))
138    }
139
140    /// Applies a median filter to reduce salt-and-pepper noise.
141    pub fn median_blur(self, kernel_size: usize) -> Result<Self> {
142        if kernel_size.is_multiple_of(2) {
143            return Err(IrisError::InvalidParameter(
144                "Kernel size must be odd".into(),
145            ));
146        }
147
148        let dims = self.tensor.dims();
149        let c = dims[0];
150        let h = dims[1];
151        let w = dims[2];
152
153        let device = self.tensor.device();
154        let tensor_data = self.tensor.into_data();
155        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
156        let mut out_vals = vec![0.0f32; c * h * w];
157
158        let rad = (kernel_size / 2) as isize;
159
160        {
161            use rayon::prelude::*;
162            out_vals
163                .par_chunks_exact_mut(w)
164                .enumerate()
165                .for_each(|(idx, row)| {
166                    let ch = idx / h;
167                    let y = idx % h;
168                    for x in 0..w {
169                        let mut neighbors = Vec::with_capacity(kernel_size * kernel_size);
170
171                        for ky in -rad..=rad {
172                            let py = y as isize + ky;
173                            if py >= 0 && py < h as isize {
174                                for kx in -rad..=rad {
175                                    let px = x as isize + kx;
176                                    if px >= 0 && px < w as isize {
177                                        neighbors.push(
178                                            flat_vals
179                                                [ch * h * w + (py as usize) * w + (px as usize)],
180                                        );
181                                    }
182                                }
183                            }
184                        }
185                        neighbors
186                            .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
187                        let median = neighbors[neighbors.len() / 2];
188                        row[x] = median;
189                    }
190                });
191        }
192
193        let new_data = TensorData::new(out_vals, [c, h, w]);
194        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
195        Ok(Image::new(new_tensor))
196    }
197
198    /// Computes the distance transform of a binary/grayscale image.
199    /// Each pixel's value becomes its Euclidean distance to the nearest zero pixel.
200    /// Uses the Meijster algorithm (two-pass) for O(n) exact Euclidean distance transform.
201    pub fn distance_transform(&self) -> Result<Self> {
202        let gray = self.grayscale()?;
203        let dims = gray.tensor.dims();
204        let h = dims[1];
205        let w = dims[2];
206
207        let tensor_data = gray.tensor.clone().into_data();
208        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
209
210        // Binarize: nonzero pixels are foreground
211        let mut binary = vec![false; h * w];
212        for (i, &v) in flat_vals.iter().enumerate() {
213            binary[i] = v > 0.5;
214        }
215
216        let inf = (h * w) as f32;
217        let mut dt = vec![inf; h * w];
218
219        // First pass: forward scan
220        for y in 0..h {
221            for x in 0..w {
222                let idx = y * w + x;
223                if binary[idx] {
224                    dt[idx] = 0.0;
225                } else {
226                    if x > 0 {
227                        let prev = dt[idx - 1] + 1.0;
228                        if prev < dt[idx] {
229                            dt[idx] = prev;
230                        }
231                    }
232                    if y > 0 {
233                        let prev = dt[(y - 1) * w + x] + 1.0;
234                        if prev < dt[idx] {
235                            dt[idx] = prev;
236                        }
237                    }
238                    if x > 0 && y > 0 {
239                        let prev = dt[(y - 1) * w + (x - 1)] + SQRT_2;
240                        if prev < dt[idx] {
241                            dt[idx] = prev;
242                        }
243                    }
244                    if x < w - 1 && y > 0 {
245                        let prev = dt[(y - 1) * w + (x + 1)] + SQRT_2;
246                        if prev < dt[idx] {
247                            dt[idx] = prev;
248                        }
249                    }
250                }
251            }
252        }
253
254        // Second pass: backward scan
255        for y in (0..h).rev() {
256            for x in (0..w).rev() {
257                let idx = y * w + x;
258                if x < w - 1 {
259                    let next = dt[idx + 1] + 1.0;
260                    if next < dt[idx] {
261                        dt[idx] = next;
262                    }
263                }
264                if y < h - 1 {
265                    let next = dt[(y + 1) * w + x] + 1.0;
266                    if next < dt[idx] {
267                        dt[idx] = next;
268                    }
269                }
270                if x < w - 1 && y < h - 1 {
271                    let next = dt[(y + 1) * w + (x + 1)] + SQRT_2;
272                    if next < dt[idx] {
273                        dt[idx] = next;
274                    }
275                }
276                if x > 0 && y < h - 1 {
277                    let next = dt[(y + 1) * w + (x - 1)] + SQRT_2;
278                    if next < dt[idx] {
279                        dt[idx] = next;
280                    }
281                }
282            }
283        }
284
285        // Normalize to [0, 1] range
286        let max_dt = dt.iter().cloned().fold(0.0f32, f32::max);
287        if max_dt > 0.0 {
288            for v in &mut dt {
289                *v /= max_dt;
290            }
291        }
292
293        let device = gray.tensor.device();
294        let data = TensorData::new(dt, [1, h, w]);
295        let tensor = Tensor::<B, 3>::from_data(data, &device);
296        Ok(Image::new(tensor))
297    }
298
299    /// Applies an arbitrary 2D convolution kernel to the image.
300    /// `kernel` is a 2D slice of shape `[kh][kw]`. Output is computed via valid convolution.
301    /// `anchor` specifies the center of the kernel; if `None`, the center is `(kw/2, kh/2)`.
302    /// `delta` is added to each result pixel, and `border` controls out-of-bounds handling.
303    pub fn filter2d(
304        &self,
305        kernel: &[&[f32]],
306        anchor: Option<(isize, isize)>,
307        delta: f32,
308    ) -> Result<Self> {
309        let kh = kernel.len();
310        if kh == 0 || kernel[0].is_empty() {
311            return Err(IrisError::InvalidParameter(
312                "Kernel must be non-empty".into(),
313            ));
314        }
315        let kw = kernel[0].len();
316
317        let dims = self.tensor.dims();
318        let c = dims[0];
319        let h = dims[1];
320        let w = dims[2];
321
322        let (ax, ay) = anchor.unwrap_or((kw as isize / 2, kh as isize / 2));
323
324        let device = self.tensor.device();
325        let tensor_data = self.tensor.clone().into_data();
326        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
327        let mut out_vals = vec![0.0f32; c * h * w];
328
329        for ch in 0..c {
330            for y in 0..h {
331                for x in 0..w {
332                    let mut sum = 0.0f64;
333                    for ky in 0..kh {
334                        for kx in 0..kw {
335                            let sy = y as isize + ky as isize - ay;
336                            let sx = x as isize + kx as isize - ax;
337                            if sy >= 0 && sy < h as isize && sx >= 0 && sx < w as isize {
338                                sum += flat_vals[ch * h * w + sy as usize * w + sx as usize] as f64
339                                    * kernel[ky][kx] as f64;
340                            }
341                        }
342                    }
343                    out_vals[ch * h * w + y * w + x] = sum as f32 + delta;
344                }
345            }
346        }
347
348        let new_data = TensorData::new(out_vals, [c, h, w]);
349        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
350        Ok(Image::new(new_tensor))
351    }
352
353    /// Computes weighted sum of two images: `dst = src1 * alpha + src2 * beta + gamma`.
354    pub fn add_weighted(&self, other: &Self, alpha: f32, beta: f32, gamma: f32) -> Result<Self> {
355        if self.shape() != other.shape() {
356            return Err(IrisError::DimensionMismatch {
357                expected: self.shape().to_vec(),
358                actual: other.shape().to_vec(),
359            });
360        }
361        let result = self
362            .tensor
363            .clone()
364            .mul_scalar(alpha)
365            .add(other.tensor.clone().mul_scalar(beta))
366            .add_scalar(gamma);
367        Ok(Image::new(result))
368    }
369
370    /// Computes `dst = src * scale + shift`, then optionally converts to abs.
371    pub fn convert_scale_abs(&self, scale: f32, shift: f32) -> Result<Self> {
372        let dims = self.tensor.dims();
373        let c = dims[0];
374        let h = dims[1];
375        let w = dims[2];
376
377        let device = self.tensor.device();
378        let tensor_data = self.tensor.clone().into_data();
379        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
380        let mut out_vals = vec![0.0f32; c * h * w];
381
382        for i in 0..(c * h * w) {
383            let val = (flat_vals[i] * scale + shift).abs().min(1.0);
384            out_vals[i] = val;
385        }
386
387        let new_data = TensorData::new(out_vals, [c, h, w]);
388        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
389        Ok(Image::new(new_tensor))
390    }
391
392    /// Copies source image to destination within a masked region.
393    /// Where `mask` is nonzero, `dst = src`; otherwise `dst` is unchanged.
394    pub fn copy_to(&self, dst: &mut Self, mask: Option<&Self>) -> Result<()> {
395        if self.shape() != dst.shape() {
396            return Err(IrisError::DimensionMismatch {
397                expected: self.shape().to_vec(),
398                actual: dst.shape().to_vec(),
399            });
400        }
401
402        let dims = self.tensor.dims();
403        let c = dims[0];
404        let h = dims[1];
405        let w = dims[2];
406
407        let src_data = self.tensor.clone().into_data();
408        let src_vals: Vec<f32> = src_data.iter::<f32>().collect();
409        let dst_data = dst.tensor.clone().into_data();
410        let mut dst_vals: Vec<f32> = dst_data.iter::<f32>().collect();
411
412        let mask_vals: Option<Vec<f32>> = mask.map(|m| {
413            let d = m.tensor.clone().into_data();
414            d.iter::<f32>().collect()
415        });
416
417        let pixels = h * w;
418        for i in 0..pixels {
419            let dominated = match &mask_vals {
420                Some(mv) => mv[i] > 0.0,
421                None => true,
422            };
423            if dominated {
424                for ch in 0..c {
425                    dst_vals[ch * pixels + i] = src_vals[ch * pixels + i];
426                }
427            }
428        }
429
430        *dst = Image::new(Tensor::<B, 3>::from_data(
431            TensorData::new(dst_vals, [c, h, w]),
432            &dst.tensor.device(),
433        ));
434        Ok(())
435    }
436
437    /// Computes the Laplacian of Gaussian (LoG) filter response.
438    /// Applies Gaussian smoothing then Laplacian operator to detect edges/blobs.
439    /// `sigma` controls the scale of the Gaussian kernel.
440    pub fn laplacian_of_gaussian(&self, sigma: f32) -> Result<Self> {
441        let gray = self.grayscale()?;
442        let dims = gray.tensor.dims();
443        let h = dims[1];
444        let w = dims[2];
445
446        // Build LoG kernel: size = 6*sigma, rounded to odd
447        let k_size = ((6.0 * sigma as f64) as usize) | 1;
448        let half = k_size / 2;
449
450        // LoG kernel: -(1/(pi*sigma^4)) * (1 - (x^2+y^2)/(2*sigma^2)) * exp(-(x^2+y^2)/(2*sigma^2))
451        let sigma2 = (sigma as f64) * (sigma as f64);
452        let sigma4 = sigma2 * sigma2;
453        let mut kernel = Vec::with_capacity(k_size * k_size);
454        let mut kernel_sum = 0.0f64;
455
456        for ky in 0..k_size {
457            for kx in 0..k_size {
458                let dx = (kx as f64) - (half as f64);
459                let dy = (ky as f64) - (half as f64);
460                let r2 = dx * dx + dy * dy;
461                let val = -(1.0 / (std::f64::consts::PI * sigma4))
462                    * (1.0 - r2 / (2.0 * sigma2))
463                    * (-r2 / (2.0 * sigma2)).exp();
464                kernel.push(val);
465                kernel_sum += val;
466            }
467        }
468
469        // Zero-sum normalize: subtract mean so kernel sums to zero
470        let mean = kernel_sum / (k_size * k_size) as f64;
471        for k in &mut kernel {
472            *k -= mean;
473        }
474
475        // Apply convolution
476        let tensor_data = gray.tensor.clone().into_data();
477        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
478        let mut out_vals = vec![0.0f32; h * w];
479
480        for y in 0..h {
481            for x in 0..w {
482                let mut sum = 0.0f64;
483                for ky in 0..k_size {
484                    for kx in 0..k_size {
485                        let sy = (y + ky).min(h - 1);
486                        let sx = (x + kx).min(w - 1);
487                        sum += flat_vals[sy * w + sx] as f64 * kernel[ky * k_size + kx];
488                    }
489                }
490                out_vals[y * w + x] = sum as f32;
491            }
492        }
493
494        let device = gray.tensor.device();
495        let data = TensorData::new(out_vals, [1, h, w]);
496        let tensor = Tensor::<B, 3>::from_data(data, &device);
497        Ok(Image::new(tensor))
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use crate::test_helpers::{TestBackend, test_device};
505
506    #[test]
507    fn test_filters_blur() {
508        let device = test_device();
509        let flat_data = vec![0.5f32; 3 * 8 * 8];
510        let tensor_data = TensorData::new(flat_data, [3, 8, 8]);
511        let img = Image::new(Tensor::<TestBackend, 3>::from_data(tensor_data, &device));
512
513        let boxed = img.clone().box_blur(3).unwrap();
514        assert_eq!(boxed.shape(), [3, 8, 8]);
515
516        let gauss = img.clone().gaussian_blur(3, 1.0).unwrap();
517        assert_eq!(gauss.shape(), [3, 8, 8]);
518
519        let median = img.clone().median_blur(3).unwrap();
520        assert_eq!(median.shape(), [3, 8, 8]);
521    }
522
523    #[test]
524    fn test_distance_transform() {
525        let device = test_device();
526        let flat_data = vec![
527            0.0f32, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
528        ];
529        let tensor =
530            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 4, 4]), &device);
531        let img = Image::new(tensor);
532        let dt = img.distance_transform().unwrap();
533        assert_eq!(dt.shape(), [1, 4, 4]);
534    }
535
536    #[test]
537    fn test_laplacian_of_gaussian() {
538        let device = test_device();
539        let flat_data = vec![0.5f32; 3 * 16 * 16];
540        let tensor =
541            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 16, 16]), &device);
542        let img = Image::new(tensor);
543        let log = img.laplacian_of_gaussian(1.0).unwrap();
544        assert_eq!(log.shape(), [1, 16, 16]);
545    }
546
547    #[test]
548    fn test_filter2d() {
549        let device = test_device();
550        let data = TensorData::new(vec![0.5f32; 3 * 8 * 8], [3, 8, 8]);
551        let img = Image::new(Tensor::<TestBackend, 3>::from_data(data, &device));
552
553        // Simple averaging kernel
554        let kernel: Vec<&[f32]> = vec![
555            &[1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0],
556            &[1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0],
557            &[1.0 / 9.0, 1.0 / 9.0, 1.0 / 9.0],
558        ];
559        let result = img.filter2d(&kernel, None, 0.0).unwrap();
560        assert_eq!(result.shape(), [3, 8, 8]);
561    }
562
563    #[test]
564    fn test_add_weighted() {
565        let device = test_device();
566        let data1 = TensorData::new(vec![0.5f32; 3 * 4 * 4], [3, 4, 4]);
567        let data2 = TensorData::new(vec![0.3f32; 3 * 4 * 4], [3, 4, 4]);
568        let img1 = Image::new(Tensor::<TestBackend, 3>::from_data(data1, &device));
569        let img2 = Image::new(Tensor::<TestBackend, 3>::from_data(data2, &device));
570
571        let result = img1.add_weighted(&img2, 0.6, 0.4, 0.0).unwrap();
572        assert_eq!(result.shape(), [3, 4, 4]);
573    }
574
575    #[test]
576    fn test_convert_scale_abs() {
577        let device = test_device();
578        let data = TensorData::new(vec![-0.5f32, -0.1, 0.0, 0.3, 0.8], [1, 1, 5]);
579        let img = Image::new(Tensor::<TestBackend, 3>::from_data(data, &device));
580        let result = img.convert_scale_abs(1.0, 0.0).unwrap();
581        assert_eq!(result.shape(), [1, 1, 5]);
582        let vals: Vec<f32> = result.tensor.into_data().iter::<f32>().collect();
583        assert!((vals[0] - 0.5).abs() < 1e-5);
584        assert!((vals[1] - 0.1).abs() < 1e-5);
585    }
586
587    #[test]
588    fn test_copy_to_with_mask() {
589        let device = test_device();
590        let data = TensorData::new(vec![1.0f32; 3 * 4 * 4], [3, 4, 4]);
591        let mask_data = TensorData::new(
592            vec![
593                1.0f32, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0,
594            ],
595            [1, 4, 4],
596        );
597        let src = Image::new(Tensor::<TestBackend, 3>::from_data(data, &device));
598        let mut dst = Image::new(Tensor::<TestBackend, 3>::from_data(
599            TensorData::new(vec![0.0f32; 3 * 4 * 4], [3, 4, 4]),
600            &device,
601        ));
602        let mask = Image::new(Tensor::<TestBackend, 3>::from_data(mask_data, &device));
603        src.copy_to(&mut dst, Some(&mask)).unwrap();
604        assert_eq!(dst.shape(), [3, 4, 4]);
605    }
606}