Deep-Live-Cam/modules/video_capture.py

154 lines
5.7 KiB
Python
Raw Normal View History

2024-12-13 22:19:11 +08:00
import cv2
import numpy as np
from typing import Optional, Tuple, Callable
2024-12-16 21:11:39 +08:00
import platform
import threading
import time
from collections import deque
2024-12-16 21:11:39 +08:00
# Only import Windows-specific library if on Windows
if platform.system() == "Windows":
from pygrabber.dshow_graph import FilterGraph
2024-12-13 22:19:11 +08:00
class VideoCapturer:
def __init__(self, device_index: int):
self.device_index = device_index
self.frame_callback = None
self._current_frame = None
self._frame_ready = threading.Event()
self.is_running = False
self.cap = None
# Performance tracking
self.frame_times = deque(maxlen=30)
self.current_fps = 0
self.target_fps = 30
self.frame_skip = 1
self.frame_counter = 0
# Buffer management
self.frame_buffer = deque(maxlen=3)
self.buffer_lock = threading.Lock()
2024-12-13 22:19:11 +08:00
2024-12-16 21:11:39 +08:00
# Initialize Windows-specific components if on Windows
if platform.system() == "Windows":
self.graph = FilterGraph()
# Verify device exists
devices = self.graph.get_input_devices()
if self.device_index >= len(devices):
raise ValueError(
f"Invalid device index {device_index}. Available devices: {len(devices)}"
)
2024-12-13 22:19:11 +08:00
def start(self, width: int = 960, height: int = 540, fps: int = 60) -> bool:
"""Initialize and start video capture with performance optimizations"""
2024-12-13 22:19:11 +08:00
try:
self.target_fps = fps
2024-12-16 21:11:39 +08:00
if platform.system() == "Windows":
# Windows-specific capture methods
capture_methods = [
(self.device_index, cv2.CAP_DSHOW), # Try DirectShow first
(self.device_index, cv2.CAP_ANY), # Then try default backend
(-1, cv2.CAP_ANY), # Try -1 as fallback
(0, cv2.CAP_ANY), # Finally try 0 without specific backend
]
for dev_id, backend in capture_methods:
try:
self.cap = cv2.VideoCapture(dev_id, backend)
if self.cap.isOpened():
break
self.cap.release()
except Exception:
continue
else:
# Unix-like systems (Linux/Mac) capture method
self.cap = cv2.VideoCapture(self.device_index)
2024-12-13 22:19:11 +08:00
if not self.cap or not self.cap.isOpened():
2024-12-16 21:11:39 +08:00
raise RuntimeError("Failed to open camera")
2024-12-13 22:19:11 +08:00
# Configure format with performance optimizations
2024-12-13 22:19:11 +08:00
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
self.cap.set(cv2.CAP_PROP_FPS, fps)
# Additional performance settings
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer to minimize latency
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # Use MJPEG for better performance
2024-12-13 22:19:11 +08:00
self.is_running = True
return True
except Exception as e:
print(f"Failed to start capture: {str(e)}")
if self.cap:
self.cap.release()
return False
def read(self) -> Tuple[bool, Optional[np.ndarray]]:
"""Read a frame from the camera with performance optimizations"""
2024-12-13 22:19:11 +08:00
if not self.is_running or self.cap is None:
return False, None
start_time = time.time()
# Implement frame skipping for performance
self.frame_counter += 1
if self.frame_counter % self.frame_skip != 0:
# Skip this frame but still read to clear buffer
ret, _ = self.cap.read()
return ret, self._current_frame if ret else None
2024-12-13 22:19:11 +08:00
ret, frame = self.cap.read()
if ret:
self._current_frame = frame
# Update performance metrics
frame_time = time.time() - start_time
self.frame_times.append(frame_time)
self._update_performance_metrics()
# Add to buffer for processing
with self.buffer_lock:
self.frame_buffer.append(frame.copy())
2024-12-13 22:19:11 +08:00
if self.frame_callback:
self.frame_callback(frame)
return True, frame
return False, None
def _update_performance_metrics(self):
"""Update FPS and adjust frame skipping based on performance"""
if len(self.frame_times) >= 10:
avg_frame_time = sum(list(self.frame_times)[-10:]) / 10
self.current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0
# Adaptive frame skipping
if self.current_fps < self.target_fps * 0.8:
self.frame_skip = min(3, self.frame_skip + 1)
elif self.current_fps > self.target_fps * 0.95:
self.frame_skip = max(1, self.frame_skip - 1)
def get_buffered_frame(self) -> Optional[np.ndarray]:
"""Get the latest frame from buffer"""
with self.buffer_lock:
return self.frame_buffer[-1] if self.frame_buffer else None
def get_fps(self) -> float:
"""Get current FPS"""
return self.current_fps
2024-12-13 22:19:11 +08:00
def release(self) -> None:
"""Stop capture and release resources"""
if self.is_running and self.cap is not None:
self.cap.release()
self.is_running = False
self.cap = None
def set_frame_callback(self, callback: Callable[[np.ndarray], None]) -> None:
"""Set callback for frame processing"""
self.frame_callback = callback