Skip to main content

torsh_vision/
streaming.rs

1//! Real-Time Video Stream Processing
2//!
3//! This module provides efficient stream processing capabilities for real-time computer vision,
4//! integrated with scirs2-vision 0.1.5's streaming features.
5//!
6//! # Features
7//! - Video frame buffering and preprocessing pipelines
8//! - Real-time performance monitoring and adaptation
9//! - Async/parallel processing for low latency
10//! - GPU-accelerated stream processing
11//! - Frame dropping and quality adaptation strategies
12//!
13//! # Examples
14//!
15//! ```rust,ignore
16//! use torsh_vision::streaming::*;
17//!
18//! // Create a video stream processor
19//! let processor = StreamProcessor::new(config)?;
20//! processor.process_stream(video_source, |frame| {
21//!     // Process each frame
22//!     detect_objects(frame)
23//! })?;
24//! ```
25
26use crate::{Result, VisionError};
27use std::collections::VecDeque;
28use std::sync::{Arc, Mutex};
29use std::time::{Duration, Instant};
30use torsh_tensor::Tensor;
31
32/// Stream processing configuration
33#[derive(Debug, Clone)]
34pub struct StreamConfig {
35    /// Maximum frames to buffer
36    pub buffer_size: usize,
37    /// Target frames per second
38    pub target_fps: f32,
39    /// Enable frame dropping if processing too slow
40    pub enable_frame_drop: bool,
41    /// Enable GPU acceleration
42    pub use_gpu: bool,
43    /// Number of parallel processing threads
44    pub num_threads: usize,
45    /// Quality adaptation strategy
46    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/// Quality adaptation strategies for maintaining real-time performance
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum QualityAdaptation {
65    /// No adaptation
66    None,
67    /// Reduce resolution when falling behind
68    ResolutionScaling,
69    /// Skip non-keyframe processing
70    KeyframeOnly,
71    /// Adaptive quality based on complexity
72    Dynamic,
73}
74
75/// Frame metadata for processing pipeline
76#[derive(Debug, Clone)]
77pub struct FrameMetadata {
78    /// Frame number in stream
79    pub frame_number: u64,
80    /// Timestamp when frame was captured
81    pub timestamp: Instant,
82    /// Frame width
83    pub width: usize,
84    /// Frame height
85    pub height: usize,
86    /// Is this a keyframe
87    pub is_keyframe: bool,
88    /// Processing priority (higher = more important)
89    pub priority: u8,
90}
91
92/// Frame with metadata
93#[derive(Debug, Clone)]
94pub struct Frame {
95    /// Frame data as tensor
96    pub data: Tensor,
97    /// Frame metadata
98    pub metadata: FrameMetadata,
99}
100
101/// Performance statistics for stream processing
102#[derive(Debug, Clone)]
103pub struct StreamStats {
104    /// Average processing time per frame (ms)
105    pub avg_processing_time: f32,
106    /// Current frames per second
107    pub current_fps: f32,
108    /// Number of dropped frames
109    pub dropped_frames: u64,
110    /// Total frames processed
111    pub total_frames: u64,
112    /// Buffer utilization (0.0 to 1.0)
113    pub buffer_utilization: f32,
114    /// Number of quality adaptations
115    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
131/// Real-time stream processor
132pub 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    /// Create a new stream processor
141    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    /// Get current performance statistics
152    pub fn stats(&self) -> StreamStats {
153        self.stats.lock().map(|s| s.clone()).unwrap_or_default()
154    }
155
156    /// Reset statistics
157    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    /// Add a frame to the processing buffer
167    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        // Check buffer capacity
173        if buffer.len() >= self.config.buffer_size {
174            if self.config.enable_frame_drop {
175                // Drop oldest non-keyframe
176                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                        // Update stats
183                        if let Ok(mut stats) = self.stats.lock() {
184                            stats.dropped_frames += 1;
185                        }
186                        break;
187                    }
188                }
189
190                if !dropped {
191                    // All are keyframes, drop oldest
192                    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        // Update buffer utilization
207        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    /// Get next frame from buffer
215    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    /// Record processing time for a frame
224    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            // Keep only recent times (last 100 frames)
229            while times.len() > 100 {
230                times.pop_front();
231            }
232
233            // Update stats
234            if let Ok(mut stats) = self.stats.lock() {
235                // Calculate average processing time
236                let sum: Duration = times.iter().sum();
237                stats.avg_processing_time = sum.as_secs_f32() * 1000.0 / times.len() as f32;
238
239                // Calculate FPS
240                if stats.avg_processing_time > 0.0 {
241                    stats.current_fps = 1000.0 / stats.avg_processing_time;
242                }
243            }
244        }
245    }
246
247    /// Process a frame and update statistics
248    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        // Apply quality adaptation if needed
255        let adapted_frame = self.adapt_frame_quality(&frame)?;
256
257        // Process the frame
258        let result = process_fn(&adapted_frame)?;
259
260        // Record processing time
261        let elapsed = start.elapsed();
262        self.record_processing_time(elapsed);
263
264        // Update total frames
265        if let Ok(mut stats) = self.stats.lock() {
266            stats.total_frames += 1;
267        }
268
269        Ok(result)
270    }
271
272    /// Adapt frame quality based on performance
273    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                // Check if we're falling behind
279                let stats = self.stats();
280                if stats.current_fps < self.config.target_fps * 0.8 {
281                    // Reduce resolution by 25% and resample with bilinear interpolation.
282                    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                // Skip non-keyframes if falling behind
296                let stats = self.stats();
297                if !frame.metadata.is_keyframe && stats.current_fps < self.config.target_fps * 0.9 {
298                    // Mark for skipping (caller should handle)
299                    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                // Combine strategies based on performance
308                let stats = self.stats();
309                let performance_ratio = stats.current_fps / self.config.target_fps;
310
311                if performance_ratio < 0.7 {
312                    // Severe degradation: halve the resolution with bilinear resampling.
313                    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                    // Moderate degradation: skip non-keyframes
323                    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    /// Check if processing is keeping up with target FPS
334    pub fn is_realtime(&self) -> bool {
335        let stats = self.stats();
336        stats.current_fps >= self.config.target_fps * 0.95
337    }
338
339    /// Get recommended adjustments for configuration
340    pub fn recommend_config_adjustments(&self) -> Vec<String> {
341        let stats = self.stats();
342        let mut recommendations = Vec::new();
343
344        // Check FPS performance
345        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        // Check buffer utilization
351        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        // Check dropped frames
356        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
370/// Downscale a video frame to the requested resolution using bilinear interpolation.
371///
372/// The frame's tensor is interpreted as `CHW` (channels, height, width) when it has
373/// three dimensions and as `HW` (height, width) when it has two; any other rank is
374/// rejected with an honest error rather than silently passing the frame through
375/// unchanged.
376///
377/// Sampling uses the half-pixel-center convention
378/// (`src = (dst + 0.5) * scale - 0.5`, `scale = src_size / dst_size`), which matches the
379/// behaviour of OpenCV's `resize` and PyTorch's
380/// `interpolate(.., mode = "bilinear", align_corners = false)`. Source coordinates that
381/// fall outside the image are clamped to the edge so border pixels are handled correctly.
382fn 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    // Nothing to resample when the resolution is unchanged.
393    if target_w == src_w && target_h == src_h {
394        return Ok(frame.clone());
395    }
396
397    // `shape()` yields an owned temporary, so copy the dimensions out within the statement.
398    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                // Bilinear blend of the four neighbouring source pixels.
444                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
469/// Frame preprocessor for common vision tasks
470pub struct FramePreprocessor {
471    /// Target size for resizing (width, height)
472    pub target_size: Option<(usize, usize)>,
473    /// Normalization mean values
474    pub normalize_mean: Option<Vec<f32>>,
475    /// Normalization std values
476    pub normalize_std: Option<Vec<f32>>,
477    /// Convert to grayscale
478    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    /// Create a new preprocessor with default settings
494    pub fn new() -> Self {
495        Self::default()
496    }
497
498    /// Set target resize dimensions
499    pub fn with_resize(mut self, width: usize, height: usize) -> Self {
500        self.target_size = Some((width, height));
501        self
502    }
503
504    /// Set normalization parameters
505    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    /// Enable grayscale conversion
512    pub fn with_grayscale(mut self) -> Self {
513        self.to_grayscale = true;
514        self
515    }
516
517    /// Preprocess a frame
518    ///
519    /// Applies the configured operations in order:
520    /// 1. Resize (nearest-neighbor) if `target_size` is set
521    /// 2. Per-channel normalization if `normalize_mean`/`normalize_std` are set
522    /// 3. Grayscale conversion (weighted average) if `to_grayscale` is set
523    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        // Step 1: Resize using nearest-neighbor sampling
529        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                // Determine number of channels from the tensor shape
536                let channels = if dims.len() == 3 {
537                    dims[0] // CHW layout
538                } 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        // Step 2: Per-channel normalization: (pixel - mean) / std
571        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        // Step 3: Grayscale conversion using luminance weights (ITU-R BT.601)
594        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                // R=dims[0]=0, G=1, B=2
603                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
622/// Batch processor for efficient multi-frame processing
623pub struct BatchProcessor {
624    batch_size: usize,
625    frames: Vec<Frame>,
626}
627
628impl BatchProcessor {
629    /// Create a new batch processor
630    pub fn new(batch_size: usize) -> Self {
631        Self {
632            batch_size,
633            frames: Vec::with_capacity(batch_size),
634        }
635    }
636
637    /// Add a frame to the batch
638    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    /// Get the current batch without clearing
649    pub fn current_batch(&self) -> &[Frame] {
650        &self.frames
651    }
652
653    /// Flush the current batch
654    pub fn flush(&mut self) -> Vec<Frame> {
655        std::mem::replace(&mut self.frames, Vec::with_capacity(self.batch_size))
656    }
657
658    /// Check if batch is full
659    pub fn is_full(&self) -> bool {
660        self.frames.len() >= self.batch_size
661    }
662
663    /// Get number of frames in current batch
664    pub fn len(&self) -> usize {
665        self.frames.len()
666    }
667
668    /// Check if batch is empty
669    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        // Fill buffer beyond capacity
731        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        // Initially no frames processed, should return true (no data)
797        // After processing, FPS will be calculated
798        processor.record_processing_time(Duration::from_millis(10));
799        let is_rt = processor.is_realtime();
800        // Result depends on target FPS (30) vs current FPS (100 from 10ms processing)
801        assert!(is_rt); // 100 FPS > 30 FPS target
802    }
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        // Create a 4×4×3 CHW frame
815        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        // CHW frame with all values = 128.0
844        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        // Normalize with mean=128, std=128 → output should be ~0.0
860        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        // Pure red image: R=1.0, G=0.0, B=0.0 → luminance = 0.299
874        let mut data = vec![0.0f32; 3 * 4 * 4];
875        // Channel 0 (R) = 1.0
876        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        // 4x4 single-channel frame where pixel(y, x) = 4*y + x:
914        //   0  1  2  3
915        //   4  5  6  7
916        //   8  9 10 11
917        //  12 13 14 15
918        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        // Dimensions must reflect the new resolution.
936        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        // Hand-computed bilinear values (half-pixel centres, scale = 2):
942        //   out(0,0): fy=fx=0.5 -> blend of {0,1,4,5} = 2.5
943        //   out(0,1): fx=2.5    -> blend of {2,3,6,7} = 4.5
944        //   out(1,0): fy=2.5    -> blend of {8,9,12,13} = 10.5
945        //   out(1,1): fy=fx=2.5 -> blend of {10,11,14,15} = 12.5
946        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        // A rank-1 tensor cannot be interpreted as HW or CHW: expect an honest error
960        // rather than a silent passthrough.
961        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        // A fresh processor has current_fps == 0, which is below target * 0.8, so the
979        // ResolutionScaling adaptation must produce a genuinely smaller frame (8 -> 6).
980        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        // `process_frame` runs the adaptation then hands the adapted frame to the closure.
1001        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}