use crate::error::VisionError;
#[derive(Debug, Clone)]
pub struct DisparityMap {
pub data: Vec<f32>,
pub width: usize,
pub height: usize,
pub min_disp: i32,
pub max_disp: i32,
}
impl DisparityMap {
pub fn new(width: usize, height: usize, min_disp: i32, max_disp: i32) -> Self {
Self {
data: vec![0.0_f32; width * height],
width,
height,
min_disp,
max_disp,
}
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> f32 {
self.data[row * self.width + col]
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, val: f32) {
self.data[row * self.width + col] = val;
}
pub fn to_depth(&self, baseline: f32, focal_length: f32) -> Vec<f32> {
self.data
.iter()
.map(|&d| {
if d > 0.0 {
baseline * focal_length / d
} else {
0.0
}
})
.collect()
}
pub fn valid_count(&self) -> usize {
self.data.iter().filter(|&&d| d > 0.0).count()
}
pub fn apply_subpixel_refinement(&mut self, cost_volume: &[f32], n_disp: usize) {
for row in 0..self.height {
for col in 0..self.width {
let d = self.get(row, col) as i32 - self.min_disp;
if d < 1 || d as usize + 1 >= n_disp {
continue;
}
let base = (row * self.width + col) * n_disp;
let c_m = cost_volume[base + d as usize - 1];
let c_0 = cost_volume[base + d as usize];
let c_p = cost_volume[base + d as usize + 1];
let denom = c_m - 2.0 * c_0 + c_p;
if denom.abs() > 1e-6 {
let delta = 0.5 * (c_m - c_p) / denom;
let refined = (d as f32 + delta + self.min_disp as f32)
.clamp(self.min_disp as f32, self.max_disp as f32);
self.set(row, col, refined);
}
}
}
}
}
pub struct BlockMatching {
pub block_size: usize,
pub min_disparity: i32,
pub max_disparity: i32,
pub uniqueness_ratio: f32,
pub lr_check: bool,
pub lr_threshold: i32,
}
impl BlockMatching {
pub fn new(block_size: usize, min_disp: i32, max_disp: i32) -> Self {
Self {
block_size,
min_disparity: min_disp,
max_disparity: max_disp,
uniqueness_ratio: 0.15,
lr_check: false,
lr_threshold: 1,
}
}
pub fn compute(
&self,
left: &[u8],
right: &[u8],
width: usize,
height: usize,
) -> Result<DisparityMap, VisionError> {
self.validate(width, height)?;
let half = self.block_size / 2;
let mut disp_map = DisparityMap::new(width, height, self.min_disparity, self.max_disparity);
let compute_for = |img_l: &[u8], img_r: &[u8]| -> Vec<f32> {
let mut out = vec![0.0f32; width * height];
for row in half..height.saturating_sub(half) {
for col in (half as i32 + self.max_disparity) as usize..width.saturating_sub(half) {
let mut best_cost: u64 = u64::MAX;
let mut best_d = self.min_disparity;
let mut second_best: u64 = u64::MAX;
for d in self.min_disparity..=self.max_disparity {
let right_col = col as i32 - d;
if right_col < half as i32 || right_col >= width as i32 - half as i32 {
continue;
}
let rc = right_col as usize;
let mut cost: u64 = 0;
for br in -(half as i32)..=half as i32 {
let lr = (row as i32 + br) as usize;
for bc in -(half as i32)..=half as i32 {
let lc = (col as i32 + bc) as usize;
let rcc = (rc as i32 + bc) as usize;
let lv = img_l[lr * width + lc] as i64;
let rv = img_r[lr * width + rcc] as i64;
cost += (lv - rv).unsigned_abs();
}
}
if cost < best_cost {
second_best = best_cost;
best_cost = cost;
best_d = d;
} else if cost < second_best {
second_best = cost;
}
}
let pass_unique = if second_best > 0 && self.uniqueness_ratio > 0.0 {
(best_cost as f32 / second_best as f32) < 1.0 - self.uniqueness_ratio
} else {
true
};
if pass_unique {
out[row * width + col] = best_d as f32;
}
}
}
out
};
let left_disp = compute_for(left, right);
if self.lr_check {
let right_disp = compute_for(right, left);
for row in 0..height {
for col in 0..width {
let dl = left_disp[row * width + col];
let matched_col = col as i32 - dl as i32;
if matched_col >= 0 && matched_col < width as i32 {
let dr = right_disp[row * width + matched_col as usize];
if (dl + dr).abs() <= self.lr_threshold as f32 {
disp_map.set(row, col, dl);
}
}
}
}
} else {
disp_map.data = left_disp;
}
Ok(disp_map)
}
fn validate(&self, width: usize, height: usize) -> Result<(), VisionError> {
if self.block_size == 0 {
return Err(VisionError::InvalidParameter(
"block_size must be at least 1".into(),
));
}
if self.min_disparity > self.max_disparity {
return Err(VisionError::InvalidParameter(
"min_disparity must be ≤ max_disparity".into(),
));
}
let half = self.block_size / 2;
if height < 2 * half + 1 || width < 2 * half + 1 {
return Err(VisionError::InvalidParameter(
"Image is smaller than the matching block".into(),
));
}
Ok(())
}
}
pub struct SemiGlobalMatching {
pub block_size: usize,
pub min_disparity: i32,
pub max_disparity: i32,
pub p1: u32,
pub p2: u32,
pub subpixel: bool,
}
impl SemiGlobalMatching {
pub fn new(min_disp: i32, max_disp: i32) -> Self {
Self {
block_size: 3,
min_disparity: min_disp,
max_disparity: max_disp,
p1: 8,
p2: 32,
subpixel: false,
}
}
pub fn compute(
&self,
left: &[u8],
right: &[u8],
width: usize,
height: usize,
) -> Result<DisparityMap, VisionError> {
self.validate(width, height)?;
let n_disp = (self.max_disparity - self.min_disparity + 1) as usize;
let census_left = self.census_transform(left, width, height);
let census_right = self.census_transform(right, width, height);
let mut base_cost = vec![0u16; height * width * n_disp];
self.build_cost_volume(
&census_left,
&census_right,
&mut base_cost,
width,
height,
n_disp,
);
let mut agg_cost = vec![0u32; height * width * n_disp];
let directions: [(i32, i32); 8] = [
(0, 1),
(0, -1),
(1, 0),
(-1, 0),
(1, 1),
(1, -1),
(-1, 1),
(-1, -1),
];
for (dr, dc) in &directions {
self.aggregate_along_path(&base_cost, &mut agg_cost, width, height, n_disp, *dr, *dc);
}
let mut disp_map = DisparityMap::new(width, height, self.min_disparity, self.max_disparity);
for row in 0..height {
for col in 0..width {
let base = (row * width + col) * n_disp;
let best_d = (0..n_disp).min_by_key(|&d| agg_cost[base + d]).unwrap_or(0);
disp_map.set(row, col, (best_d as i32 + self.min_disparity) as f32);
}
}
if self.subpixel {
let agg_f32: Vec<f32> = agg_cost.iter().map(|&v| v as f32).collect();
disp_map.apply_subpixel_refinement(&agg_f32, n_disp);
}
Ok(disp_map)
}
fn census_transform(&self, img: &[u8], width: usize, height: usize) -> Vec<u64> {
let mut census = vec![0u64; width * height];
let h = self.block_size;
for row in h..height.saturating_sub(h) {
for col in h..width.saturating_sub(h) {
let center = img[row * width + col];
let mut bits = 0u64;
let mut pos = 0u32;
'outer: for br in -(h as i32)..=h as i32 {
for bc in -(h as i32)..=h as i32 {
if br == 0 && bc == 0 {
continue;
}
let nr = (row as i32 + br) as usize;
let nc = (col as i32 + bc) as usize;
if img[nr * width + nc] < center {
bits |= 1u64 << pos;
}
pos += 1;
if pos >= 64 {
break 'outer;
}
}
}
census[row * width + col] = bits;
}
}
census
}
fn build_cost_volume(
&self,
census_l: &[u64],
census_r: &[u64],
costs: &mut [u16],
width: usize,
height: usize,
n_disp: usize,
) {
for row in 0..height {
for col in 0..width {
let base = (row * width + col) * n_disp;
for d in 0..n_disp {
let d_i = d as i32 + self.min_disparity;
let right_col = col as i32 - d_i;
costs[base + d] = if right_col >= 0 && right_col < width as i32 {
let hamming = (census_l[row * width + col]
^ census_r[row * width + right_col as usize])
.count_ones() as u16;
hamming.min(255)
} else {
255
};
}
}
}
}
fn aggregate_along_path(
&self,
base_cost: &[u16],
agg: &mut [u32],
width: usize,
height: usize,
n_disp: usize,
dr: i32,
dc: i32,
) {
let rows: Box<dyn Iterator<Item = usize>> = if dr >= 0 {
Box::new(0..height)
} else {
Box::new((0..height).rev())
};
for row in rows {
let cols: Box<dyn Iterator<Item = usize>> = if dc >= 0 {
Box::new(0..width)
} else {
Box::new((0..width).rev())
};
for col in cols {
let prev_r = row as i32 - dr;
let prev_c = col as i32 - dc;
let has_prev =
prev_r >= 0 && prev_r < height as i32 && prev_c >= 0 && prev_c < width as i32;
let cur_base = (row * width + col) * n_disp;
if !has_prev {
for d in 0..n_disp {
agg[cur_base + d] =
agg[cur_base + d].saturating_add(base_cost[cur_base + d] as u32);
}
continue;
}
let pr = prev_r as usize;
let pc = prev_c as usize;
let prev_base = (pr * width + pc) * n_disp;
let min_prev = (0..n_disp)
.map(|d| agg[prev_base + d])
.min()
.unwrap_or(u32::MAX);
for d in 0..n_disp {
let matching = base_cost[cur_base + d] as u32;
let same_d = agg[prev_base + d];
let d_plus = if d + 1 < n_disp {
agg[prev_base + d + 1].saturating_add(self.p1)
} else {
u32::MAX
};
let d_minus = if d > 0 {
agg[prev_base + d - 1].saturating_add(self.p1)
} else {
u32::MAX
};
let other = min_prev.saturating_add(self.p2);
let path_cost =
matching + same_d.min(d_plus).min(d_minus).min(other) - min_prev;
agg[cur_base + d] = agg[cur_base + d].saturating_add(path_cost);
}
}
}
}
fn validate(&self, width: usize, height: usize) -> Result<(), VisionError> {
if self.min_disparity > self.max_disparity {
return Err(VisionError::InvalidParameter(
"min_disparity must be ≤ max_disparity".into(),
));
}
if self.block_size == 0 {
return Err(VisionError::InvalidParameter(
"block_size must be at least 1".into(),
));
}
if height < 2 * self.block_size + 1 || width < 2 * self.block_size + 1 {
return Err(VisionError::InvalidParameter(
"Image is smaller than the census window".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_stereo(width: usize, height: usize, true_disp: i32) -> (Vec<u8>, Vec<u8>) {
let left: Vec<u8> = (0..height * width)
.map(|i| ((i % width) * 255 / width) as u8)
.collect();
let mut right = vec![128u8; height * width];
for row in 0..height {
for col in 0..width {
let src = col as i32 + true_disp;
if src >= 0 && src < width as i32 {
right[row * width + col] = left[row * width + src as usize];
}
}
}
(left, right)
}
#[test]
fn test_disparity_map_new_and_accessors() {
let mut dm = DisparityMap::new(10, 8, 0, 16);
assert_eq!(dm.width, 10);
assert_eq!(dm.height, 8);
dm.set(3, 5, 7.0);
assert!((dm.get(3, 5) - 7.0).abs() < 1e-6);
}
#[test]
fn test_to_depth() {
let mut dm = DisparityMap::new(4, 4, 0, 16);
dm.set(1, 1, 4.0);
let depth = dm.to_depth(0.1, 500.0);
assert!((depth[4 + 1] - 12.5).abs() < 1e-4, "depth={}", depth[5]);
assert_eq!(depth[0], 0.0);
}
#[test]
fn test_block_matching_uniform_image() {
let bm = BlockMatching::new(3, 0, 8);
let img = vec![200u8; 32 * 32];
let disp = bm.compute(&img, &img, 32, 32).expect("BM failed");
assert_eq!(disp.width, 32);
assert_eq!(disp.height, 32);
}
#[test]
fn test_block_matching_known_disparity() {
let width = 64;
let height = 32;
let true_disp = 4i32;
let (left, right) = synthetic_stereo(width, height, true_disp);
let bm = BlockMatching {
block_size: 5,
min_disparity: 0,
max_disparity: 16,
uniqueness_ratio: 0.10,
lr_check: false,
lr_threshold: 1,
};
let disp = bm.compute(&left, &right, width, height).expect("BM failed");
let mut correct = 0;
let mut total = 0;
let margin = 8usize;
for row in margin..height - margin {
for col in margin..width - margin {
let d = disp.get(row, col);
if d > 0.0 {
if (d - true_disp as f32).abs() <= 1.0 {
correct += 1;
}
total += 1;
}
}
}
if total > 0 {
assert!(
correct * 100 / total >= 50,
"BM accuracy too low: {correct}/{total}"
);
}
}
#[test]
fn test_sgm_runs_without_error() {
let sgm = SemiGlobalMatching::new(0, 8);
let img = vec![100u8; 32 * 32];
let disp = sgm.compute(&img, &img, 32, 32).expect("SGM failed");
assert_eq!(disp.width, 32);
}
#[test]
fn test_block_matching_invalid_params() {
let bm = BlockMatching::new(3, 5, 2); let res = bm.compute(&[0u8; 64], &[0u8; 64], 8, 8);
assert!(res.is_err());
}
}