glint-mask-tools 0.1.1

Rust implementation of glint mask generation tools for UAV and aerial imagery
Documentation
/// Threshold-based glint detection algorithm.
///
/// This module implements the primary glint detection algorithm using
/// per-band thresholds to identify pixels with high reflectance values
/// that likely represent specular reflections.
use crate::core::algorithm::{validate_thresholds, GlintAlgorithm};
use crate::error::{GlintError, Result};
use ndarray::{Array2, Array3};
use rayon::prelude::*;

/// Threshold-based glint detection algorithm
///
/// This algorithm identifies glint pixels by applying threshold values
/// to each band of the image. A pixel is marked as glint if any band
/// exceeds its corresponding threshold value.
///
/// The algorithm uses a disjunctive approach: if any band at a pixel
/// location exceeds its threshold, that pixel is marked as glint.
#[derive(Debug, Clone)]
pub struct ThresholdAlgorithm {
    /// Threshold values for each band (must be in range [0.0, 1.0])
    thresholds: Vec<f64>,
}

impl ThresholdAlgorithm {
    /// Create a new threshold algorithm with the given threshold values
    ///
    /// # Arguments
    ///
    /// * `thresholds` - Threshold values for each band, must be in range [0.0, 1.0]
    ///
    /// # Example
    ///
    /// ```rust
    /// use glint_mask_tools::algorithms::ThresholdAlgorithm;
    ///
    /// // RGB thresholds: stricter for blue (glint often appears in blue),
    /// // less strict for red and green
    /// let algorithm = ThresholdAlgorithm::new(vec![0.9, 0.8, 0.7])?;
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn new(thresholds: Vec<f64>) -> Result<Self> {
        validate_thresholds(&thresholds)?;

        if thresholds.is_empty() {
            return Err(GlintError::validation(
                "At least one threshold must be provided",
            ));
        }

        Ok(Self { thresholds })
    }

    /// Create a threshold algorithm with uniform thresholds for all bands
    ///
    /// # Arguments
    ///
    /// * `threshold` - Single threshold value to use for all bands
    /// * `band_count` - Number of bands to create thresholds for
    pub fn uniform(threshold: f64, band_count: usize) -> Result<Self> {
        if band_count == 0 {
            return Err(GlintError::validation("Band count must be greater than 0"));
        }

        Self::new(vec![threshold; band_count])
    }

    /// Get the threshold values
    pub fn thresholds(&self) -> &[f64] {
        &self.thresholds
    }

    /// Get the number of bands this algorithm expects
    pub fn band_count(&self) -> usize {
        self.thresholds.len()
    }

    /// Set new threshold values
    pub fn set_thresholds(&mut self, thresholds: Vec<f64>) -> Result<()> {
        validate_thresholds(&thresholds)?;

        if thresholds.is_empty() {
            return Err(GlintError::validation(
                "At least one threshold must be provided",
            ));
        }

        self.thresholds = thresholds;
        Ok(())
    }

    /// Update a specific band's threshold
    pub fn set_band_threshold(&mut self, band_index: usize, threshold: f64) -> Result<()> {
        if band_index >= self.thresholds.len() {
            return Err(GlintError::validation(format!(
                "Band index {} out of range (max: {})",
                band_index,
                self.thresholds.len() - 1
            )));
        }

        crate::core::algorithm::validate_threshold(threshold)?;
        self.thresholds[band_index] = threshold;
        Ok(())
    }
}

impl GlintAlgorithm for ThresholdAlgorithm {
    fn detect_glint(&self, image: &Array3<f64>) -> Result<Array2<u8>> {
        let (height, width, bands) = image.dim();

        // Validate band count
        if bands != self.thresholds.len() {
            return Err(GlintError::BandCountMismatch {
                expected: self.thresholds.len(),
                actual: bands,
            });
        }

        // Apply threshold detection with parallel row processing
        let mask_data: Vec<u8> = (0..height * width)
            .into_par_iter()
            .map(|idx| {
                let y = idx / width;
                let x = idx % width;
                let mut is_glint = false;

                // Check each band against its threshold
                for b in 0..bands {
                    if image[[y, x, b]] > self.thresholds[b] {
                        is_glint = true;
                        break; // Disjunctive approach - any band exceeding threshold
                    }
                }

                if is_glint {
                    1
                } else {
                    0
                }
            })
            .collect();

        // Create output mask from the parallel-computed data
        let mask = Array2::from_shape_vec((height, width), mask_data)
            .map_err(|_| GlintError::processing("Failed to create output mask"))?;

        Ok(mask)
    }

    fn name(&self) -> &'static str {
        "Threshold"
    }

    fn description(&self) -> &'static str {
        "Detects glint using per-band threshold values in a disjunctive manner"
    }

    fn validate_parameters(&self) -> Result<()> {
        validate_thresholds(&self.thresholds)?;

        if self.thresholds.is_empty() {
            return Err(GlintError::validation("No thresholds specified"));
        }

        Ok(())
    }

    fn required_bands(&self) -> Option<usize> {
        Some(self.thresholds.len())
    }

    fn supports_bands(&self, band_count: usize) -> bool {
        band_count == self.thresholds.len()
    }
}

/// Builder for creating threshold algorithms with validation
#[derive(Debug, Default)]
pub struct ThresholdAlgorithmBuilder {
    thresholds: Vec<f64>,
}

impl ThresholdAlgorithmBuilder {
    /// Create a new builder
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a threshold for a band
    pub fn add_threshold(mut self, threshold: f64) -> Result<Self> {
        crate::core::algorithm::validate_threshold(threshold)?;
        self.thresholds.push(threshold);
        Ok(self)
    }

