1use crate::error::{Result, VisionError};
16use crate::optical_flow_dense::warp_image;
17use scirs2_core::ndarray::{Array2, Array3};
18use std::collections::VecDeque;
19
20#[derive(Debug, Clone)]
26pub struct VideoFrame {
27 pub data: Array3<f64>,
29 pub timestamp: f64,
31 pub width: usize,
33 pub height: usize,
35}
36
37impl VideoFrame {
38 pub fn new(data: Array3<f64>, timestamp: f64) -> Result<Self> {
45 let shape = data.dim();
46 let height = shape.0;
47 let width = shape.1;
48 Ok(Self {
49 data,
50 timestamp,
51 width,
52 height,
53 })
54 }
55
56 pub fn channels(&self) -> usize {
58 self.data.dim().2
59 }
60
61 pub fn channel(&self, ch: usize) -> Result<Array2<f64>> {
63 if ch >= self.channels() {
64 return Err(VisionError::InvalidParameter(format!(
65 "channel index {ch} out of range (frame has {} channels)",
66 self.channels()
67 )));
68 }
69 Ok(self
70 .data
71 .slice(scirs2_core::ndarray::s![.., .., ch])
72 .to_owned())
73 }
74
75 pub fn to_grayscale(&self) -> Array2<f64> {
77 let (h, w, c) = self.data.dim();
78 let mut gray = Array2::<f64>::zeros((h, w));
79 let weight = 1.0 / c as f64;
80 for ch in 0..c {
81 for r in 0..h {
82 for col in 0..w {
83 gray[[r, col]] += self.data[[r, col, ch]] * weight;
84 }
85 }
86 }
87 gray
88 }
89}
90
91#[derive(Debug, Clone)]
99pub struct FrameBuffer {
100 pub frames: VecDeque<VideoFrame>,
102 pub capacity: usize,
104}
105
106impl FrameBuffer {
107 pub fn new(capacity: usize) -> Result<Self> {
109 if capacity == 0 {
110 return Err(VisionError::InvalidParameter(
111 "FrameBuffer: capacity must be at least 1".into(),
112 ));
113 }
114 Ok(Self {
115 frames: VecDeque::with_capacity(capacity),
116 capacity,
117 })
118 }
119
120 pub fn push(&mut self, frame: VideoFrame) {
123 if self.frames.len() == self.capacity {
124 self.frames.pop_front();
125 }
126 self.frames.push_back(frame);
127 }
128
129 pub fn len(&self) -> usize {
131 self.frames.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
136 self.frames.is_empty()
137 }
138
139 pub fn is_full(&self) -> bool {
141 self.frames.len() == self.capacity
142 }
143
144 pub fn latest(&self) -> Option<&VideoFrame> {
146 self.frames.back()
147 }
148
149 pub fn iter(&self) -> impl Iterator<Item = &VideoFrame> {
151 self.frames.iter()
152 }
153}
154
155pub fn temporal_median_filter(frames: &[Array3<f64>], window: usize) -> Result<Array3<f64>> {
170 if frames.is_empty() {
171 return Err(VisionError::InvalidParameter(
172 "temporal_median_filter: frames slice must not be empty".into(),
173 ));
174 }
175 let window = window.min(frames.len()).max(1);
176 let ref_shape = frames[0].dim();
177 for (i, f) in frames.iter().enumerate() {
178 if f.dim() != ref_shape {
179 return Err(VisionError::DimensionMismatch(format!(
180 "temporal_median_filter: frame {i} shape {:?} != reference {:?}",
181 f.dim(),
182 ref_shape
183 )));
184 }
185 }
186
187 let (h, w, c) = ref_shape;
188 let start = frames.len().saturating_sub(window);
189 let window_frames = &frames[start..];
190 let n = window_frames.len();
191 let mut output = Array3::<f64>::zeros((h, w, c));
192
193 for row in 0..h {
194 for col in 0..w {
195 for ch in 0..c {
196 let mut vals: Vec<f64> = window_frames.iter().map(|f| f[[row, col, ch]]).collect();
197 vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
198 output[[row, col, ch]] = if n % 2 == 1 {
199 vals[n / 2]
200 } else {
201 (vals[n / 2 - 1] + vals[n / 2]) * 0.5
202 };
203 }
204 }
205 }
206
207 Ok(output)
208}
209
210#[derive(Debug, Clone)]
216pub struct MogBackground {
217 pub n_gaussians: usize,
219 pub learning_rate: f64,
221 pub threshold: f64,
223 means: Option<Array3<f64>>,
225 variances: Option<Array3<f64>>,
226 weights: Option<Array3<f64>>,
227}
228
229impl MogBackground {
230 pub fn new(n_gaussians: usize, learning_rate: f64, threshold: f64) -> Result<Self> {
234 if n_gaussians == 0 {
235 return Err(VisionError::InvalidParameter(
236 "MogBackground: n_gaussians must be at least 1".into(),
237 ));
238 }
239 if !(0.0..=1.0).contains(&learning_rate) {
240 return Err(VisionError::InvalidParameter(
241 "MogBackground: learning_rate must be in (0, 1]".into(),
242 ));
243 }
244 Ok(Self {
245 n_gaussians,
246 learning_rate,
247 threshold,
248 means: None,
249 variances: None,
250 weights: None,
251 })
252 }
253}
254
255impl Default for MogBackground {
256 fn default() -> Self {
257 Self {
258 n_gaussians: 3,
259 learning_rate: 0.005,
260 threshold: 2.5,
261 means: None,
262 variances: None,
263 weights: None,
264 }
265 }
266}
267
268pub fn background_subtraction_mog(
275 frame: &Array2<f64>,
276 background_model: &mut MogBackground,
277) -> Result<Array2<bool>> {
278 let (rows, cols) = frame.dim();
279 let k = background_model.n_gaussians;
280 let alpha = background_model.learning_rate;
281 let thr = background_model.threshold;
282
283 if background_model.means.is_none() {
285 let init_weight = 1.0 / k as f64;
288 let mut means = Array3::<f64>::zeros((rows, cols, k));
289 let variances = Array3::<f64>::from_elem((rows, cols, k), 0.01);
290 let weights = Array3::<f64>::from_elem((rows, cols, k), init_weight);
291 for r in 0..rows {
293 for c in 0..cols {
294 for ki in 0..k {
295 means[[r, c, ki]] = frame[[r, c]];
296 }
297 }
298 }
299 background_model.means = Some(means);
300 background_model.variances = Some(variances);
301 background_model.weights = Some(weights);
302 }
303
304 let means = background_model.means.as_mut().expect("means initialised");
305 let variances = background_model
306 .variances
307 .as_mut()
308 .expect("variances initialised");
309 let weights = background_model
310 .weights
311 .as_mut()
312 .expect("weights initialised");
313
314 let mut fg_mask = Array2::<bool>::from_elem((rows, cols), false);
315
316 for r in 0..rows {
317 for c in 0..cols {
318 let pixel = frame[[r, c]];
319 let mut matched = false;
320 let mut best_ki = 0usize;
321
322 for ki in 0..k {
324 let diff = pixel - means[[r, c, ki]];
325 let var = variances[[r, c, ki]];
326 if var > 1e-12 && diff * diff / var < thr * thr {
327 let rho = alpha / weights[[r, c, ki]].max(1e-12);
329 means[[r, c, ki]] += rho * diff;
330 variances[[r, c, ki]] = (1.0 - rho) * var + rho * diff * diff;
331 weights[[r, c, ki]] = (1.0 - alpha) * weights[[r, c, ki]] + alpha;
332
333 matched = true;
334 best_ki = ki;
335 break;
336 }
337 }
338
339 if !matched {
340 let mut min_w = weights[[r, c, 0]];
342 let mut min_ki = 0;
343 for ki in 1..k {
344 if weights[[r, c, ki]] < min_w {
345 min_w = weights[[r, c, ki]];
346 min_ki = ki;
347 }
348 }
349 means[[r, c, min_ki]] = pixel;
350 variances[[r, c, min_ki]] = 0.01;
351 weights[[r, c, min_ki]] = alpha;
352 best_ki = min_ki;
353 }
354
355 let mut w_sum = 0.0_f64;
357 for ki in 0..k {
358 w_sum += weights[[r, c, ki]];
359 }
360 if w_sum > 1e-12 {
361 for ki in 0..k {
362 weights[[r, c, ki]] /= w_sum;
363 }
364 }
365
366 let is_bg = matched && weights[[r, c, best_ki]] > 1.0 / k as f64;
369 fg_mask[[r, c]] = !is_bg;
370 }
371 }
372
373 Ok(fg_mask)
374}
375
376pub fn frame_interpolation(
393 frame1: &Array3<f64>,
394 frame2: &Array3<f64>,
395 t: f64,
396 flow: (&Array2<f64>, &Array2<f64>),
397) -> Result<Array3<f64>> {
398 let shape = frame1.dim();
399 if shape != frame2.dim() {
400 return Err(VisionError::DimensionMismatch(
401 "frame_interpolation: frame1 and frame2 must have identical shapes".into(),
402 ));
403 }
404 let (u, v) = flow;
405 let (h, w, _) = shape;
406 if u.dim() != (h, w) || v.dim() != (h, w) {
407 return Err(VisionError::DimensionMismatch(
408 "frame_interpolation: flow field spatial shape must match frame spatial shape".into(),
409 ));
410 }
411 if !(0.0..=1.0).contains(&t) {
412 return Err(VisionError::InvalidParameter(
413 "frame_interpolation: t must be in [0, 1]".into(),
414 ));
415 }
416
417 let (rows, cols, channels) = shape;
418
419 let u_fwd = u.mapv(|x| x * t);
421 let v_fwd = v.mapv(|x| x * t);
422 let u_bwd = u.mapv(|x| -x * (1.0 - t));
424 let v_bwd = v.mapv(|x| -x * (1.0 - t));
425
426 let mut output = Array3::<f64>::zeros((rows, cols, channels));
427
428 for ch in 0..channels {
429 let ch1 = frame1
430 .slice(scirs2_core::ndarray::s![.., .., ch])
431 .to_owned();
432 let ch2 = frame2
433 .slice(scirs2_core::ndarray::s![.., .., ch])
434 .to_owned();
435
436 let warped1 = warp_image(&ch1, &u_fwd, &v_fwd)?;
437 let warped2 = warp_image(&ch2, &u_bwd, &v_bwd)?;
438
439 for r in 0..rows {
440 for c in 0..cols {
441 output[[r, c, ch]] = (1.0 - t) * warped1[[r, c]] + t * warped2[[r, c]];
442 }
443 }
444 }
445
446 Ok(output)
447}
448
449#[cfg(test)]
454mod tests {
455 use super::*;
456 use scirs2_core::ndarray::{Array2, Array3};
457
458 fn rgb_frame(h: usize, w: usize, val: f64) -> Array3<f64> {
459 Array3::from_elem((h, w, 3), val)
460 }
461
462 #[test]
463 fn frame_buffer_circular_eviction() {
464 let mut buf = FrameBuffer::new(3).expect("FrameBuffer::new failed");
465 for i in 0..5u32 {
466 let data = rgb_frame(4, 4, i as f64 / 10.0);
467 let frame = VideoFrame::new(data, i as f64).expect("VideoFrame::new failed");
468 buf.push(frame);
469 }
470 assert_eq!(buf.len(), 3);
471 assert!((buf.frames[0].timestamp - 2.0).abs() < 1e-9);
473 }
474
475 #[test]
476 fn temporal_median_filter_single_frame() {
477 let f = rgb_frame(4, 4, 0.7);
478 let out =
479 temporal_median_filter(std::slice::from_ref(&f), 1).expect("median filter failed");
480 for &v in out.iter() {
481 assert!((v - 0.7).abs() < 1e-10);
482 }
483 }
484
485 #[test]
486 fn temporal_median_filter_three_frames() {
487 let f1 = rgb_frame(2, 2, 0.2);
489 let f2 = rgb_frame(2, 2, 0.5);
490 let f3 = rgb_frame(2, 2, 0.8);
491 let out = temporal_median_filter(&[f1, f2, f3], 3).expect("median filter failed");
492 for &v in out.iter() {
493 assert!((v - 0.5).abs() < 1e-10, "expected 0.5, got {v}");
494 }
495 }
496
497 #[test]
498 fn mog_background_first_frame_all_foreground_zero() {
499 let frame = Array2::from_elem((4, 4), 0.5_f64);
502 let mut model = MogBackground::new(3, 0.005, 2.5).expect("MogBackground::new failed");
503 let mask = background_subtraction_mog(&frame, &mut model).expect("mog failed");
504 assert_eq!(mask.dim(), (4, 4));
508 }
509
510 #[test]
511 fn mog_background_converges_to_background() {
512 let frame = Array2::from_elem((4, 4), 0.5_f64);
514 let mut model = MogBackground::new(3, 0.1, 2.5).expect("MogBackground::new failed");
515 for _ in 0..50 {
516 let _ = background_subtraction_mog(&frame, &mut model);
517 }
518 let mask = background_subtraction_mog(&frame, &mut model).expect("mog failed");
519 for val in mask.iter() {
521 assert!(!val, "expected background, got foreground");
522 }
523 }
524
525 #[test]
526 fn frame_interpolation_at_zero_returns_frame1() {
527 let f1 = rgb_frame(4, 4, 0.2);
528 let f2 = rgb_frame(4, 4, 0.8);
529 let u = Array2::zeros((4, 4));
530 let v = Array2::zeros((4, 4));
531 let out = frame_interpolation(&f1, &f2, 0.0, (&u, &v)).expect("interpolation failed");
532 for &val in out.iter() {
533 assert!((val - 0.2).abs() < 1e-10, "expected 0.2, got {val}");
534 }
535 }
536
537 #[test]
538 fn frame_interpolation_at_one_returns_frame2() {
539 let f1 = rgb_frame(4, 4, 0.2);
540 let f2 = rgb_frame(4, 4, 0.8);
541 let u = Array2::zeros((4, 4));
542 let v = Array2::zeros((4, 4));
543 let out = frame_interpolation(&f1, &f2, 1.0, (&u, &v)).expect("interpolation failed");
544 for &val in out.iter() {
545 assert!((val - 0.8).abs() < 1e-10, "expected 0.8, got {val}");
546 }
547 }
548}