1use crate::{Result, VisionError};
27use std::collections::VecDeque;
28use std::sync::{Arc, Mutex};
29use std::time::{Duration, Instant};
30use torsh_tensor::Tensor;
31
32#[derive(Debug, Clone)]
34pub struct StreamConfig {
35 pub buffer_size: usize,
37 pub target_fps: f32,
39 pub enable_frame_drop: bool,
41 pub use_gpu: bool,
43 pub num_threads: usize,
45 pub quality_adaptation: QualityAdaptation,
47}
48
49impl Default for StreamConfig {
50 fn default() -> Self {
51 Self {
52 buffer_size: 30,
53 target_fps: 30.0,
54 enable_frame_drop: true,
55 use_gpu: false,
56 num_threads: 4,
57 quality_adaptation: QualityAdaptation::None,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum QualityAdaptation {
65 None,
67 ResolutionScaling,
69 KeyframeOnly,
71 Dynamic,
73}
74
75#[derive(Debug, Clone)]
77pub struct FrameMetadata {
78 pub frame_number: u64,
80 pub timestamp: Instant,
82 pub width: usize,
84 pub height: usize,
86 pub is_keyframe: bool,
88 pub priority: u8,
90}
91
92#[derive(Debug, Clone)]
94pub struct Frame {
95 pub data: Tensor,
97 pub metadata: FrameMetadata,
99}
100
101#[derive(Debug, Clone)]
103pub struct StreamStats {
104 pub avg_processing_time: f32,
106 pub current_fps: f32,
108 pub dropped_frames: u64,
110 pub total_frames: u64,
112 pub buffer_utilization: f32,
114 pub num_adaptations: u64,
116}
117
118impl Default for StreamStats {
119 fn default() -> Self {
120 Self {
121 avg_processing_time: 0.0,
122 current_fps: 0.0,
123 dropped_frames: 0,
124 total_frames: 0,
125 buffer_utilization: 0.0,
126 num_adaptations: 0,
127 }
128 }
129}
130
131pub struct StreamProcessor {
133 config: StreamConfig,
134 stats: Arc<Mutex<StreamStats>>,
135 frame_buffer: Arc<Mutex<VecDeque<Frame>>>,
136 processing_times: Arc<Mutex<VecDeque<Duration>>>,
137}
138
139impl StreamProcessor {
140 pub fn new(config: StreamConfig) -> Result<Self> {
142 let buffer_size = config.buffer_size;
143 Ok(Self {
144 config,
145 stats: Arc::new(Mutex::new(StreamStats::default())),
146 frame_buffer: Arc::new(Mutex::new(VecDeque::with_capacity(buffer_size))),
147 processing_times: Arc::new(Mutex::new(VecDeque::with_capacity(100))),
148 })
149 }
150
151 pub fn stats(&self) -> StreamStats {
153 self.stats.lock().map(|s| s.clone()).unwrap_or_default()
154 }
155
156 pub fn reset_stats(&self) {
158 if let Ok(mut stats) = self.stats.lock() {
159 *stats = StreamStats::default();
160 }
161 if let Ok(mut times) = self.processing_times.lock() {
162 times.clear();
163 }
164 }
165
166 pub fn push_frame(&self, frame: Frame) -> Result<()> {
168 let mut buffer = self.frame_buffer.lock().map_err(|_| {
169 VisionError::InvalidParameter("Failed to lock frame buffer".to_string())
170 })?;
171
172 if buffer.len() >= self.config.buffer_size {
174 if self.config.enable_frame_drop {
175 let mut dropped = false;
177 for i in 0..buffer.len() {
178 if !buffer[i].metadata.is_keyframe {
179 buffer.remove(i);
180 dropped = true;
181
182 if let Ok(mut stats) = self.stats.lock() {
184 stats.dropped_frames += 1;
185 }
186 break;
187 }
188 }
189
190 if !dropped {
191 buffer.pop_front();
193 if let Ok(mut stats) = self.stats.lock() {
194 stats.dropped_frames += 1;
195 }
196 }
197 } else {
198 return Err(VisionError::InvalidParameter(
199 "Frame buffer full and dropping disabled".to_string(),
200 ));
201 }
202 }
203
204 buffer.push_back(frame);
205
206 if let Ok(mut stats) = self.stats.lock() {
208 stats.buffer_utilization = buffer.len() as f32 / self.config.buffer_size as f32;
209 }
210
211 Ok(())
212 }
213
214 pub fn pop_frame(&self) -> Result<Option<Frame>> {
216 let mut buffer = self.frame_buffer.lock().map_err(|_| {
217 VisionError::InvalidParameter("Failed to lock frame buffer".to_string())
218 })?;
219
220 Ok(buffer.pop_front())
221 }
222
223 pub fn record_processing_time(&self, duration: Duration) {
225 if let Ok(mut times) = self.processing_times.lock() {
226 times.push_back(duration);
227
228 while times.len() > 100 {
230 times.pop_front();
231 }
232
233 if let Ok(mut stats) = self.stats.lock() {
235 let sum: Duration = times.iter().sum();
237 stats.avg_processing_time = sum.as_secs_f32() * 1000.0 / times.len() as f32;
238
239 if stats.avg_processing_time > 0.0 {
241 stats.current_fps = 1000.0 / stats.avg_processing_time;
242 }
243 }
244 }
245 }
246
247 pub fn process_frame<F, T>(&self, frame: Frame, process_fn: F) -> Result<T>
249 where
250 F: FnOnce(&Frame) -> Result<T>,
251 {
252 let start = Instant::now();
253
254 let adapted_frame = self.adapt_frame_quality(&frame)?;
256
257 let result = process_fn(&adapted_frame)?;
259
260 let elapsed = start.elapsed();
262 self.record_processing_time(elapsed);
263
264 if let Ok(mut stats) = self.stats.lock() {
266 stats.total_frames += 1;
267 }
268
269 Ok(result)
270 }
271
272 fn adapt_frame_quality(&self, frame: &Frame) -> Result<Frame> {
274 match self.config.quality_adaptation {
275 QualityAdaptation::None => Ok(frame.clone()),
276
277 QualityAdaptation::ResolutionScaling => {
278 let stats = self.stats();
280 if stats.current_fps < self.config.target_fps * 0.8 {
281 let new_width = ((frame.metadata.width as f32 * 0.75) as usize).max(1);
283 let new_height = ((frame.metadata.height as f32 * 0.75) as usize).max(1);
284
285 let downscaled = downscale_frame_bilinear(frame, new_width, new_height)?;
286 if let Ok(mut stats) = self.stats.lock() {
287 stats.num_adaptations += 1;
288 }
289 return Ok(downscaled);
290 }
291 Ok(frame.clone())
292 }
293
294 QualityAdaptation::KeyframeOnly => {
295 let stats = self.stats();
297 if !frame.metadata.is_keyframe && stats.current_fps < self.config.target_fps * 0.9 {
298 if let Ok(mut stats_lock) = self.stats.lock() {
300 stats_lock.num_adaptations += 1;
301 }
302 }
303 Ok(frame.clone())
304 }
305
306 QualityAdaptation::Dynamic => {
307 let stats = self.stats();
309 let performance_ratio = stats.current_fps / self.config.target_fps;
310
311 if performance_ratio < 0.7 {
312 let new_width = ((frame.metadata.width as f32 * 0.5) as usize).max(1);
314 let new_height = ((frame.metadata.height as f32 * 0.5) as usize).max(1);
315
316 let downscaled = downscale_frame_bilinear(frame, new_width, new_height)?;
317 if let Ok(mut stats_lock) = self.stats.lock() {
318 stats_lock.num_adaptations += 1;
319 }
320 return Ok(downscaled);
321 } else if performance_ratio < 0.9 && !frame.metadata.is_keyframe {
322 if let Ok(mut stats_lock) = self.stats.lock() {
324 stats_lock.num_adaptations += 1;
325 }
326 }
327
328 Ok(frame.clone())
329 }
330 }
331 }
332
333 pub fn is_realtime(&self) -> bool {
335 let stats = self.stats();
336 stats.current_fps >= self.config.target_fps * 0.95
337 }
338
339 pub fn recommend_config_adjustments(&self) -> Vec<String> {
341 let stats = self.stats();
342 let mut recommendations = Vec::new();
343
344 if stats.current_fps < self.config.target_fps * 0.8 {
346 recommendations
347 .push("Consider reducing target_fps or enabling quality adaptation".to_string());
348 }
349
350 if stats.buffer_utilization > 0.9 {
352 recommendations.push("Buffer is frequently full - consider increasing buffer_size or enabling frame dropping".to_string());
353 }
354
355 if stats.total_frames > 0 {
357 let drop_rate = stats.dropped_frames as f32 / stats.total_frames as f32;
358 if drop_rate > 0.1 {
359 recommendations.push(format!(
360 "High frame drop rate ({:.1}%) - consider reducing input rate or optimizing processing",
361 drop_rate * 100.0
362 ));
363 }
364 }
365
366 recommendations
367 }
368}
369
370fn downscale_frame_bilinear(frame: &Frame, target_w: usize, target_h: usize) -> Result<Frame> {
383 let src_w = frame.metadata.width;
384 let src_h = frame.metadata.height;
385
386 if target_w == 0 || target_h == 0 {
387 return Err(VisionError::InvalidParameter(
388 "downscale target resolution must be non-zero".to_string(),
389 ));
390 }
391
392 if target_w == src_w && target_h == src_h {
394 return Ok(frame.clone());
395 }
396
397 let dims = frame.data.shape().dims().to_vec();
399 let channels = match dims.len() {
400 3 => dims[0],
401 2 => 1,
402 other => {
403 return Err(VisionError::InvalidShape(format!(
404 "bilinear downscale expects a 2D (HW) or 3D (CHW) frame tensor, got rank {other}"
405 )));
406 }
407 };
408
409 let src_data: Vec<f32> = frame.data.to_vec()?;
410 let expected = channels * src_h * src_w;
411 if src_data.len() != expected {
412 return Err(VisionError::InvalidShape(format!(
413 "frame tensor has {} elements but metadata implies {expected} ({channels}x{src_h}x{src_w})",
414 src_data.len()
415 )));
416 }
417
418 let scale_y = src_h as f32 / target_h as f32;
419 let scale_x = src_w as f32 / target_w as f32;
420 let src_h_max = src_h as isize - 1;
421 let src_w_max = src_w as isize - 1;
422
423 let mut out = vec![0.0f32; channels * target_h * target_w];
424
425 for c in 0..channels {
426 let src_plane = c * src_h * src_w;
427 let dst_plane = c * target_h * target_w;
428 for oy in 0..target_h {
429 let fy = (oy as f32 + 0.5) * scale_y - 0.5;
430 let y_floor = fy.floor();
431 let wy = fy - y_floor;
432 let y0 = (y_floor as isize).clamp(0, src_h_max) as usize;
433 let y1 = (y_floor as isize + 1).clamp(0, src_h_max) as usize;
434 let row0 = src_plane + y0 * src_w;
435 let row1 = src_plane + y1 * src_w;
436 for ox in 0..target_w {
437 let fx = (ox as f32 + 0.5) * scale_x - 0.5;
438 let x_floor = fx.floor();
439 let wx = fx - x_floor;
440 let x0 = (x_floor as isize).clamp(0, src_w_max) as usize;
441 let x1 = (x_floor as isize + 1).clamp(0, src_w_max) as usize;
442
443 let v00 = src_data[row0 + x0];
445 let v01 = src_data[row0 + x1];
446 let v10 = src_data[row1 + x0];
447 let v11 = src_data[row1 + x1];
448 let top = v00 + (v01 - v00) * wx;
449 let bot = v10 + (v11 - v10) * wx;
450 out[dst_plane + oy * target_w + ox] = top + (bot - top) * wy;
451 }
452 }
453 }
454
455 let new_shape = if dims.len() == 3 {
456 vec![channels, target_h, target_w]
457 } else {
458 vec![target_h, target_w]
459 };
460 let data = Tensor::from_vec(out, &new_shape)?;
461
462 let mut metadata = frame.metadata.clone();
463 metadata.width = target_w;
464 metadata.height = target_h;
465
466 Ok(Frame { data, metadata })
467}
468
469pub struct FramePreprocessor {
471 pub target_size: Option<(usize, usize)>,
473 pub normalize_mean: Option<Vec<f32>>,
475 pub normalize_std: Option<Vec<f32>>,
477 pub to_grayscale: bool,
479}
480
481impl Default for FramePreprocessor {
482 fn default() -> Self {
483 Self {
484 target_size: None,
485 normalize_mean: None,
486 normalize_std: None,
487 to_grayscale: false,
488 }
489 }
490}
491
492impl FramePreprocessor {
493 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn with_resize(mut self, width: usize, height: usize) -> Self {
500 self.target_size = Some((width, height));
501 self
502 }
503
504 pub fn with_normalize(mut self, mean: Vec<f32>, std: Vec<f32>) -> Self {
506 self.normalize_mean = Some(mean);
507 self.normalize_std = Some(std);
508 self
509 }
510
511 pub fn with_grayscale(mut self) -> Self {
513 self.to_grayscale = true;
514 self
515 }
516
517 pub fn preprocess(&self, frame: &Frame) -> Result<Frame> {
524 let mut data = frame.data.clone();
525 let mut width = frame.metadata.width;
526 let mut height = frame.metadata.height;
527
528 if let Some((target_w, target_h)) = self.target_size {
530 if target_w != width || target_h != height {
531 let orig_data: Vec<f32> = data.to_vec().map_err(|e| VisionError::TensorError(e))?;
532 let shape = data.shape();
533 let dims = shape.dims();
534
535 let channels = if dims.len() == 3 {
537 dims[0] } else {
539 1
540 };
541
542 let mut resized = vec![0.0f32; channels * target_h * target_w];
543 let scale_y = height as f32 / target_h as f32;
544 let scale_x = width as f32 / target_w as f32;
545
546 for c in 0..channels {
547 for oy in 0..target_h {
548 for ox in 0..target_w {
549 let src_y = ((oy as f32 * scale_y) as usize).min(height - 1);
550 let src_x = ((ox as f32 * scale_x) as usize).min(width - 1);
551 let src_idx = c * height * width + src_y * width + src_x;
552 let dst_idx = c * target_h * target_w + oy * target_w + ox;
553 resized[dst_idx] = orig_data[src_idx];
554 }
555 }
556 }
557
558 let new_shape = if dims.len() == 3 {
559 vec![channels, target_h, target_w]
560 } else {
561 vec![target_h, target_w]
562 };
563 data = Tensor::from_vec(resized, &new_shape)
564 .map_err(|e| VisionError::TensorError(e))?;
565 width = target_w;
566 height = target_h;
567 }
568 }
569
570 if let (Some(means), Some(stds)) = (&self.normalize_mean, &self.normalize_std) {
572 let shape = data.shape();
573 let dims = shape.dims();
574 let channels = if dims.len() == 3 { dims[0] } else { 1 };
575 let mut pixel_data: Vec<f32> =
576 data.to_vec().map_err(|e| VisionError::TensorError(e))?;
577 let pixels_per_channel = height * width;
578
579 for c in 0..channels.min(means.len()).min(stds.len()) {
580 let mean = means[c];
581 let std = stds[c].max(1e-7);
582 let offset = c * pixels_per_channel;
583 for i in 0..pixels_per_channel {
584 pixel_data[offset + i] = (pixel_data[offset + i] - mean) / std;
585 }
586 }
587
588 let shape_vec = dims.to_vec();
589 data = Tensor::from_vec(pixel_data, &shape_vec)
590 .map_err(|e| VisionError::TensorError(e))?;
591 }
592
593 if self.to_grayscale {
595 let shape = data.shape();
596 let dims = shape.dims();
597 if dims.len() == 3 && dims[0] >= 3 {
598 let pixel_data: Vec<f32> =
599 data.to_vec().map_err(|e| VisionError::TensorError(e))?;
600 let pixels = height * width;
601 let mut gray = vec![0.0f32; pixels];
602 for i in 0..pixels {
604 let r = pixel_data[i];
605 let g = pixel_data[pixels + i];
606 let b = pixel_data[2 * pixels + i];
607 gray[i] = 0.299 * r + 0.587 * g + 0.114 * b;
608 }
609 data = Tensor::from_vec(gray, &[1, height, width])
610 .map_err(|e| VisionError::TensorError(e))?;
611 }
612 }
613
614 let mut metadata = frame.metadata.clone();
615 metadata.width = width;
616 metadata.height = height;
617
618 Ok(Frame { data, metadata })
619 }
620}
621
622pub struct BatchProcessor {
624 batch_size: usize,
625 frames: Vec<Frame>,
626}
627
628impl BatchProcessor {
629 pub fn new(batch_size: usize) -> Self {
631 Self {
632 batch_size,
633 frames: Vec::with_capacity(batch_size),
634 }
635 }
636
637 pub fn add_frame(&mut self, frame: Frame) -> Option<Vec<Frame>> {
639 self.frames.push(frame);
640
641 if self.frames.len() >= self.batch_size {
642 Some(self.flush())
643 } else {
644 None
645 }
646 }
647
648 pub fn current_batch(&self) -> &[Frame] {
650 &self.frames
651 }
652
653 pub fn flush(&mut self) -> Vec<Frame> {
655 std::mem::replace(&mut self.frames, Vec::with_capacity(self.batch_size))
656 }
657
658 pub fn is_full(&self) -> bool {
660 self.frames.len() >= self.batch_size
661 }
662
663 pub fn len(&self) -> usize {
665 self.frames.len()
666 }
667
668 pub fn is_empty(&self) -> bool {
670 self.frames.is_empty()
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677
678 fn create_dummy_frame(frame_number: u64) -> Frame {
679 use torsh_core::DeviceType;
680 Frame {
681 data: Tensor::zeros(&[224, 224, 3], DeviceType::Cpu).expect("Failed to create tensor"),
682 metadata: FrameMetadata {
683 frame_number,
684 timestamp: Instant::now(),
685 width: 224,
686 height: 224,
687 is_keyframe: frame_number % 10 == 0,
688 priority: 1,
689 },
690 }
691 }
692
693 #[test]
694 fn test_stream_config_default() {
695 let config = StreamConfig::default();
696 assert_eq!(config.buffer_size, 30);
697 assert_eq!(config.target_fps, 30.0);
698 assert!(config.enable_frame_drop);
699 }
700
701 #[test]
702 fn test_stream_processor_creation() {
703 let processor = StreamProcessor::new(StreamConfig::default());
704 assert!(processor.is_ok());
705 }
706
707 #[test]
708 fn test_push_pop_frame() {
709 let processor =
710 StreamProcessor::new(StreamConfig::default()).expect("Failed to create processor");
711
712 let frame = create_dummy_frame(1);
713 processor
714 .push_frame(frame.clone())
715 .expect("Failed to push frame");
716
717 let popped = processor.pop_frame().expect("Failed to pop frame");
718 assert!(popped.is_some());
719 assert_eq!(popped.unwrap().metadata.frame_number, 1);
720 }
721
722 #[test]
723 fn test_frame_dropping() {
724 let mut config = StreamConfig::default();
725 config.buffer_size = 2;
726 config.enable_frame_drop = true;
727
728 let processor = StreamProcessor::new(config).expect("Failed to create processor");
729
730 for i in 0..5 {
732 let frame = create_dummy_frame(i);
733 processor.push_frame(frame).expect("Failed to push frame");
734 }
735
736 let stats = processor.stats();
737 assert!(stats.dropped_frames > 0);
738 }
739
740 #[test]
741 fn test_processing_time_recording() {
742 let processor =
743 StreamProcessor::new(StreamConfig::default()).expect("Failed to create processor");
744
745 processor.record_processing_time(Duration::from_millis(10));
746 processor.record_processing_time(Duration::from_millis(20));
747
748 let stats = processor.stats();
749 assert!(stats.avg_processing_time > 0.0);
750 }
751
752 #[test]
753 fn test_batch_processor() {
754 let mut batch = BatchProcessor::new(3);
755
756 assert!(batch.is_empty());
757 assert!(!batch.is_full());
758
759 batch.add_frame(create_dummy_frame(1));
760 batch.add_frame(create_dummy_frame(2));
761
762 assert_eq!(batch.len(), 2);
763 assert!(!batch.is_full());
764
765 let result = batch.add_frame(create_dummy_frame(3));
766 assert!(result.is_some());
767 assert_eq!(result.unwrap().len(), 3);
768 assert!(batch.is_empty());
769 }
770
771 #[test]
772 fn test_frame_preprocessor() {
773 let preprocessor = FramePreprocessor::new()
774 .with_resize(224, 224)
775 .with_grayscale();
776
777 let frame = create_dummy_frame(1);
778 let result = preprocessor.preprocess(&frame);
779 assert!(result.is_ok());
780 }
781
782 #[test]
783 fn test_quality_adaptation_variants() {
784 assert_eq!(QualityAdaptation::None, QualityAdaptation::None);
785 assert_ne!(
786 QualityAdaptation::None,
787 QualityAdaptation::ResolutionScaling
788 );
789 }
790
791 #[test]
792 fn test_is_realtime() {
793 let processor =
794 StreamProcessor::new(StreamConfig::default()).expect("Failed to create processor");
795
796 processor.record_processing_time(Duration::from_millis(10));
799 let is_rt = processor.is_realtime();
800 assert!(is_rt); }
803
804 #[test]
805 fn test_stream_stats_default() {
806 let stats = StreamStats::default();
807 assert_eq!(stats.total_frames, 0);
808 assert_eq!(stats.dropped_frames, 0);
809 assert_eq!(stats.current_fps, 0.0);
810 }
811
812 #[test]
813 fn test_frame_preprocessor_resize() {
814 let data: Vec<f32> = (0..48).map(|x| x as f32).collect();
816 let tensor = Tensor::from_vec(data, &[3, 4, 4]).expect("tensor creation should succeed");
817
818 let frame = Frame {
819 data: tensor,
820 metadata: FrameMetadata {
821 frame_number: 0,
822 timestamp: Instant::now(),
823 width: 4,
824 height: 4,
825 is_keyframe: true,
826 priority: 1,
827 },
828 };
829
830 let preprocessor = FramePreprocessor::new().with_resize(2, 2);
831 let result = preprocessor
832 .preprocess(&frame)
833 .expect("resize should succeed");
834 assert_eq!(result.metadata.width, 2);
835 assert_eq!(result.metadata.height, 2);
836 let shape = result.data.shape();
837 let dims = shape.dims();
838 assert_eq!(dims, &[3, 2, 2]);
839 }
840
841 #[test]
842 fn test_frame_preprocessor_normalize() {
843 let data = vec![128.0f32; 3 * 4 * 4];
845 let tensor = Tensor::from_vec(data, &[3, 4, 4]).expect("tensor creation should succeed");
846
847 let frame = Frame {
848 data: tensor,
849 metadata: FrameMetadata {
850 frame_number: 0,
851 timestamp: Instant::now(),
852 width: 4,
853 height: 4,
854 is_keyframe: true,
855 priority: 1,
856 },
857 };
858
859 let preprocessor = FramePreprocessor::new()
861 .with_normalize(vec![128.0, 128.0, 128.0], vec![128.0, 128.0, 128.0]);
862 let result = preprocessor
863 .preprocess(&frame)
864 .expect("normalize should succeed");
865 let vals: Vec<f32> = result.data.to_vec().expect("to_vec should succeed");
866 for &v in &vals {
867 assert!(v.abs() < 1e-5, "Expected ~0 after normalization, got {v}");
868 }
869 }
870
871 #[test]
872 fn test_frame_preprocessor_grayscale() {
873 let mut data = vec![0.0f32; 3 * 4 * 4];
875 for i in 0..(4 * 4) {
877 data[i] = 1.0;
878 }
879 let tensor = Tensor::from_vec(data, &[3, 4, 4]).expect("tensor creation should succeed");
880
881 let frame = Frame {
882 data: tensor,
883 metadata: FrameMetadata {
884 frame_number: 0,
885 timestamp: Instant::now(),
886 width: 4,
887 height: 4,
888 is_keyframe: true,
889 priority: 1,
890 },
891 };
892
893 let preprocessor = FramePreprocessor::new().with_grayscale();
894 let result = preprocessor
895 .preprocess(&frame)
896 .expect("grayscale should succeed");
897
898 let shape = result.data.shape();
899 let dims = shape.dims();
900 assert_eq!(dims, &[1, 4, 4]);
901
902 let vals: Vec<f32> = result.data.to_vec().expect("to_vec should succeed");
903 for &v in &vals {
904 assert!(
905 (v - 0.299).abs() < 1e-5,
906 "Expected 0.299 luminance for pure red, got {v}"
907 );
908 }
909 }
910
911 #[test]
912 fn test_downscale_bilinear_known_values() {
913 let data: Vec<f32> = (0..16).map(|x| x as f32).collect();
919 let tensor = Tensor::from_vec(data, &[1, 4, 4]).expect("tensor creation should succeed");
920
921 let frame = Frame {
922 data: tensor,
923 metadata: FrameMetadata {
924 frame_number: 0,
925 timestamp: Instant::now(),
926 width: 4,
927 height: 4,
928 is_keyframe: true,
929 priority: 1,
930 },
931 };
932
933 let result = downscale_frame_bilinear(&frame, 2, 2).expect("downscale should succeed");
934
935 assert_eq!(result.metadata.width, 2);
937 assert_eq!(result.metadata.height, 2);
938 let shape = result.data.shape();
939 assert_eq!(shape.dims(), &[1, 2, 2]);
940
941 let expected = [2.5f32, 4.5, 10.5, 12.5];
947 let vals: Vec<f32> = result.data.to_vec().expect("to_vec should succeed");
948 assert_eq!(vals.len(), expected.len());
949 for (got, want) in vals.iter().zip(expected.iter()) {
950 assert!(
951 (got - want).abs() < 1e-6,
952 "bilinear downscale mismatch: got {got}, expected {want}"
953 );
954 }
955 }
956
957 #[test]
958 fn test_downscale_bilinear_rejects_bad_rank() {
959 let tensor = Tensor::from_vec(vec![0.0f32; 4], &[4]).expect("tensor creation");
962 let frame = Frame {
963 data: tensor,
964 metadata: FrameMetadata {
965 frame_number: 0,
966 timestamp: Instant::now(),
967 width: 4,
968 height: 1,
969 is_keyframe: true,
970 priority: 1,
971 },
972 };
973 assert!(downscale_frame_bilinear(&frame, 2, 1).is_err());
974 }
975
976 #[test]
977 fn test_resolution_scaling_triggers_real_downscale() {
978 let config = StreamConfig {
981 quality_adaptation: QualityAdaptation::ResolutionScaling,
982 ..Default::default()
983 };
984 let processor = StreamProcessor::new(config).expect("processor creation");
985
986 let data: Vec<f32> = (0..64).map(|x| x as f32).collect();
987 let tensor = Tensor::from_vec(data, &[1, 8, 8]).expect("tensor creation");
988 let frame = Frame {
989 data: tensor,
990 metadata: FrameMetadata {
991 frame_number: 0,
992 timestamp: Instant::now(),
993 width: 8,
994 height: 8,
995 is_keyframe: true,
996 priority: 1,
997 },
998 };
999
1000 let adapted = processor
1002 .process_frame(frame, |f| Ok(f.clone()))
1003 .expect("process_frame should succeed");
1004
1005 assert_eq!(adapted.metadata.width, 6);
1006 assert_eq!(adapted.metadata.height, 6);
1007 let shape = adapted.data.shape();
1008 assert_eq!(shape.dims(), &[1, 6, 6]);
1009 assert_eq!(processor.stats().num_adaptations, 1);
1010 }
1011}