Skip to main content

iris/threshold/
mod.rs

1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Tensor, TensorData, backend::Backend};
4
5/// Different types of thresholding operations.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub enum ThresholdType {
8    Binary,
9    BinaryInv,
10    Trunc,
11    ToZero,
12    ToZeroInv,
13}
14
15/// Strategy for adaptive threshold computation.
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum AdaptiveMethod {
18    /// Mean of the blockSize x blockSize neighborhood.
19    MeanC,
20    /// Gaussian-weighted sum of the blockSize x blockSize neighborhood.
21    GaussianC,
22}
23
24impl<B: Backend> Image<B> {
25    /// Applies a fixed-level threshold to each array element.
26    pub fn threshold(&self, thresh: f32, maxval: f32, thresh_type: ThresholdType) -> Result<Self> {
27        let dims = self.tensor.dims();
28        let c = dims[0];
29        let h = dims[1];
30        let w = dims[2];
31
32        let device = self.tensor.device();
33        let tensor_data = self.tensor.clone().into_data();
34        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
35        let mut out_vals = vec![0.0f32; c * h * w];
36
37        {
38            use rayon::prelude::*;
39            out_vals
40                .par_iter_mut()
41                .enumerate()
42                .for_each(|(i, out_val)| {
43                    let val = flat_vals[i];
44                    *out_val = match thresh_type {
45                        ThresholdType::Binary => {
46                            if val > thresh {
47                                maxval
48                            } else {
49                                0.0
50                            }
51                        }
52                        ThresholdType::BinaryInv => {
53                            if val > thresh {
54                                0.0
55                            } else {
56                                maxval
57                            }
58                        }
59                        ThresholdType::Trunc => {
60                            if val > thresh {
61                                thresh
62                            } else {
63                                val
64                            }
65                        }
66                        ThresholdType::ToZero => {
67                            if val > thresh {
68                                val
69                            } else {
70                                0.0
71                            }
72                        }
73                        ThresholdType::ToZeroInv => {
74                            if val > thresh {
75                                0.0
76                            } else {
77                                val
78                            }
79                        }
80                    };
81                });
82        }
83
84        let new_data = TensorData::new(out_vals, [c, h, w]);
85        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
86        Ok(Image::new(new_tensor))
87    }
88
89    /// Automatically computes a threshold using Otsu's method and applies binary thresholding.
90    pub fn threshold_otsu(&self, maxval: f32) -> Result<Self> {
91        let gray = self.grayscale()?;
92        let dims = gray.tensor.dims();
93        let h = dims[1];
94        let w = dims[2];
95
96        let tensor_data = gray.tensor.clone().into_data();
97        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
98
99        let mut hist = [0u32; 256];
100        for &val in &flat_vals {
101            let bin = (val.clamp(0.0, 1.0) * 255.0) as usize;
102            hist[bin] += 1;
103        }
104
105        let total = (h * w) as f32;
106        let mut sum = 0.0f32;
107        for (i, &count) in hist.iter().enumerate() {
108            sum += (i as f32) * (count as f32);
109        }
110
111        let mut sum_b = 0.0f32;
112        let mut w_b = 0.0f32;
113        let mut max_var = 0.0f32;
114        let mut threshold = 0;
115
116        for (t, &count) in hist.iter().enumerate() {
117            w_b += count as f32;
118            if w_b == 0.0 {
119                continue;
120            }
121            let w_f = total - w_b;
122            if w_f == 0.0 {
123                break;
124            }
125
126            sum_b += (t as f32) * (count as f32);
127            let m_b = sum_b / w_b;
128            let m_f = (sum - sum_b) / w_f;
129
130            let var_between = w_b * w_f * (m_b - m_f) * (m_b - m_f);
131            if var_between > max_var {
132                max_var = var_between;
133                threshold = t;
134            }
135        }
136
137        let thresh_float = (threshold as f32) / 255.0;
138        self.threshold(thresh_float, maxval, ThresholdType::Binary)
139    }
140
141    /// Automatically computes a threshold using the Triangle method and applies binary thresholding.
142    ///
143    /// The Triangle method draws a line from the histogram peak to the far end of the histogram,
144    /// then finds the threshold at the point of maximum distance from the line.
145    pub fn threshold_triangle(&self, maxval: f32) -> Result<Self> {
146        let gray = self.grayscale()?;
147        let dims = gray.tensor.dims();
148        let _h = dims[1];
149        let _w = dims[2];
150
151        let tensor_data = gray.tensor.clone().into_data();
152        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
153
154        let mut hist = [0u32; 256];
155        for &val in &flat_vals {
156            let bin = (val.clamp(0.0, 1.0) * 255.0) as usize;
157            hist[bin] += 1;
158        }
159
160        // Find the peak of the histogram
161        let mut peak = 0;
162        let mut max_count = 0u32;
163        for (i, &count) in hist.iter().enumerate() {
164            if count > max_count {
165                max_count = count;
166                peak = i;
167            }
168        }
169
170        // Find the far end (last non-zero bin)
171        let mut last = 255;
172        for i in (0..256).rev() {
173            if hist[i] > 0 {
174                last = i;
175                break;
176            }
177        }
178
179        if peak == last {
180            let thresh_float = (peak as f32) / 255.0;
181            return self.threshold(thresh_float, maxval, ThresholdType::Binary);
182        }
183
184        // Draw line from (peak, max_count) to (last, 0)
185        let dx = (last as f64) - (peak as f64);
186        let dy = 0.0 - (max_count as f64);
187        let line_len = (dx * dx + dy * dy).sqrt();
188
189        // For each bin between peak and last, compute distance from the line
190        let mut max_dist = 0.0f64;
191        let mut threshold = peak;
192
193        for i in peak..=last {
194            let px = i as f64;
195            let py = hist[i] as f64;
196            // Distance from point (px, py) to line from (peak, max_count) to (last, 0)
197            let dist = ((dy * px - dx * py + (last as f64) * (max_count as f64)
198                - (peak as f64) * 0.0)
199                / line_len)
200                .abs();
201            if dist > max_dist {
202                max_dist = dist;
203                threshold = i;
204            }
205        }
206
207        let thresh_float = (threshold as f32) / 255.0;
208        self.threshold(thresh_float, maxval, ThresholdType::Binary)
209    }
210
211    /// Applies adaptive thresholding where each pixel gets its own threshold
212    /// based on a neighborhood statistic.
213    pub fn adaptive_threshold(
214        &self,
215        maxval: f32,
216        method: AdaptiveMethod,
217        block_size: usize,
218        c: f32,
219    ) -> Result<Self> {
220        if block_size == 0 || block_size.is_multiple_of(2) {
221            return Err(IrisError::InvalidParameter(
222                "block_size must be a positive odd number".into(),
223            ));
224        }
225
226        let gray = self.grayscale()?;
227        let dims = gray.tensor.dims();
228        let h = dims[1];
229        let w = dims[2];
230
231        let tensor_data = gray.tensor.clone().into_data();
232        let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
233        let mut out_vals = vec![0.0f32; h * w];
234
235        let half = block_size / 2;
236
237        // Build integral image for fast neighborhood sum computation
238        let mut integral = vec![0.0f64; (h + 1) * (w + 1)];
239        for y in 0..h {
240            let mut row_sum = 0.0f64;
241            for x in 0..w {
242                row_sum += flat_vals[y * w + x] as f64;
243                integral[(y + 1) * (w + 1) + (x + 1)] = integral[y * (w + 1) + (x + 1)] + row_sum;
244            }
245        }
246
247        // Compute Gaussian weights for GaussianC method
248        let gaussian_kernel = if method == AdaptiveMethod::GaussianC {
249            let sigma = (block_size as f64) / 6.0;
250            let mut kernel = Vec::with_capacity(block_size * block_size);
251            for ky in 0..block_size {
252                for kx in 0..block_size {
253                    let dy = (ky as f64) - (half as f64);
254                    let dx = (kx as f64) - (half as f64);
255                    let weight = (-(dx * dx + dy * dy) / (2.0 * sigma * sigma)).exp();
256                    kernel.push(weight);
257                }
258            }
259            let sum: f64 = kernel.iter().sum();
260            for k in &mut kernel {
261                *k /= sum;
262            }
263            Some(kernel)
264        } else {
265            None
266        };
267
268        let _total_pixels = block_size * block_size;
269
270        for y in 0..h {
271            for x in 0..w {
272                let y1 = y.saturating_sub(half).min(h - 1);
273                let y2 = (y + half).min(h - 1);
274                let x1 = x.saturating_sub(half).min(w - 1);
275                let x2 = (x + half).min(w - 1);
276
277                let mean = if let Some(ref kernel) = gaussian_kernel {
278                    // Gaussian-weighted sum
279                    let mut weighted_sum = 0.0f64;
280                    let mut ki = 0;
281                    for ky in y1..=y2 {
282                        for kx in x1..=x2 {
283                            weighted_sum += flat_vals[ky * w + kx] as f64 * kernel[ki];
284                            ki += 1;
285                        }
286                    }
287                    weighted_sum
288                } else {
289                    // Mean: use integral image for O(1) neighborhood sum
290                    let area = integral[(y2 + 1) * (w + 1) + (x2 + 1)]
291                        - integral[y1 * (w + 1) + (x2 + 1)]
292                        - integral[(y2 + 1) * (w + 1) + x1]
293                        + integral[y1 * (w + 1) + x1];
294                    let count = ((y2 - y1 + 1) * (x2 - x1 + 1)) as f64;
295                    area / count
296                };
297
298                let pixel = flat_vals[y * w + x];
299                if pixel > (mean as f32) - c {
300                    out_vals[y * w + x] = maxval;
301                } else {
302                    out_vals[y * w + x] = 0.0;
303                }
304            }
305        }
306
307        let new_data = TensorData::new(out_vals, [1, h, w]);
308        let new_tensor = Tensor::<B, 3>::from_data(new_data, &gray.tensor.device());
309        Ok(Image::new(new_tensor))
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::test_helpers::{TestBackend, test_device};
317
318    #[test]
319    fn test_threshold() {
320        let device = test_device();
321        let flat_data = vec![0.5f32; 3 * 8 * 8];
322        let tensor_data = TensorData::new(flat_data, [3, 8, 8]);
323        let img = Image::new(Tensor::<TestBackend, 3>::from_data(tensor_data, &device));
324
325        let thresh = img.threshold(0.4, 1.0, ThresholdType::Binary).unwrap();
326        assert_eq!(thresh.shape(), [3, 8, 8]);
327
328        let otsu = img.threshold_otsu(1.0).unwrap();
329        assert_eq!(otsu.shape(), [3, 8, 8]);
330    }
331
332    #[test]
333    fn test_triangle_threshold() {
334        let device = test_device();
335        let mut flat_data = vec![0.0f32; 16 * 16];
336        // Create bimodal distribution
337        for y in 0..16 {
338            for x in 0..16 {
339                if x < 8 {
340                    flat_data[y * 16 + x] = 0.2;
341                } else {
342                    flat_data[y * 16 + x] = 0.8;
343                }
344            }
345        }
346        let tensor =
347            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 16, 16]), &device);
348        let img = Image::new(tensor);
349        let result = img.threshold_triangle(1.0).unwrap();
350        assert_eq!(result.shape(), [1, 16, 16]);
351    }
352
353    #[test]
354    fn test_adaptive_threshold() {
355        let device = test_device();
356        let mut flat_data = vec![0.0f32; 16 * 16];
357        for y in 0..16 {
358            for x in 0..16 {
359                flat_data[y * 16 + x] = (x as f32) / 16.0;
360            }
361        }
362        let tensor =
363            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [1, 16, 16]), &device);
364        let img = Image::new(tensor);
365
366        let result = img
367            .adaptive_threshold(1.0, AdaptiveMethod::MeanC, 3, 0.05)
368            .unwrap();
369        assert_eq!(result.shape(), [1, 16, 16]);
370
371        let result_gauss = img
372            .adaptive_threshold(1.0, AdaptiveMethod::GaussianC, 5, 0.05)
373            .unwrap();
374        assert_eq!(result_gauss.shape(), [1, 16, 16]);
375    }
376}