1use crate::error::{Result, VisionError};
17
18#[derive(Debug, Clone)]
24pub struct VosConfig {
25 pub n_memory_frames: usize,
27 pub feature_stride: usize,
29 pub similarity_temperature: f64,
31}
32
33impl Default for VosConfig {
34 fn default() -> Self {
35 Self {
36 n_memory_frames: 3,
37 feature_stride: 4,
38 similarity_temperature: 0.1,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
49pub struct FrameMask {
50 pub frame_idx: usize,
52 pub mask: Vec<Vec<bool>>,
55}
56
57impl FrameMask {
58 pub fn new(frame_idx: usize, mask: Vec<Vec<bool>>) -> Self {
60 Self { frame_idx, mask }
61 }
62
63 pub fn shape(&self) -> (usize, usize) {
65 let h = self.mask.len();
66 let w = if h > 0 { self.mask[0].len() } else { 0 };
67 (h, w)
68 }
69}
70
71#[derive(Debug, Clone)]
75pub struct FrameFeatures {
76 pub frame_idx: usize,
78 pub features: Vec<Vec<Vec<f64>>>,
81}
82
83impl FrameFeatures {
84 pub fn shape(&self) -> (usize, usize, usize) {
86 let fh = self.features.len();
87 let fw = if fh > 0 { self.features[0].len() } else { 0 };
88 let fd = if fh > 0 && fw > 0 {
89 self.features[0][0].len()
90 } else {
91 0
92 };
93 (fh, fw, fd)
94 }
95}
96
97pub fn extract_mask_features(
120 frame: &[Vec<[f64; 3]>],
121 mask: &[Vec<bool>],
122 stride: usize,
123) -> Result<FrameFeatures> {
124 if frame.is_empty() {
125 return Err(VisionError::InvalidParameter(
126 "frame must not be empty".into(),
127 ));
128 }
129 if stride == 0 {
130 return Err(VisionError::InvalidParameter("stride must be > 0".into()));
131 }
132
133 let h = frame.len();
134 let w = frame[0].len();
135 let fh = h.div_ceil(stride);
136 let fw = w.div_ceil(stride);
137
138 let mut feat_map: Vec<Vec<Vec<f64>>> = Vec::with_capacity(fh);
139
140 for fr in 0..fh {
141 let mut row_feats: Vec<Vec<f64>> = Vec::with_capacity(fw);
142 for fc in 0..fw {
143 let pr_start = fr * stride;
144 let pr_end = (pr_start + stride).min(h);
145 let pc_start = fc * stride;
146 let pc_end = (pc_start + stride).min(w);
147
148 let mut sum_r = 0.0_f64;
150 let mut sum_g = 0.0_f64;
151 let mut sum_b = 0.0_f64;
152 let mut mask_count = 0usize;
153 let mut total_count = 0usize;
154
155 let mid_r = pr_start + (pr_end - pr_start) / 2;
157 let mid_c = pc_start + (pc_end - pc_start) / 2;
158 let mut quad_mask = [0usize; 4];
159 let mut quad_total = [0usize; 4];
160
161 for (r, frame_row) in frame.iter().enumerate().take(pr_end).skip(pr_start) {
162 for (c, pixel) in frame_row.iter().enumerate().take(pc_end).skip(pc_start) {
163 let is_masked = mask
164 .get(r)
165 .and_then(|row| row.get(c))
166 .copied()
167 .unwrap_or(false);
168 total_count += 1;
169
170 let q = match (r < mid_r, c < mid_c) {
171 (true, true) => 0,
172 (true, false) => 1,
173 (false, true) => 2,
174 (false, false) => 3,
175 };
176 quad_total[q] += 1;
177
178 if is_masked {
179 sum_r += pixel[0];
180 sum_g += pixel[1];
181 sum_b += pixel[2];
182 mask_count += 1;
183 quad_mask[q] += 1;
184 }
185 }
186 }
187
188 let (mean_r, mean_g, mean_b) = if mask_count > 0 {
189 let n = mask_count as f64;
190 (sum_r / n, sum_g / n, sum_b / n)
191 } else if total_count > 0 {
192 let n = total_count as f64;
194 let (ar, ag, ab) = frame[pr_start..pr_end]
195 .iter()
196 .flat_map(|r| r[pc_start..pc_end].iter())
197 .fold((0.0_f64, 0.0_f64, 0.0_f64), |(ar, ag, ab), px| {
198 (ar + px[0], ag + px[1], ab + px[2])
199 });
200 (ar / n, ag / n, ab / n)
201 } else {
202 (0.0, 0.0, 0.0)
203 };
204
205 let pyramid: Vec<f64> = (0..4)
207 .map(|q| {
208 if quad_total[q] > 0 {
209 quad_mask[q] as f64 / quad_total[q] as f64
210 } else {
211 0.0
212 }
213 })
214 .collect();
215
216 let mut fv = vec![mean_r, mean_g, mean_b];
217 fv.extend_from_slice(&pyramid);
218 row_feats.push(fv);
219 }
220 feat_map.push(row_feats);
221 }
222
223 Ok(FrameFeatures {
224 frame_idx: 0,
225 features: feat_map,
226 })
227}
228
229fn dot(a: &[f64], b: &[f64]) -> f64 {
234 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
235}
236
237pub fn propagate_mask(
262 memory_frames: &[FrameMask],
263 memory_features: &[FrameFeatures],
264 query_features: &FrameFeatures,
265 config: &VosConfig,
266) -> Result<Vec<Vec<f64>>> {
267 if memory_frames.is_empty() || memory_features.is_empty() {
268 return Err(VisionError::InvalidParameter(
269 "memory_frames and memory_features must not be empty".into(),
270 ));
271 }
272 if memory_frames.len() != memory_features.len() {
273 return Err(VisionError::InvalidParameter(
274 "memory_frames and memory_features must have the same length".into(),
275 ));
276 }
277
278 let (qfh, qfw, _) = query_features.shape();
279 let temp = config.similarity_temperature;
280
281 let mut mem_cells: Vec<(Vec<f64>, f64)> = Vec::new();
284
285 for (mf, mfeat) in memory_frames.iter().zip(memory_features.iter()) {
286 let (mh, mw) = mf.shape();
287 let (fh, fw, _) = mfeat.shape();
288
289 let stride_r = if fh > 0 { mh.div_ceil(fh) } else { 1 };
290 let stride_c = if fw > 0 { mw.div_ceil(fw) } else { 1 };
291
292 for fr in 0..fh {
293 for fc in 0..fw {
294 let fv = mfeat.features[fr][fc].clone();
295
296 let pr_start = fr * stride_r;
298 let pr_end = (pr_start + stride_r).min(mh);
299 let pc_start = fc * stride_c;
300 let pc_end = (pc_start + stride_c).min(mw);
301
302 let mut fg_count = 0usize;
303 let mut total = 0usize;
304 for r in pr_start..pr_end {
305 for c in pc_start..pc_end {
306 total += 1;
307 if mf
308 .mask
309 .get(r)
310 .and_then(|row| row.get(c))
311 .copied()
312 .unwrap_or(false)
313 {
314 fg_count += 1;
315 }
316 }
317 }
318 let mask_val = if total > 0 {
319 fg_count as f64 / total as f64
320 } else {
321 0.0
322 };
323 mem_cells.push((fv, mask_val));
324 }
325 }
326 }
327
328 if mem_cells.is_empty() {
329 return Err(VisionError::InvalidParameter(
330 "No memory cells available".into(),
331 ));
332 }
333
334 let mut soft_feat: Vec<Vec<f64>> = vec![vec![0.0; qfw]; qfh];
336
337 for (qr, soft_feat_row) in soft_feat.iter_mut().enumerate().take(qfh) {
338 for (qc, soft_feat_val) in soft_feat_row.iter_mut().enumerate().take(qfw) {
339 let q = &query_features.features[qr][qc];
340
341 let logits: Vec<f64> = mem_cells.iter().map(|(mv, _)| dot(q, mv) / temp).collect();
343 let max_l = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
344 let exps: Vec<f64> = logits.iter().map(|&l| (l - max_l).exp()).collect();
345 let sum_exp: f64 = exps.iter().sum();
346
347 let soft = if sum_exp > 0.0 {
348 exps.iter()
349 .zip(mem_cells.iter())
350 .map(|(&e, (_, mv))| (e / sum_exp) * mv)
351 .sum::<f64>()
352 } else {
353 0.0_f64
354 };
355 *soft_feat_val = soft.clamp(0.0, 1.0);
356 }
357 }
358
359 let (orig_h, orig_w) = memory_frames[0].shape();
362 let out_h = if orig_h > 0 { orig_h } else { qfh };
363 let out_w = if orig_w > 0 { orig_w } else { qfw };
364 let stride_h = config.feature_stride.max(1);
365 let stride_w = config.feature_stride.max(1);
366
367 let mut soft_mask: Vec<Vec<f64>> = vec![vec![0.0; out_w]; out_h];
368 for (r, mask_row) in soft_mask.iter_mut().enumerate().take(out_h) {
369 for (c, mask_val) in mask_row.iter_mut().enumerate().take(out_w) {
370 let fr = (r / stride_h).min(qfh.saturating_sub(1));
371 let fc = (c / stride_w).min(qfw.saturating_sub(1));
372 *mask_val = soft_feat[fr][fc];
373 }
374 }
375
376 Ok(soft_mask)
377}
378
379pub fn threshold_mask(soft_mask: &[Vec<f64>], threshold: f64) -> Vec<Vec<bool>> {
387 soft_mask
388 .iter()
389 .map(|row| row.iter().map(|&v| v >= threshold).collect())
390 .collect()
391}
392
393pub fn mask_iou(pred: &[Vec<bool>], gt: &[Vec<bool>]) -> f64 {
398 let mut intersection = 0usize;
399 let mut union_ = 0usize;
400
401 let h = pred.len().min(gt.len());
402 for r in 0..h {
403 let w = pred[r].len().min(gt[r].len());
404 for c in 0..w {
405 let p = pred[r][c];
406 let g = gt[r][c];
407 if p && g {
408 intersection += 1;
409 }
410 if p || g {
411 union_ += 1;
412 }
413 }
414 }
415
416 if union_ == 0 {
417 1.0
418 } else {
419 intersection as f64 / union_ as f64
420 }
421}
422
423#[cfg(test)]
428mod tests {
429 use super::*;
430
431 fn make_frame(h: usize, w: usize) -> Vec<Vec<[f64; 3]>> {
432 (0..h)
433 .map(|r| {
434 (0..w)
435 .map(|c| [(r as f64) / (h as f64), (c as f64) / (w as f64), 0.5])
436 .collect()
437 })
438 .collect()
439 }
440
441 fn make_mask(h: usize, w: usize, fg_rows: std::ops::Range<usize>) -> Vec<Vec<bool>> {
442 (0..h)
443 .map(|r| (0..w).map(|_| fg_rows.contains(&r)).collect())
444 .collect()
445 }
446
447 #[test]
448 fn test_extract_mask_features_shape() {
449 let h = 16;
450 let w = 20;
451 let stride = 4;
452 let frame = make_frame(h, w);
453 let mask = make_mask(h, w, 4..12);
454 let ff = extract_mask_features(&frame, &mask, stride).expect("extraction failed");
455 let (fh, fw, fd) = ff.shape();
456 assert_eq!(fh, 4, "fh mismatch");
458 assert_eq!(fw, 5, "fw mismatch");
459 assert_eq!(fd, 7, "fd mismatch: expected 3 colour + 4 spatial = 7");
460 }
461
462 #[test]
463 fn test_extract_mask_features_zero_stride_error() {
464 let frame = make_frame(8, 8);
465 let mask = make_mask(8, 8, 0..4);
466 assert!(extract_mask_features(&frame, &mask, 0).is_err());
467 }
468
469 #[test]
470 fn test_extract_mask_features_empty_error() {
471 let mask: Vec<Vec<bool>> = Vec::new();
472 let frame: Vec<Vec<[f64; 3]>> = Vec::new();
473 assert!(extract_mask_features(&frame, &mask, 4).is_err());
474 }
475
476 #[test]
477 fn test_propagate_mask_soft_in_range() {
478 let h = 8;
479 let w = 8;
480 let stride = 2;
481 let frame = make_frame(h, w);
482 let mask_data = make_mask(h, w, 2..6);
483
484 let ff = extract_mask_features(&frame, &mask_data, stride).expect("extract failed");
485 let fm = FrameMask::new(0, mask_data);
486
487 let config = VosConfig {
488 n_memory_frames: 1,
489 feature_stride: stride,
490 similarity_temperature: 0.1,
491 };
492
493 let soft = propagate_mask(&[fm], std::slice::from_ref(&ff), &ff, &config)
494 .expect("propagation failed");
495
496 for row in &soft {
497 for &v in row {
498 assert!((0.0..=1.0).contains(&v), "soft value out of range: {v}");
499 }
500 }
501 }
502
503 #[test]
504 fn test_mask_iou_identical() {
505 let mask = make_mask(8, 8, 2..6);
506 let iou = mask_iou(&mask, &mask);
507 assert!((iou - 1.0).abs() < 1e-9, "iou = {iou}");
508 }
509
510 #[test]
511 fn test_mask_iou_disjoint() {
512 let pred = make_mask(8, 8, 0..4);
513 let gt = make_mask(8, 8, 4..8);
514 let iou = mask_iou(&pred, >);
515 assert_eq!(iou, 0.0, "iou = {iou}");
516 }
517
518 #[test]
519 fn test_mask_iou_all_background() {
520 let h = 4;
521 let w = 4;
522 let pred: Vec<Vec<bool>> = vec![vec![false; w]; h];
523 let gt: Vec<Vec<bool>> = vec![vec![false; w]; h];
524 let iou = mask_iou(&pred, >);
525 assert_eq!(iou, 1.0);
526 }
527
528 #[test]
529 fn test_threshold_mask() {
530 let soft: Vec<Vec<f64>> = vec![vec![0.3, 0.7], vec![0.5, 0.2]];
531 let binary = threshold_mask(&soft, 0.5);
532 assert!(!binary[0][0]);
533 assert!(binary[0][1]);
534 assert!(binary[1][0]);
535 assert!(!binary[1][1]);
536 }
537
538 #[test]
539 fn test_frame_mask_shape() {
540 let mask = make_mask(10, 12, 0..5);
541 let fm = FrameMask::new(3, mask);
542 assert_eq!(fm.shape(), (10, 12));
543 assert_eq!(fm.frame_idx, 3);
544 }
545}