Skip to main content

glint_mask_tools/algorithms/
threshold.rs

1/// Threshold-based glint detection algorithm.
2///
3/// This module implements the primary glint detection algorithm using
4/// per-band thresholds to identify pixels with high reflectance values
5/// that likely represent specular reflections.
6use crate::core::algorithm::{validate_thresholds, GlintAlgorithm};
7use crate::error::{GlintError, Result};
8use ndarray::{Array2, Array3};
9use rayon::prelude::*;
10
11/// Threshold-based glint detection algorithm
12///
13/// This algorithm identifies glint pixels by applying threshold values
14/// to each band of the image. A pixel is marked as glint if any band
15/// exceeds its corresponding threshold value.
16///
17/// The algorithm uses a disjunctive approach: if any band at a pixel
18/// location exceeds its threshold, that pixel is marked as glint.
19#[derive(Debug, Clone)]
20pub struct ThresholdAlgorithm {
21    /// Threshold values for each band (must be in range [0.0, 1.0])
22    thresholds: Vec<f64>,
23}
24
25impl ThresholdAlgorithm {
26    /// Create a new threshold algorithm with the given threshold values
27    ///
28    /// # Arguments
29    ///
30    /// * `thresholds` - Threshold values for each band, must be in range [0.0, 1.0]
31    ///
32    /// # Example
33    ///
34    /// ```rust
35    /// use glint_mask_tools::algorithms::ThresholdAlgorithm;
36    ///
37    /// // RGB thresholds: stricter for blue (glint often appears in blue),
38    /// // less strict for red and green
39    /// let algorithm = ThresholdAlgorithm::new(vec![0.9, 0.8, 0.7])?;
40    /// # Ok::<(), Box<dyn std::error::Error>>(())
41    /// ```
42    pub fn new(thresholds: Vec<f64>) -> Result<Self> {
43        validate_thresholds(&thresholds)?;
44
45        if thresholds.is_empty() {
46            return Err(GlintError::validation(
47                "At least one threshold must be provided",
48            ));
49        }
50
51        Ok(Self { thresholds })
52    }
53
54    /// Create a threshold algorithm with uniform thresholds for all bands
55    ///
56    /// # Arguments
57    ///
58    /// * `threshold` - Single threshold value to use for all bands
59    /// * `band_count` - Number of bands to create thresholds for
60    pub fn uniform(threshold: f64, band_count: usize) -> Result<Self> {
61        if band_count == 0 {
62            return Err(GlintError::validation("Band count must be greater than 0"));
63        }
64
65        Self::new(vec![threshold; band_count])
66    }
67
68    /// Get the threshold values
69    pub fn thresholds(&self) -> &[f64] {
70        &self.thresholds
71    }
72
73    /// Get the number of bands this algorithm expects
74    pub fn band_count(&self) -> usize {
75        self.thresholds.len()
76    }
77
78    /// Set new threshold values
79    pub fn set_thresholds(&mut self, thresholds: Vec<f64>) -> Result<()> {
80        validate_thresholds(&thresholds)?;
81
82        if thresholds.is_empty() {
83            return Err(GlintError::validation(
84                "At least one threshold must be provided",
85            ));
86        }
87
88        self.thresholds = thresholds;
89        Ok(())
90    }
91
92    /// Update a specific band's threshold
93    pub fn set_band_threshold(&mut self, band_index: usize, threshold: f64) -> Result<()> {
94        if band_index >= self.thresholds.len() {
95            return Err(GlintError::validation(format!(
96                "Band index {} out of range (max: {})",
97                band_index,
98                self.thresholds.len() - 1
99            )));
100        }
101
102        crate::core::algorithm::validate_threshold(threshold)?;
103        self.thresholds[band_index] = threshold;
104        Ok(())
105    }
106}
107
108impl GlintAlgorithm for ThresholdAlgorithm {
109    fn detect_glint(&self, image: &Array3<f64>) -> Result<Array2<u8>> {
110        let (height, width, bands) = image.dim();
111
112        // Validate band count
113        if bands != self.thresholds.len() {
114            return Err(GlintError::BandCountMismatch {
115                expected: self.thresholds.len(),
116                actual: bands,
117            });
118        }
119
120        // Apply threshold detection with parallel row processing
121        let mask_data: Vec<u8> = (0..height * width)
122            .into_par_iter()
123            .map(|idx| {
124                let y = idx / width;
125                let x = idx % width;
126                let mut is_glint = false;
127
128                // Check each band against its threshold
129                for b in 0..bands {
130                    if image[[y, x, b]] > self.thresholds[b] {
131                        is_glint = true;
132                        break; // Disjunctive approach - any band exceeding threshold
133                    }
134                }
135
136                if is_glint {
137                    1
138                } else {
139                    0
140                }
141            })
142            .collect();
143
144        // Create output mask from the parallel-computed data
145        let mask = Array2::from_shape_vec((height, width), mask_data)
146            .map_err(|_| GlintError::processing("Failed to create output mask"))?;
147
148        Ok(mask)
149    }
150
151    fn name(&self) -> &'static str {
152        "Threshold"
153    }
154
155    fn description(&self) -> &'static str {
156        "Detects glint using per-band threshold values in a disjunctive manner"
157    }
158
159    fn validate_parameters(&self) -> Result<()> {
160        validate_thresholds(&self.thresholds)?;
161
162        if self.thresholds.is_empty() {
163            return Err(GlintError::validation("No thresholds specified"));
164        }
165
166        Ok(())
167    }
168
169    fn required_bands(&self) -> Option<usize> {
170        Some(self.thresholds.len())
171    }
172
173    fn supports_bands(&self, band_count: usize) -> bool {
174        band_count == self.thresholds.len()
175    }
176}
177
178/// Builder for creating threshold algorithms with validation
179#[derive(Debug, Default)]
180pub struct ThresholdAlgorithmBuilder {
181    thresholds: Vec<f64>,
182}
183
184impl ThresholdAlgorithmBuilder {
185    /// Create a new builder
186    pub fn new() -> Self {
187        Self::default()
188    }
189
190    /// Add a threshold for a band
191    pub fn add_threshold(mut self, threshold: f64) -> Result<Self> {
192        crate::core::algorithm::validate_threshold(threshold)?;
193        self.thresholds.push(threshold);
194        Ok(self)
195    }
196
197    /// Add multiple thresholds at once
198    pub fn add_thresholds(mut self, thresholds: &[f64]) -> Result<Self> {
199        validate_thresholds(thresholds)?;
200        self.thresholds.extend_from_slice(thresholds);
201        Ok(self)
202    }
203
204    /// Set thresholds from a slice, replacing any existing thresholds
205    pub fn with_thresholds(mut self, thresholds: &[f64]) -> Result<Self> {
206        validate_thresholds(thresholds)?;
207        self.thresholds = thresholds.to_vec();
208        Ok(self)
209    }
210
211    /// Create uniform thresholds for all bands
212    pub fn uniform(mut self, threshold: f64, band_count: usize) -> Result<Self> {
213        crate::core::algorithm::validate_threshold(threshold)?;
214
215        if band_count == 0 {
216            return Err(GlintError::validation("Band count must be greater than 0"));
217        }
218
219        self.thresholds = vec![threshold; band_count];
220        Ok(self)
221    }
222
223    /// Build the threshold algorithm
224    pub fn build(self) -> Result<ThresholdAlgorithm> {
225        ThresholdAlgorithm::new(self.thresholds)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn test_threshold_algorithm_creation() {
235        let algorithm = ThresholdAlgorithm::new(vec![0.8, 0.9, 0.7]).unwrap();
236        assert_eq!(algorithm.thresholds(), &[0.8, 0.9, 0.7]);
237        assert_eq!(algorithm.band_count(), 3);
238    }
239
240    #[test]
241    fn test_threshold_algorithm_validation() {
242        // Valid thresholds should work
243        assert!(ThresholdAlgorithm::new(vec![0.5, 0.8]).is_ok());
244
245        // Invalid thresholds should fail
246        assert!(ThresholdAlgorithm::new(vec![1.5]).is_err());
247        assert!(ThresholdAlgorithm::new(vec![-0.1]).is_err());
248        assert!(ThresholdAlgorithm::new(vec![]).is_err());
249    }
250
251    #[test]
252    fn test_uniform_threshold() {
253        let algorithm = ThresholdAlgorithm::uniform(0.8, 3).unwrap();
254        assert_eq!(algorithm.thresholds(), &[0.8, 0.8, 0.8]);
255
256        assert!(ThresholdAlgorithm::uniform(1.5, 3).is_err());
257        assert!(ThresholdAlgorithm::uniform(0.8, 0).is_err());
258    }
259
260    #[test]
261    fn test_glint_detection() {
262        let algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6, 0.7]).unwrap();
263
264        // Create a test image with known values
265        let mut image = ndarray::Array3::zeros((3, 3, 3));
266
267        // Set pixel (1,1) to exceed thresholds in all bands
268        image[[1, 1, 0]] = 0.8; // > 0.5
269        image[[1, 1, 1]] = 0.9; // > 0.6
270        image[[1, 1, 2]] = 0.9; // > 0.7
271
272        // Set pixel (0,0) to exceed threshold only in first band
273        image[[0, 0, 0]] = 0.6; // > 0.5
274        image[[0, 0, 1]] = 0.3; // < 0.6
275        image[[0, 0, 2]] = 0.3; // < 0.7
276
277        // Set pixel (2,2) to not exceed any thresholds
278        image[[2, 2, 0]] = 0.3; // < 0.5
279        image[[2, 2, 1]] = 0.3; // < 0.6
280        image[[2, 2, 2]] = 0.3; // < 0.7
281
282        let mask = algorithm.detect_glint(&image).unwrap();
283
284        // Check results
285        assert_eq!(mask[[1, 1]], 1); // Should be masked (exceeds all thresholds)
286        assert_eq!(mask[[0, 0]], 1); // Should be masked (exceeds first threshold)
287        assert_eq!(mask[[2, 2]], 0); // Should not be masked
288        assert_eq!(mask[[0, 1]], 0); // Default zero pixel should not be masked
289    }
290
291    #[test]
292    fn test_band_count_mismatch() {
293        let algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6]).unwrap();
294        let image = ndarray::Array3::zeros((2, 2, 3)); // 3 bands, but algorithm expects 2
295
296        let result = algorithm.detect_glint(&image);
297        assert!(result.is_err());
298        assert!(matches!(
299            result.unwrap_err(),
300            GlintError::BandCountMismatch { .. }
301        ));
302    }
303
304    #[test]
305    fn test_threshold_updates() {
306        let mut algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6, 0.7]).unwrap();
307
308        // Update single band threshold
309        algorithm.set_band_threshold(1, 0.8).unwrap();
310        assert_eq!(algorithm.thresholds()[1], 0.8);
311
312        // Invalid band index should fail
313        assert!(algorithm.set_band_threshold(5, 0.8).is_err());
314
315        // Invalid threshold value should fail
316        assert!(algorithm.set_band_threshold(1, 1.5).is_err());
317
318        // Update all thresholds
319        algorithm.set_thresholds(vec![0.9, 0.8, 0.7]).unwrap();
320        assert_eq!(algorithm.thresholds(), &[0.9, 0.8, 0.7]);
321    }
322
323    #[test]
324    fn test_builder_pattern() {
325        let algorithm = ThresholdAlgorithmBuilder::new()
326            .add_threshold(0.8)
327            .unwrap()
328            .add_threshold(0.9)
329            .unwrap()
330            .add_threshold(0.7)
331            .unwrap()
332            .build()
333            .unwrap();
334
335        assert_eq!(algorithm.thresholds(), &[0.8, 0.9, 0.7]);
336
337        let uniform_algorithm = ThresholdAlgorithmBuilder::new()
338            .uniform(0.85, 4)
339            .unwrap()
340            .build()
341            .unwrap();
342
343        assert_eq!(uniform_algorithm.thresholds(), &[0.85, 0.85, 0.85, 0.85]);
344    }
345
346    #[test]
347    fn test_algorithm_interface() {
348        let algorithm = ThresholdAlgorithm::new(vec![0.8]).unwrap();
349
350        assert_eq!(algorithm.name(), "Threshold");
351        assert!(algorithm.description().contains("threshold"));
352        assert_eq!(algorithm.required_bands(), Some(1));
353        assert!(algorithm.supports_bands(1));
354        assert!(!algorithm.supports_bands(2));
355        assert!(algorithm.validate_parameters().is_ok());
356    }
357}