use crate::{AlignError, AlignResult};
#[derive(Debug, Clone)]
pub struct StereoDepthConfig {
pub block_size: usize,
pub min_disparity: i32,
pub max_disparity: i32,
pub focal_length_px: f64,
pub baseline_m: f64,
}
impl StereoDepthConfig {
pub fn validate(&self) -> AlignResult<()> {
if self.block_size == 0 || self.block_size % 2 == 0 {
return Err(AlignError::InvalidConfig(
"block_size must be a positive odd number".to_string(),
));
}
if self.min_disparity >= self.max_disparity {
return Err(AlignError::InvalidConfig(
"min_disparity must be less than max_disparity".to_string(),
));
}
if self.focal_length_px <= 0.0 {
return Err(AlignError::InvalidConfig(
"focal_length_px must be positive".to_string(),
));
}
if self.baseline_m <= 0.0 {
return Err(AlignError::InvalidConfig(
"baseline_m must be positive".to_string(),
));
}
Ok(())
}
}
impl Default for StereoDepthConfig {
fn default() -> Self {
Self {
block_size: 7,
min_disparity: 0,
max_disparity: 64,
focal_length_px: 700.0,
baseline_m: 0.12,
}
}
}
#[derive(Debug, Default)]
pub struct StereoDepthEstimator;
impl StereoDepthEstimator {
#[must_use]
pub fn new() -> Self {
Self
}
pub fn compute_depth_map(
&self,
left: &[u8],
right: &[u8],
width: usize,
height: usize,
config: &StereoDepthConfig,
) -> AlignResult<Vec<f32>> {
config.validate()?;
if left.len() != width * height || right.len() != width * height {
return Err(AlignError::InsufficientData(
"Image buffer length does not match width × height".to_string(),
));
}
let half = config.block_size / 2;
let max_disp = config.max_disparity as usize;
let min_disp = config.min_disparity.max(0) as usize;
let mut depth_map = vec![f32::INFINITY; width * height];
for y in 0..height {
let row_start = y.saturating_sub(half);
let row_end = (y + half + 1).min(height);
for x in 0..width {
let col_start_l = x.saturating_sub(half);
let col_end_l = (x + half + 1).min(width);
let mut best_sad = u64::MAX;
let mut best_disp: Option<usize> = None;
for d in min_disp..max_disp {
let right_col_start = if col_start_l >= d {
col_start_l - d
} else {
continue; };
let right_col_end = if col_end_l > d {
col_end_l - d
} else {
continue;
};
let mut sad: u64 = 0;
let mut count: usize = 0;
for row in row_start..row_end {
let left_row = &left[row * width..];
let right_row = &right[row * width..];
let l_slice = &left_row[col_start_l..col_end_l];
let r_slice = &right_row[right_col_start..right_col_end];
let cols = l_slice.len().min(r_slice.len());
for c in 0..cols {
let diff =
(i32::from(l_slice[c]) - i32::from(r_slice[c])).unsigned_abs();
sad += u64::from(diff);
count += 1;
}
}
if count == 0 {
continue;
}
let norm_sad = sad / count as u64;
if norm_sad < best_sad {
best_sad = norm_sad;
best_disp = Some(d);
}
}
if let Some(d) = best_disp {
if d == 0 {
depth_map[y * width + x] = f32::INFINITY;
} else {
let depth = (config.focal_length_px * config.baseline_m) / d as f64;
depth_map[y * width + x] = depth as f32;
}
}
}
}
Ok(depth_map)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform_image(width: usize, height: usize, value: u8) -> Vec<u8> {
vec![value; width * height]
}
#[test]
fn test_depth_map_uniform_disparity() {
let config = StereoDepthConfig {
block_size: 3,
min_disparity: 2,
max_disparity: 16,
focal_length_px: 400.0,
baseline_m: 0.1,
};
let estimator = StereoDepthEstimator::new();
let w = 24usize;
let h = 12usize;
let left = uniform_image(w, h, 100);
let right = uniform_image(w, h, 100);
let depth = estimator
.compute_depth_map(&left, &right, w, h, &config)
.expect("depth map should succeed");
assert_eq!(depth.len(), w * h);
let expected = (config.focal_length_px * config.baseline_m) / config.min_disparity as f64;
for (idx, &d) in depth.iter().enumerate() {
if d.is_finite() {
assert!(
(f64::from(d) - expected).abs() < 1.0,
"pixel {idx}: depth {d} expected ~{expected}"
);
}
}
}
#[test]
fn test_depth_map_zero_disparity_infinite_depth() {
let config = StereoDepthConfig {
block_size: 3,
min_disparity: 0, max_disparity: 8,
focal_length_px: 500.0,
baseline_m: 0.2,
};
let estimator = StereoDepthEstimator::new();
let w = 20usize;
let h = 10usize;
let img = uniform_image(w, h, 128);
let depth = estimator
.compute_depth_map(&img, &img, w, h, &config)
.expect("depth map should succeed");
assert_eq!(depth.len(), w * h);
for &d in &depth {
assert!(d.is_infinite(), "expected infinite depth, got {d}");
}
}
#[test]
fn test_depth_map_known_shift() {
let config = StereoDepthConfig {
block_size: 3,
min_disparity: 1,
max_disparity: 10,
focal_length_px: 300.0,
baseline_m: 0.06,
};
let estimator = StereoDepthEstimator::new();
let w = 32usize;
let h = 16usize;
let shift: usize = 4;
let mut left = vec![50u8; w * h];
let mut right = vec![50u8; w * h];
let stripe_col_l: usize = 12;
let stripe_col_r = stripe_col_l.saturating_sub(shift);
for row in 0..h {
left[row * w + stripe_col_l] = 200;
if stripe_col_r < w {
right[row * w + stripe_col_r] = 200;
}
}
let depth = estimator
.compute_depth_map(&left, &right, w, h, &config)
.expect("depth map should succeed");
let expected = (config.focal_length_px * config.baseline_m) / shift as f64;
let half = config.block_size / 2;
for row in half..(h - half) {
let idx = row * w + stripe_col_l;
let d = depth[idx];
if d.is_finite() {
assert!(
(f64::from(d) - expected).abs() < expected * 0.5,
"row {row}: depth {d} expected ~{expected}"
);
}
}
}
#[test]
fn test_config_validation_rejects_even_block_size() {
let config = StereoDepthConfig {
block_size: 4,
..StereoDepthConfig::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_validation_rejects_inverted_disparity_range() {
let config = StereoDepthConfig {
min_disparity: 10,
max_disparity: 5,
..StereoDepthConfig::default()
};
assert!(config.validate().is_err());
}
}