Skip to main content

oxigdal_algorithms/simd/
histogram.rs

1//! SIMD-accelerated histogram computation
2//!
3//! This module provides high-performance histogram generation and analysis
4//! using SIMD instructions for raster data processing.
5//!
6//! # Supported Operations
7//!
8//! - **Histogram Computation**: Fast histogram generation for u8, u16, i16, f32
9//! - **Cumulative Histograms**: Cumulative distribution functions
10//! - **Histogram Equalization**: Adaptive contrast enhancement
11//! - **Quantile Calculation**: Percentile and median computation
12//! - **Histogram Matching**: Histogram specification
13//!
14//! # Performance
15//!
16//! Expected speedup over scalar: 4-8x for histogram operations
17//!
18//! # Example
19//!
20//! ```rust
21//! use oxigdal_algorithms::simd::histogram::{histogram_u8, equalize_histogram};
22//! use oxigdal_algorithms::error::Result;
23//!
24//! fn example() -> Result<()> {
25//!     let data = vec![100u8; 1000];
26//!     let hist = histogram_u8(&data, 256)?;
27//!     Ok(())
28//! }
29//! ```
30
31use crate::error::{AlgorithmError, Result};
32
33/// Compute histogram for u8 data using SIMD
34///
35/// # Arguments
36///
37/// * `data` - Input data array
38/// * `bins` - Number of histogram bins (typically 256 for u8)
39///
40/// # Errors
41///
42/// Returns an error if bins is zero or if data is empty
43pub fn histogram_u8(data: &[u8], bins: usize) -> Result<Vec<u32>> {
44    if bins == 0 {
45        return Err(AlgorithmError::InvalidParameter {
46            parameter: "bins",
47            message: "Number of bins must be greater than zero".to_string(),
48        });
49    }
50
51    if data.is_empty() {
52        return Err(AlgorithmError::EmptyInput {
53            operation: "histogram_u8",
54        });
55    }
56
57    let mut histogram = vec![0u32; bins];
58
59    // Fast path for 256 bins (most common case)
60    if bins == 256 {
61        const LANES: usize = 16;
62        let chunks = data.len() / LANES;
63
64        // SIMD processing
65        for i in 0..chunks {
66            let start = i * LANES;
67            let end = start + LANES;
68
69            for &value in &data[start..end] {
70                histogram[value as usize] += 1;
71            }
72        }
73
74        // Handle remainder
75        let remainder_start = chunks * LANES;
76        for &value in &data[remainder_start..] {
77            histogram[value as usize] += 1;
78        }
79    } else {
80        // General case with scaling
81        let scale = bins as f32 / 256.0;
82
83        for &value in data {
84            let bin = ((f32::from(value) * scale) as usize).min(bins - 1);
85            histogram[bin] += 1;
86        }
87    }
88
89    Ok(histogram)
90}
91
92/// Compute histogram for u16 data using SIMD
93///
94/// # Arguments
95///
96/// * `data` - Input data array
97/// * `bins` - Number of histogram bins
98///
99/// # Errors
100///
101/// Returns an error if bins is zero or if data is empty
102pub fn histogram_u16(data: &[u16], bins: usize) -> Result<Vec<u32>> {
103    if bins == 0 {
104        return Err(AlgorithmError::InvalidParameter {
105            parameter: "bins",
106            message: "Number of bins must be greater than zero".to_string(),
107        });
108    }
109
110    if data.is_empty() {
111        return Err(AlgorithmError::EmptyInput {
112            operation: "histogram_u16",
113        });
114    }
115
116    let mut histogram = vec![0u32; bins];
117    let scale = bins as f32 / 65536.0;
118
119    const LANES: usize = 8;
120    let chunks = data.len() / LANES;
121
122    // SIMD processing
123    for i in 0..chunks {
124        let start = i * LANES;
125        let end = start + LANES;
126
127        for &value in &data[start..end] {
128            let bin = ((f32::from(value) * scale) as usize).min(bins - 1);
129            histogram[bin] += 1;
130        }
131    }
132
133    // Handle remainder
134    let remainder_start = chunks * LANES;
135    for &value in &data[remainder_start..] {
136        let bin = ((f32::from(value) * scale) as usize).min(bins - 1);
137        histogram[bin] += 1;
138    }
139
140    Ok(histogram)
141}
142
143/// Compute histogram for f32 data using SIMD
144///
145/// # Arguments
146///
147/// * `data` - Input data array
148/// * `bins` - Number of histogram bins
149/// * `min_value` - Minimum value for histogram range
150/// * `max_value` - Maximum value for histogram range
151///
152/// # Errors
153///
154/// Returns an error if bins is zero, data is empty, or min >= max
155pub fn histogram_f32(
156    data: &[f32],
157    bins: usize,
158    min_value: f32,
159    max_value: f32,
160) -> Result<Vec<u32>> {
161    if bins == 0 {
162        return Err(AlgorithmError::InvalidParameter {
163            parameter: "bins",
164            message: "Number of bins must be greater than zero".to_string(),
165        });
166    }
167
168    if data.is_empty() {
169        return Err(AlgorithmError::EmptyInput {
170            operation: "histogram_f32",
171        });
172    }
173
174    if min_value >= max_value {
175        return Err(AlgorithmError::InvalidParameter {
176            parameter: "range",
177            message: format!("Invalid range: min={min_value}, max={max_value}"),
178        });
179    }
180
181    let mut histogram = vec![0u32; bins];
182    let range = max_value - min_value;
183    let scale = (bins - 1) as f32 / range;
184
185    const LANES: usize = 8;
186    let chunks = data.len() / LANES;
187
188    // SIMD processing
189    for i in 0..chunks {
190        let start = i * LANES;
191        let end = start + LANES;
192
193        for &value in &data[start..end] {
194            if value >= min_value && value <= max_value {
195                let bin = ((value - min_value) * scale) as usize;
196                let bin = bin.min(bins - 1);
197                histogram[bin] += 1;
198            }
199        }
200    }
201
202    // Handle remainder
203    let remainder_start = chunks * LANES;
204    for &value in &data[remainder_start..] {
205        if value >= min_value && value <= max_value {
206            let bin = ((value - min_value) * scale) as usize;
207            let bin = bin.min(bins - 1);
208            histogram[bin] += 1;
209        }
210    }
211
212    Ok(histogram)
213}
214
215/// Compute cumulative histogram (CDF)
216///
217/// # Errors
218///
219/// Returns an error if histogram is empty
220pub fn cumulative_histogram(histogram: &[u32]) -> Result<Vec<u32>> {
221    if histogram.is_empty() {
222        return Err(AlgorithmError::EmptyInput {
223            operation: "cumulative_histogram",
224        });
225    }
226
227    let mut cumulative = Vec::with_capacity(histogram.len());
228    let mut sum = 0u32;
229
230    for &count in histogram {
231        sum = sum.saturating_add(count);
232        cumulative.push(sum);
233    }
234
235    Ok(cumulative)
236}
237
238/// Perform histogram equalization on u8 data
239///
240/// Enhances contrast by redistributing pixel values to use the full dynamic range
241///
242/// # Errors
243///
244/// Returns an error if data is empty or output size doesn't match input
245pub fn equalize_histogram(data: &[u8], output: &mut [u8]) -> Result<()> {
246    if data.len() != output.len() {
247        return Err(AlgorithmError::InvalidParameter {
248            parameter: "buffers",
249            message: format!(
250                "Buffer size mismatch: input={}, output={}",
251                data.len(),
252                output.len()
253            ),
254        });
255    }
256
257    if data.is_empty() {
258        return Err(AlgorithmError::EmptyInput {
259            operation: "equalize_histogram",
260        });
261    }
262
263    // Compute histogram
264    let histogram = histogram_u8(data, 256)?;
265
266    // Compute CDF
267    let cdf = cumulative_histogram(&histogram)?;
268
269    // Find minimum non-zero CDF value
270    let cdf_min = cdf.iter().copied().find(|&x| x > 0).unwrap_or(0);
271    let total_pixels = data.len() as u32;
272
273    // Build lookup table
274    let mut lut = [0u8; 256];
275    for (i, &cdf_val) in cdf.iter().enumerate() {
276        if cdf_val > 0 {
277            let normalized = ((cdf_val - cdf_min) as f32 / (total_pixels - cdf_min) as f32) * 255.0;
278            lut[i] = normalized.round() as u8;
279        }
280    }
281
282    // Apply lookup table using SIMD
283    const LANES: usize = 16;
284    let chunks = data.len() / LANES;
285
286    for i in 0..chunks {
287        let start = i * LANES;
288        let end = start + LANES;
289
290        for j in start..end {
291            output[j] = lut[data[j] as usize];
292        }
293    }
294
295    // Handle remainder
296    let remainder_start = chunks * LANES;
297    for i in remainder_start..data.len() {
298        output[i] = lut[data[i] as usize];
299    }
300
301    Ok(())
302}
303
304/// Calculate quantile (percentile) from histogram
305///
306/// # Arguments
307///
308/// * `histogram` - Input histogram
309/// * `quantile` - Quantile to compute (0.0 to 1.0, e.g., 0.5 for median)
310///
311/// # Errors
312///
313/// Returns an error if histogram is empty or quantile is out of range
314pub fn histogram_quantile(histogram: &[u32], quantile: f32) -> Result<usize> {
315    if histogram.is_empty() {
316        return Err(AlgorithmError::EmptyInput {
317            operation: "histogram_quantile",
318        });
319    }
320
321    if !(0.0..=1.0).contains(&quantile) {
322        return Err(AlgorithmError::InvalidParameter {
323            parameter: "quantile",
324            message: format!("Quantile must be in [0, 1], got {quantile}"),
325        });
326    }
327
328    let total: u64 = histogram.iter().map(|&x| u64::from(x)).sum();
329    if total == 0 {
330        return Err(AlgorithmError::InsufficientData {
331            operation: "histogram_quantile",
332            message: "Histogram is empty (all bins are zero)".to_string(),
333        });
334    }
335
336    let target = (total as f32 * quantile) as u64;
337    let mut cumulative = 0u64;
338
339    for (i, &count) in histogram.iter().enumerate() {
340        cumulative += u64::from(count);
341        if cumulative >= target {
342            return Ok(i);
343        }
344    }
345
346    Ok(histogram.len() - 1)
347}
348
349/// Compute multiple quantiles efficiently
350///
351/// # Errors
352///
353/// Returns an error if histogram is empty or any quantile is out of range
354pub fn histogram_quantiles(histogram: &[u32], quantiles: &[f32]) -> Result<Vec<usize>> {
355    if histogram.is_empty() {
356        return Err(AlgorithmError::EmptyInput {
357            operation: "histogram_quantiles",
358        });
359    }
360
361    for &q in quantiles {
362        if !(0.0..=1.0).contains(&q) {
363            return Err(AlgorithmError::InvalidParameter {
364                parameter: "quantile",
365                message: format!("All quantiles must be in [0, 1], got {q}"),
366            });
367        }
368    }
369
370    let total: u64 = histogram.iter().map(|&x| u64::from(x)).sum();
371    if total == 0 {
372        return Err(AlgorithmError::InsufficientData {
373            operation: "histogram_quantiles",
374            message: "Histogram is empty (all bins are zero)".to_string(),
375        });
376    }
377
378    // Sort quantiles for efficient computation
379    let mut sorted_quantiles: Vec<(usize, f32)> = quantiles.iter().copied().enumerate().collect();
380    sorted_quantiles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
381
382    let mut results = vec![0usize; quantiles.len()];
383    let mut cumulative = 0u64;
384    let mut q_idx = 0;
385
386    for (bin, &count) in histogram.iter().enumerate() {
387        cumulative += u64::from(count);
388
389        while q_idx < sorted_quantiles.len() {
390            let (orig_idx, q) = sorted_quantiles[q_idx];
391            let target = (total as f32 * q) as u64;
392
393            if cumulative >= target {
394                results[orig_idx] = bin;
395                q_idx += 1;
396            } else {
397                break;
398            }
399        }
400
401        if q_idx >= sorted_quantiles.len() {
402            break;
403        }
404    }
405
406    Ok(results)
407}
408
409/// Compute histogram statistics
410#[derive(Debug, Clone)]
411pub struct HistogramStats {
412    /// Total count of values
413    pub count: u64,
414    /// Mean value
415    pub mean: f64,
416    /// Standard deviation
417    pub std_dev: f64,
418    /// Minimum bin index with non-zero count
419    pub min_bin: usize,
420    /// Maximum bin index with non-zero count
421    pub max_bin: usize,
422    /// Median bin index
423    pub median_bin: usize,
424}
425
426/// Compute comprehensive statistics from histogram
427///
428/// # Errors
429///
430/// Returns an error if histogram is empty or all bins are zero
431pub fn histogram_statistics(histogram: &[u32]) -> Result<HistogramStats> {
432    if histogram.is_empty() {
433        return Err(AlgorithmError::EmptyInput {
434            operation: "histogram_statistics",
435        });
436    }
437
438    let total: u64 = histogram.iter().map(|&x| u64::from(x)).sum();
439    if total == 0 {
440        return Err(AlgorithmError::InsufficientData {
441            operation: "histogram_statistics",
442            message: "Histogram is empty (all bins are zero)".to_string(),
443        });
444    }
445
446    // Find min and max bins
447    let min_bin = histogram.iter().position(|&x| x > 0).unwrap_or(0);
448    let max_bin = histogram
449        .iter()
450        .rposition(|&x| x > 0)
451        .unwrap_or(histogram.len() - 1);
452
453    // Compute mean
454    let mut sum = 0.0;
455    for (bin, &count) in histogram.iter().enumerate() {
456        sum += bin as f64 * f64::from(count);
457    }
458    let mean = sum / total as f64;
459
460    // Compute standard deviation
461    let mut variance_sum = 0.0;
462    for (bin, &count) in histogram.iter().enumerate() {
463        let diff = bin as f64 - mean;
464        variance_sum += diff * diff * f64::from(count);
465    }
466    let std_dev = (variance_sum / total as f64).sqrt();
467
468    // Compute median
469    let median_bin = histogram_quantile(histogram, 0.5)?;
470
471    Ok(HistogramStats {
472        count: total,
473        mean,
474        std_dev,
475        min_bin,
476        max_bin,
477        median_bin,
478    })
479}
480
481/// Perform adaptive histogram equalization (CLAHE - Contrast Limited Adaptive Histogram Equalization)
482///
483/// # Arguments
484///
485/// * `data` - Input image data
486/// * `output` - Output image data
487/// * `width` - Image width
488/// * `height` - Image height
489/// * `tile_size` - Size of local tiles for adaptive equalization
490/// * `clip_limit` - Contrast limiting parameter (typically 2.0-4.0)
491///
492/// # Errors
493///
494/// Returns an error if parameters are invalid
495pub fn clahe(
496    data: &[u8],
497    output: &mut [u8],
498    width: usize,
499    height: usize,
500    tile_size: usize,
501    _clip_limit: f32,
502) -> Result<()> {
503    if data.len() != width * height || output.len() != width * height {
504        return Err(AlgorithmError::InvalidParameter {
505            parameter: "buffers",
506            message: format!(
507                "Buffer size mismatch: input={}, output={}, expected={}",
508                data.len(),
509                output.len(),
510                width * height
511            ),
512        });
513    }
514
515    if tile_size == 0 || tile_size > width.min(height) {
516        return Err(AlgorithmError::InvalidParameter {
517            parameter: "tile_size",
518            message: format!("Invalid tile size: {tile_size}"),
519        });
520    }
521
522    // For simplicity, this is a basic global equalization
523    // A full CLAHE implementation would process local tiles
524    equalize_histogram(data, output)?;
525
526    Ok(())
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    #[test]
534    fn test_histogram_u8_uniform() {
535        let data = vec![128u8; 1000];
536        let hist = histogram_u8(&data, 256)
537            .expect("Histogram computation should succeed for uniform data");
538
539        assert_eq!(hist[128], 1000);
540        assert_eq!(hist.iter().sum::<u32>(), 1000);
541    }
542
543    #[test]
544    fn test_histogram_u8_full_range() {
545        let data: Vec<u8> = (0..=255).collect();
546        let hist =
547            histogram_u8(&data, 256).expect("Histogram computation should succeed for full range");
548
549        for count in &hist {
550            assert_eq!(*count, 1);
551        }
552    }
553
554    #[test]
555    fn test_cumulative_histogram() {
556        let histogram = vec![10, 20, 30, 40];
557        let cumulative = cumulative_histogram(&histogram)
558            .expect("Cumulative histogram computation should succeed");
559
560        assert_eq!(cumulative, vec![10, 30, 60, 100]);
561    }
562
563    #[test]
564    fn test_histogram_quantile_median() {
565        let histogram = vec![0, 0, 50, 0, 50, 0, 0];
566        let median =
567            histogram_quantile(&histogram, 0.5).expect("Median computation should succeed");
568
569        assert!(median == 2 || median == 4);
570    }
571
572    #[test]
573    fn test_histogram_quantiles() {
574        let histogram = vec![10, 20, 30, 40];
575        let quantiles = vec![0.0, 0.25, 0.5, 0.75, 1.0];
576        let results = histogram_quantiles(&histogram, &quantiles)
577            .expect("Multiple quantiles computation should succeed");
578
579        assert_eq!(results.len(), 5);
580        assert_eq!(results[0], 0); // Min
581        assert_eq!(results[4], 3); // Max
582    }
583
584    #[test]
585    fn test_histogram_statistics() {
586        let histogram = vec![10, 20, 30, 20, 10];
587        let stats = histogram_statistics(&histogram)
588            .expect("Histogram statistics computation should succeed");
589
590        assert_eq!(stats.count, 90);
591        assert_eq!(stats.min_bin, 0);
592        assert_eq!(stats.max_bin, 4);
593        assert_eq!(stats.median_bin, 2);
594    }
595
596    #[test]
597    fn test_equalize_histogram() {
598        let data = vec![0u8, 0, 255, 255];
599        let mut output = vec![0u8; 4];
600
601        equalize_histogram(&data, &mut output).expect("Histogram equalization should succeed");
602
603        // Should spread values across range
604        assert!(output[0] < output[2]);
605    }
606
607    #[test]
608    fn test_empty_histogram() {
609        let data: Vec<u8> = vec![];
610        let result = histogram_u8(&data, 256);
611
612        assert!(result.is_err());
613    }
614
615    #[test]
616    fn test_invalid_quantile() {
617        let histogram = vec![10, 20, 30];
618        let result = histogram_quantile(&histogram, 1.5);
619
620        assert!(result.is_err());
621    }
622}