Skip to main content

oxigeo_algorithms/simd/
histogram.rs

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