    /// Add multiple thresholds at once
    pub fn add_thresholds(mut self, thresholds: &[f64]) -> Result<Self> {
        validate_thresholds(thresholds)?;
        self.thresholds.extend_from_slice(thresholds);
        Ok(self)
    }

    /// Set thresholds from a slice, replacing any existing thresholds
    pub fn with_thresholds(mut self, thresholds: &[f64]) -> Result<Self> {
        validate_thresholds(thresholds)?;
        self.thresholds = thresholds.to_vec();
        Ok(self)
    }

    /// Create uniform thresholds for all bands
    pub fn uniform(mut self, threshold: f64, band_count: usize) -> Result<Self> {
        crate::core::algorithm::validate_threshold(threshold)?;

        if band_count == 0 {
            return Err(GlintError::validation("Band count must be greater than 0"));
        }

        self.thresholds = vec![threshold; band_count];
        Ok(self)
    }

    /// Build the threshold algorithm
    pub fn build(self) -> Result<ThresholdAlgorithm> {
        ThresholdAlgorithm::new(self.thresholds)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_threshold_algorithm_creation() {
        let algorithm = ThresholdAlgorithm::new(vec![0.8, 0.9, 0.7]).unwrap();
        assert_eq!(algorithm.thresholds(), &[0.8, 0.9, 0.7]);
        assert_eq!(algorithm.band_count(), 3);
    }

    #[test]
    fn test_threshold_algorithm_validation() {
        // Valid thresholds should work
        assert!(ThresholdAlgorithm::new(vec![0.5, 0.8]).is_ok());

        // Invalid thresholds should fail
        assert!(ThresholdAlgorithm::new(vec![1.5]).is_err());
        assert!(ThresholdAlgorithm::new(vec![-0.1]).is_err());
        assert!(ThresholdAlgorithm::new(vec![]).is_err());
    }

    #[test]
    fn test_uniform_threshold() {
        let algorithm = ThresholdAlgorithm::uniform(0.8, 3).unwrap();
        assert_eq!(algorithm.thresholds(), &[0.8, 0.8, 0.8]);

        assert!(ThresholdAlgorithm::uniform(1.5, 3).is_err());
        assert!(ThresholdAlgorithm::uniform(0.8, 0).is_err());
    }

    #[test]
    fn test_glint_detection() {
        let algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6, 0.7]).unwrap();

        // Create a test image with known values
        let mut image = ndarray::Array3::zeros((3, 3, 3));

        // Set pixel (1,1) to exceed thresholds in all bands
        image[[1, 1, 0]] = 0.8; // > 0.5
        image[[1, 1, 1]] = 0.9; // > 0.6
        image[[1, 1, 2]] = 0.9; // > 0.7

        // Set pixel (0,0) to exceed threshold only in first band
        image[[0, 0, 0]] = 0.6; // > 0.5
        image[[0, 0, 1]] = 0.3; // < 0.6
        image[[0, 0, 2]] = 0.3; // < 0.7

        // Set pixel (2,2) to not exceed any thresholds
        image[[2, 2, 0]] = 0.3; // < 0.5
        image[[2, 2, 1]] = 0.3; // < 0.6
        image[[2, 2, 2]] = 0.3; // < 0.7

        let mask = algorithm.detect_glint(&image).unwrap();

        // Check results
        assert_eq!(mask[[1, 1]], 1); // Should be masked (exceeds all thresholds)
        assert_eq!(mask[[0, 0]], 1); // Should be masked (exceeds first threshold)
        assert_eq!(mask[[2, 2]], 0); // Should not be masked
        assert_eq!(mask[[0, 1]], 0); // Default zero pixel should not be masked
    }

    #[test]
    fn test_band_count_mismatch() {
        let algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6]).unwrap();
        let image = ndarray::Array3::zeros((2, 2, 3)); // 3 bands, but algorithm expects 2

        let result = algorithm.detect_glint(&image);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            GlintError::BandCountMismatch { .. }
        ));
    }

    #[test]
    fn test_threshold_updates() {
        let mut algorithm = ThresholdAlgorithm::new(vec![0.5, 0.6, 0.7]).unwrap();

        // Update single band threshold
        algorithm.set_band_threshold(1, 0.8).unwrap();
        assert_eq!(algorithm.thresholds()[1], 0.8);

        // Invalid band index should fail
        assert!(algorithm.set_band_threshold(5, 0.8).is_err());

        // Invalid threshold value should fail
        assert!(algorithm.set_band_threshold(1, 1.5).is_err());

        // Update all thresholds
        algorithm.set_thresholds(vec![0.9, 0.8, 0.7]).unwrap();
        assert_eq!(algorithm.thresholds(), &[0.9, 0.8, 0.7]);
    }

    #[test]
    fn test_builder_pattern() {
        let algorithm = ThresholdAlgorithmBuilder::new()
            .add_threshold(0.8)
            .unwrap()
            .add_threshold(0.9)
            .unwrap()
            .add_threshold(0.7)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(algorithm.thresholds(), &[0.8, 0.9, 0.7]);

        let uniform_algorithm = ThresholdAlgorithmBuilder::new()
            .uniform(0.85, 4)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(uniform_algorithm.thresholds(), &[0.85, 0.85, 0.85, 0.85]);
    }

    #[test]
    fn test_algorithm_interface() {
        let algorithm = ThresholdAlgorithm::new(vec![0.8]).unwrap();

        assert_eq!(algorithm.name(), "Threshold");
        assert!(algorithm.description().contains("threshold"));
        assert_eq!(algorithm.required_bands(), Some(1));
        assert!(algorithm.supports_bands(1));
        assert!(!algorithm.supports_bands(2));
        assert!(algorithm.validate_parameters().is_ok());
    }
}