1use crate::error::{IrisError, Result};
2use crate::image::Image;
3use burn::tensor::{Tensor, TensorData, backend::Backend};
4
5pub struct StereoBlockMatcher {
7 block_size: i32,
9 num_disparities: i32,
11 min_disparity: i32,
13}
14
15impl StereoBlockMatcher {
16 pub fn new(block_size: i32, num_disparities: i32) -> Result<Self> {
21 if block_size < 3 || block_size % 2 == 0 {
22 return Err(IrisError::InvalidParameter(format!(
23 "block_size must be odd and >= 3, got {block_size}"
24 )));
25 }
26 if num_disparities <= 0 || num_disparities % 16 != 0 {
27 return Err(IrisError::InvalidParameter(format!(
28 "num_disparities must be > 0 and divisible by 16, got {num_disparities}"
29 )));
30 }
31 Ok(Self {
32 block_size,
33 num_disparities,
34 min_disparity: 0,
35 })
36 }
37
38 pub fn with_min_disparity(mut self, min_disparity: i32) -> Self {
40 self.min_disparity = min_disparity;
41 self
42 }
43
44 pub fn compute<B: Backend>(&self, left: &Image<B>, right: &Image<B>) -> Result<Tensor<B, 2>> {
50 if left.channels() != 1 || right.channels() != 1 {
51 return Err(IrisError::InvalidParameter(
52 "Stereo input images must be single-channel (grayscale)".into(),
53 ));
54 }
55 if left.shape() != right.shape() {
56 return Err(IrisError::DimensionMismatch {
57 expected: left.shape().to_vec(),
58 actual: right.shape().to_vec(),
59 });
60 }
61
62 let dims = left.tensor.dims();
63 let h = dims[1];
64 let w = dims[2];
65
66 let half_block = self.block_size / 2;
67 let max_disp = self.num_disparities;
68 let min_d = self.min_disparity;
69
70 let left_data: Vec<f32> = left.tensor.clone().into_data().iter::<f32>().collect();
71 let right_data: Vec<f32> = right.tensor.clone().into_data().iter::<f32>().collect();
72
73 let mut disparity = vec![0.0f32; h * w];
74
75 for y in 0..h {
76 for x in 0..w {
77 let mut best_disp = 0i32;
78 let mut best_cost = f32::MAX;
79
80 for d in min_d..(min_d + max_disp) {
81 let mut cost = 0.0f32;
83 let mut valid = true;
84
85 for by in -half_block..=half_block {
86 for bx in -half_block..=half_block {
87 let ly = y as i32 + by;
88 let lx = x as i32 + bx;
89 let rx = lx - d;
90
91 if ly < 0 || ly >= h as i32 || lx < 0 || lx >= w as i32 {
92 valid = false;
93 break;
94 }
95 if rx < 0 || rx >= w as i32 {
96 valid = false;
97 break;
98 }
99
100 let l_val = left_data[ly as usize * w + lx as usize];
101 let r_val = right_data[ly as usize * w + rx as usize];
102 cost += (l_val - r_val).abs();
103 }
104 if !valid {
105 break;
106 }
107 }
108
109 if valid && cost < best_cost {
110 best_cost = cost;
111 best_disp = d;
112 }
113 }
114
115 disparity[y * w + x] = (best_disp as f32) * 16.0;
117 }
118 }
119
120 let device = left.tensor.device();
121 let data = TensorData::new(disparity, [h, w]);
122 let tensor = Tensor::<B, 2>::from_data(data, &device);
123 Ok(tensor)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::test_helpers::{TestBackend, test_device};
131 use burn::tensor::TensorData;
132
133 #[test]
134 fn test_stereo_new_valid() {
135 let matcher = StereoBlockMatcher::new(7, 64).unwrap();
136 assert_eq!(matcher.block_size, 7);
137 assert_eq!(matcher.num_disparities, 64);
138 assert_eq!(matcher.min_disparity, 0);
139 }
140
141 #[test]
142 fn test_stereo_new_invalid_block_size() {
143 assert!(StereoBlockMatcher::new(4, 64).is_err());
144 assert!(StereoBlockMatcher::new(2, 64).is_err());
145 }
146
147 #[test]
148 fn test_stereo_new_invalid_disparities() {
149 assert!(StereoBlockMatcher::new(7, 0).is_err());
150 assert!(StereoBlockMatcher::new(7, 7).is_err());
151 }
152
153 #[test]
154 fn test_stereo_with_min_disparity() {
155 let matcher = StereoBlockMatcher::new(3, 32)
156 .unwrap()
157 .with_min_disparity(5);
158 assert_eq!(matcher.min_disparity, 5);
159 }
160
161 #[test]
162 fn test_stereo_compute_uniform() {
163 let device = test_device();
164 let w = 32;
165 let h = 16;
166 let flat = vec![0.5f32; h * w];
168 let left = Image::new(Tensor::<TestBackend, 3>::from_data(
169 TensorData::new(flat.clone(), [1, h, w]),
170 &device,
171 ));
172 let right = Image::new(Tensor::<TestBackend, 3>::from_data(
173 TensorData::new(flat, [1, h, w]),
174 &device,
175 ));
176
177 let matcher = StereoBlockMatcher::new(3, 16).unwrap();
178 let disp = matcher.compute(&left, &right).unwrap();
179
180 assert_eq!(disp.dims(), [h, w]);
181 let vals: Vec<f32> = disp.into_data().iter::<f32>().collect();
182 assert!(vals.iter().all(|&v| v.abs() < 1e-5));
183 }
184
185 #[test]
186 fn test_stereo_compute_shifted() {
187 let device = test_device();
188 let w = 48;
189 let h = 16;
190 let mut left_vals = vec![0.0f32; h * w];
191 let mut right_vals = vec![0.0f32; h * w];
192
193 for y in 0..h {
195 left_vals[y * w + 24] = 1.0;
196 }
197 for y in 0..h {
199 right_vals[y * w + 20] = 1.0;
200 }
201
202 let left = Image::new(Tensor::<TestBackend, 3>::from_data(
203 TensorData::new(left_vals, [1, h, w]),
204 &device,
205 ));
206 let right = Image::new(Tensor::<TestBackend, 3>::from_data(
207 TensorData::new(right_vals, [1, h, w]),
208 &device,
209 ));
210
211 let matcher = StereoBlockMatcher::new(3, 32).unwrap();
212 let disp = matcher.compute(&left, &right).unwrap();
213 let vals: Vec<f32> = disp.into_data().iter::<f32>().collect();
214
215 let center_disp = vals[(h / 2) * w + 24];
217 assert!(
218 (center_disp - 64.0).abs() < 1.0,
219 "Expected ~64.0, got {center_disp}"
220 );
221 }
222
223 #[test]
224 fn test_stereo_shape_mismatch() {
225 let device = test_device();
226 let left = Image::new(Tensor::<TestBackend, 3>::from_data(
227 TensorData::new(vec![0.5f32; 8 * 8], [1, 8, 8]),
228 &device,
229 ));
230 let right = Image::new(Tensor::<TestBackend, 3>::from_data(
231 TensorData::new(vec![0.5f32; 10 * 10], [1, 10, 10]),
232 &device,
233 ));
234
235 let matcher = StereoBlockMatcher::new(3, 16).unwrap();
236 assert!(matcher.compute(&left, &right).is_err());
237 }
238}