385 lines
15 KiB
Plaintext
385 lines
15 KiB
Plaintext
from PySide6.QtCore import QObject, Signal, QThread, Qt, QMutex, QWaitCondition, QTimer
|
|
from PySide6.QtGui import QImage, QPixmap
|
|
import cv2
|
|
import time
|
|
import numpy as np
|
|
from collections import deque
|
|
from typing import Dict, List, Optional
|
|
import os
|
|
import sys
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Import utilities
|
|
from utils.annotation_utils import (
|
|
draw_detections,
|
|
draw_violations,
|
|
draw_performance_metrics,
|
|
resize_frame_for_display,
|
|
convert_cv_to_qimage,
|
|
convert_cv_to_pixmap
|
|
)
|
|
|
|
class VideoController(QObject):
|
|
frame_ready = Signal(object, object, object, dict) # QPixmap, detections, violations, metrics
|
|
raw_frame_ready = Signal(np.ndarray, list, list, float) # frame, detections, violations, fps
|
|
|
|
def __init__(self, model_manager=None):
|
|
"""
|
|
Initialize video controller.
|
|
|
|
Args:
|
|
model_manager: Model manager instance for detection and violation
|
|
"""
|
|
super().__init__()
|
|
self.model_manager = model_manager
|
|
self.source = 0 # Default camera source
|
|
self._running = False
|
|
self.frame_count = 0
|
|
self.start_time = 0
|
|
self.source_fps = 0
|
|
self.actual_fps = 0
|
|
self.processing_times = deque(maxlen=30)
|
|
self.cap = None # VideoCapture object
|
|
|
|
# Configure thread
|
|
self.thread = QThread()
|
|
self.moveToThread(self.thread)
|
|
self.thread.started.connect(self._run)
|
|
|
|
# Performance measurement
|
|
self.mutex = QMutex()
|
|
self.condition = QWaitCondition()
|
|
self.performance_metrics = {
|
|
'FPS': 0.0,
|
|
'Detection (ms)': 0.0,
|
|
'Violation (ms)': 0.0,
|
|
'Total (ms)': 0.0
|
|
}
|
|
|
|
# Setup render timer
|
|
self.render_timer = QTimer()
|
|
self.render_timer.timeout.connect(self._process_frame)
|
|
|
|
# Frame buffer
|
|
self.current_frame = None
|
|
self.current_detections = []
|
|
self.current_violations = []
|
|
|
|
# Debug counter
|
|
self.debug_counter = 0
|
|
|
|
def set_source(self, source):
|
|
"""Set video source (file path, camera index, or URL)"""
|
|
print(f"DEBUG: VideoController.set_source called with: {source} (type: {type(source)})")
|
|
|
|
was_running = self._running
|
|
if self._running:
|
|
self.stop()
|
|
|
|
# Critical fix: Make sure source is properly set
|
|
if source is None:
|
|
print("WARNING: Received None source, defaulting to camera 0")
|
|
self.source = 0
|
|
elif isinstance(source, str) and source.strip():
|
|
# Handle file paths - verify the file exists
|
|
if os.path.exists(source):
|
|
self.source = source
|
|
print(f"DEBUG: VideoController source set to file: {self.source}")
|
|
else:
|
|
# Try to interpret as camera index or URL
|
|
try:
|
|
# If it's a digit string, convert to integer camera index
|
|
if source.isdigit():
|
|
self.source = int(source)
|
|
print(f"DEBUG: VideoController source set to camera index: {self.source}")
|
|
else:
|
|
# Treat as URL or special device string
|
|
self.source = source
|
|
print(f"DEBUG: VideoController source set to URL/device: {self.source}")
|
|
except ValueError:
|
|
print(f"WARNING: Could not interpret source: {source}, defaulting to camera 0")
|
|
self.source = 0
|
|
elif isinstance(source, int):
|
|
# Camera index
|
|
self.source = source
|
|
print(f"DEBUG: VideoController source set to camera index: {self.source}")
|
|
else:
|
|
print(f"WARNING: Unrecognized source type: {type(source)}, defaulting to camera 0")
|
|
self.source = 0
|
|
|
|
# Get properties of the source (fps, dimensions, etc)
|
|
self._get_source_properties()
|
|
|
|
if was_running:
|
|
self.start()
|
|
|
|
def _get_source_properties(self):
|
|
"""Get properties of video source"""
|
|
try:
|
|
cap = cv2.VideoCapture(self.source)
|
|
if cap.isOpened():
|
|
self.source_fps = cap.get(cv2.CAP_PROP_FPS)
|
|
if self.source_fps <= 0:
|
|
self.source_fps = 30.0 # Default if undetectable
|
|
|
|
self.frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
self.frame_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
self.frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
cap.release()
|
|
|
|
print(f"Video source: {self.frame_width}x{self.frame_height}, {self.source_fps} FPS")
|
|
else:
|
|
print("Failed to open video source")
|
|
except Exception as e:
|
|
print(f"Error getting source properties: {e}")
|
|
|
|
def start(self):
|
|
"""Start video processing"""
|
|
if not self._running:
|
|
self._running = True
|
|
self.start_time = time.time()
|
|
self.frame_count = 0
|
|
self.debug_counter = 0
|
|
print("DEBUG: Starting video processing thread")
|
|
|
|
# Start the processing thread
|
|
if not self.thread.isRunning():
|
|
self.thread.start()
|
|
|
|
# Start the render timer with a faster interval (16ms = ~60fps)
|
|
self.render_timer.start(16)
|
|
print("DEBUG: Render timer started")
|
|
|
|
def stop(self):
|
|
"""Stop video processing"""
|
|
if self._running:
|
|
print("DEBUG: Stopping video processing")
|
|
self._running = False
|
|
self.render_timer.stop()
|
|
|
|
# Properly terminate the thread
|
|
self.thread.quit()
|
|
if not self.thread.wait(3000): # Wait 3 seconds max
|
|
self.thread.terminate()
|
|
print("WARNING: Thread termination forced")
|
|
|
|
# Close the capture if it exists
|
|
if self.cap and self.cap.isOpened():
|
|
self.cap.release()
|
|
self.cap = None
|
|
|
|
# Clear the current frame
|
|
self.mutex.lock()
|
|
self.current_frame = None
|
|
self.mutex.unlock()
|
|
print("DEBUG: Video processing stopped")
|
|
|
|
def capture_snapshot(self) -> np.ndarray:
|
|
"""Capture current frame"""
|
|
if self.current_frame is not None:
|
|
return self.current_frame.copy()
|
|
return None
|
|
|
|
def _run(self):
|
|
"""Main processing loop (runs in thread)"""
|
|
try:
|
|
# Print the source we're trying to open
|
|
print(f"DEBUG: Opening video source: {self.source} (type: {type(self.source)})")
|
|
|
|
# Initialize the capture
|
|
self.cap = None
|
|
|
|
# Handle different source types
|
|
if isinstance(self.source, str) and os.path.exists(self.source):
|
|
# It's a valid file path
|
|
print(f"DEBUG: Opening video file: {self.source}")
|
|
self.cap = cv2.VideoCapture(self.source)
|
|
|
|
# Verify file opened successfully
|
|
if not self.cap.isOpened():
|
|
print(f"ERROR: Could not open video file: {self.source}")
|
|
return
|
|
|
|
elif isinstance(self.source, int) or (isinstance(self.source, str) and self.source.isdigit()):
|
|
# It's a camera index
|
|
camera_idx = int(self.source) if isinstance(self.source, str) else self.source
|
|
print(f"DEBUG: Opening camera: {camera_idx}")
|
|
self.cap = cv2.VideoCapture(camera_idx)
|
|
|
|
# Try a few times to open camera (sometimes takes a moment)
|
|
retry_count = 0
|
|
while not self.cap.isOpened() and retry_count < 3:
|
|
print(f"Camera not ready, retrying ({retry_count+1}/3)...")
|
|
time.sleep(1)
|
|
self.cap.release()
|
|
self.cap = cv2.VideoCapture(camera_idx)
|
|
retry_count += 1
|
|
|
|
if not self.cap.isOpened():
|
|
print(f"ERROR: Could not open camera {camera_idx} after {retry_count} attempts")
|
|
return
|
|
else:
|
|
# Try as a string source (URL or device path)
|
|
print(f"DEBUG: Opening source as string: {self.source}")
|
|
self.cap = cv2.VideoCapture(str(self.source))
|
|
|
|
if not self.cap.isOpened():
|
|
print(f"ERROR: Could not open source: {self.source}")
|
|
return
|
|
|
|
# Check again to ensure capture is valid
|
|
if not self.cap or not self.cap.isOpened():
|
|
print(f"ERROR: Could not open video source {self.source}")
|
|
return
|
|
|
|
# Configure frame timing based on source FPS
|
|
frame_time = 1.0 / self.source_fps if self.source_fps > 0 else 0.033
|
|
prev_time = time.time()
|
|
|
|
# Log successful opening
|
|
print(f"SUCCESS: Video source opened: {self.source}")
|
|
print(f"Source info - FPS: {self.source_fps}, Size: {self.frame_width}x{self.frame_height}")
|
|
|
|
# Main processing loop
|
|
while self._running and self.cap.isOpened():
|
|
ret, frame = self.cap.read()
|
|
if not ret:
|
|
print("End of video or read error")
|
|
break
|
|
|
|
# Detection and violation processing
|
|
process_start = time.time()
|
|
|
|
# Process detections
|
|
detection_start = time.time()
|
|
detections = []
|
|
if self.model_manager:
|
|
detections = self.model_manager.detect(frame)
|
|
detection_time = (time.time() - detection_start) * 1000
|
|
|
|
# Violation detection is disabled
|
|
violation_start = time.time()
|
|
violations = []
|
|
# if self.model_manager and detections:
|
|
# violations = self.model_manager.detect_violations(
|
|
# detections, frame, time.time()
|
|
# )
|
|
violation_time = (time.time() - violation_start) * 1000
|
|
|
|
# Update tracking if available
|
|
if self.model_manager:
|
|
detections = self.model_manager.update_tracking(detections, frame)
|
|
|
|
# Calculate timing metrics
|
|
process_time = (time.time() - process_start) * 1000
|
|
self.processing_times.append(process_time)
|
|
|
|
# Update FPS
|
|
now = time.time()
|
|
self.frame_count += 1
|
|
elapsed = now - self.start_time
|
|
if elapsed > 0:
|
|
self.actual_fps = self.frame_count / elapsed
|
|
|
|
fps_smoothed = 1.0 / (now - prev_time) if now > prev_time else 0
|
|
prev_time = now
|
|
|
|
# Update metrics
|
|
self.performance_metrics = {
|
|
'FPS': f"{fps_smoothed:.1f}",
|
|
'Detection (ms)': f"{detection_time:.1f}",
|
|
'Violation (ms)': f"{violation_time:.1f}",
|
|
'Total (ms)': f"{process_time:.1f}"
|
|
}
|
|
|
|
# Store current frame data (thread-safe)
|
|
self.mutex.lock()
|
|
self.current_frame = frame.copy()
|
|
self.current_detections = detections
|
|
self.current_violations = violations
|
|
self.mutex.unlock()
|
|
|
|
# Signal for raw data subscribers
|
|
self.raw_frame_ready.emit(frame.copy(), detections, violations, fps_smoothed)
|
|
|
|
# Control processing rate for file sources
|
|
if isinstance(self.source, str) and self.source_fps > 0:
|
|
frame_duration = time.time() - process_start
|
|
if frame_duration < frame_time:
|
|
time.sleep(frame_time - frame_duration)
|
|
|
|
if self.cap:
|
|
self.cap.release()
|
|
self.cap = None
|
|
|
|
except Exception as e:
|
|
print(f"Video processing error: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
finally:
|
|
self._running = False
|
|
if self.cap and self.cap.isOpened():
|
|
self.cap.release()
|
|
self.cap = None
|
|
|
|
def _process_frame(self):
|
|
"""Process current frame for UI rendering (called by timer)"""
|
|
if not self._running:
|
|
return
|
|
|
|
# Debug counter
|
|
if hasattr(self, 'debug_counter'):
|
|
self.debug_counter += 1
|
|
if self.debug_counter % 30 == 0: # Print every ~30 frames
|
|
print(f"DEBUG: Frame processing iteration: {self.debug_counter}")
|
|
|
|
# Get frame data safely
|
|
self.mutex.lock()
|
|
frame = self.current_frame.copy() if self.current_frame is not None else None
|
|
detections = self.current_detections.copy() if hasattr(self, 'current_detections') and self.current_detections else []
|
|
violations = self.current_violations.copy() if hasattr(self, 'current_violations') and self.current_violations else []
|
|
metrics = self.performance_metrics.copy()
|
|
self.mutex.unlock()
|
|
|
|
if frame is None:
|
|
print("DEBUG: _process_frame skipped - no frame available")
|
|
return
|
|
|
|
try:
|
|
# Annotate frame
|
|
annotated_frame = frame.copy()
|
|
if detections:
|
|
annotated_frame = draw_detections(annotated_frame, detections, True, True)
|
|
|
|
# Draw metrics
|
|
annotated_frame = draw_performance_metrics(annotated_frame, metrics)
|
|
|
|
# Resize for display
|
|
display_frame = resize_frame_for_display(annotated_frame)
|
|
|
|
# Convert to QPixmap directly using a better approach
|
|
rgb_image = cv2.cvtColor(display_frame, cv2.COLOR_BGR2RGB)
|
|
h, w, ch = rgb_image.shape
|
|
bytes_per_line = ch * w
|
|
|
|
# Create QImage - critical: use .copy() to ensure data stays valid
|
|
q_image = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
|
|
|
|
# Convert to pixmap
|
|
pixmap = QPixmap.fromImage(q_image)
|
|
|
|
# Emit signal with processed frame
|
|
if not pixmap.isNull():
|
|
print(f"DEBUG: Emitting pixmap: {pixmap.width()}x{pixmap.height()}")
|
|
self.frame_ready.emit(pixmap, detections, violations, metrics)
|
|
else:
|
|
print("ERROR: Created QPixmap is null")
|
|
|
|
except Exception as e:
|
|
print(f"ERROR in _process_frame: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|