use crate::error::{AlgorithmError, Result};
use oxigeo_core::buffer::RasterBuffer;
#[derive(Debug, Clone, Copy, Default)]
pub struct ZonalStatistics {
pub zone_id: i32,
pub count: u64,
pub sum: f64,
pub mean: f64,
pub min: f64,
pub max: f64,
pub std_dev: f64,
}
pub fn compute_zonal_stats(
values: &RasterBuffer,
zones: &RasterBuffer,
) -> Result<Vec<ZonalStatistics>> {
if values.width() != zones.width() || values.height() != zones.height() {
return Err(AlgorithmError::InvalidDimensions {
message: "Rasters must have same dimensions",
actual: values.width() as usize,
expected: zones.width() as usize,
});
}
let mut stats_map: std::collections::HashMap<i32, ZonalStatistics> =
std::collections::HashMap::new();
for y in 0..values.height() {
for x in 0..values.width() {
let zone_raw = zones.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let value = values.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if zones.is_nodata(zone_raw) || !zone_raw.is_finite() {
continue;
}
if values.is_nodata(value) || !value.is_finite() {
continue;
}
let zone_id = zone_raw as i32;
let stats = stats_map.entry(zone_id).or_insert_with(|| ZonalStatistics {
zone_id,
min: f64::MAX,
max: f64::MIN,
..Default::default()
});
stats.count += 1;
stats.sum += value;
stats.min = stats.min.min(value);
stats.max = stats.max.max(value);
}
}
for stats in stats_map.values_mut() {
stats.mean = stats.sum / stats.count as f64;
}
for y in 0..values.height() {
for x in 0..values.width() {
let zone_raw = zones.get_pixel(x, y).map_err(AlgorithmError::Core)?;
let value = values.get_pixel(x, y).map_err(AlgorithmError::Core)?;
if zones.is_nodata(zone_raw) || !zone_raw.is_finite() {
continue;
}
if values.is_nodata(value) || !value.is_finite() {
continue;
}
let zone_id = zone_raw as i32;
if let Some(stats) = stats_map.get_mut(&zone_id) {
let diff = value - stats.mean;
stats.std_dev += diff * diff;
}
}
}
for stats in stats_map.values_mut() {
stats.std_dev = (stats.std_dev / stats.count as f64).sqrt();
}
Ok(stats_map.into_values().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use oxigeo_core::types::RasterDataType;
#[test]
fn test_zonal_stats() {
let mut values = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
let mut zones = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
for y in 0..5 {
for x in 0..5 {
values.set_pixel(x, y, (x + y) as f64).ok();
zones.set_pixel(x, y, (x / 2) as f64).ok();
}
}
let result = compute_zonal_stats(&values, &zones);
assert!(result.is_ok());
}
#[test]
fn test_zonal_stats_excludes_nodata_values() {
use oxigeo_core::types::NoDataValue;
let mut values = RasterBuffer::nodata_filled(
4,
4,
RasterDataType::Float64,
NoDataValue::Float(f64::NAN),
);
let mut zones = RasterBuffer::zeros(4, 4, RasterDataType::Float64);
for y in 0..4 {
for x in 0..4 {
values
.set_pixel(x, y, 4.0)
.expect("set value pixel should succeed in test");
zones
.set_pixel(x, y, 1.0)
.expect("set zone pixel should succeed in test");
}
}
values
.set_pixel(0, 0, f64::NAN)
.expect("set NoData value should succeed in test");
let result =
compute_zonal_stats(&values, &zones).expect("zonal stats should succeed in test");
assert_eq!(result.len(), 1);
let zone = result[0];
assert_eq!(zone.zone_id, 1);
assert_eq!(zone.count, 15);
assert!(zone.sum.is_finite(), "sum must not be NaN-poisoned");
assert!((zone.sum - 60.0).abs() < 1e-9);
assert!((zone.mean - 4.0).abs() < 1e-9);
assert!((zone.std_dev - 0.0).abs() < 1e-9);
}
#[test]
fn test_zonal_stats_skips_nodata_zone() {
use oxigeo_core::types::NoDataValue;
let mut values = RasterBuffer::zeros(3, 3, RasterDataType::Float64);
let mut zones = RasterBuffer::nodata_filled(
3,
3,
RasterDataType::Float64,
NoDataValue::Float(f64::NAN),
);
for y in 0..3 {
for x in 0..3 {
values
.set_pixel(x, y, 10.0)
.expect("set value pixel should succeed in test");
zones
.set_pixel(x, y, 5.0)
.expect("set zone pixel should succeed in test");
}
}
zones
.set_pixel(1, 1, f64::NAN)
.expect("set NoData zone should succeed in test");
let result =
compute_zonal_stats(&values, &zones).expect("zonal stats should succeed in test");
assert_eq!(result.len(), 1);
assert_eq!(result[0].zone_id, 5);
assert_eq!(result[0].count, 8);
}
}