Files
PlanarianScanner/test_tube_scanner/modules/webcam_capture.py
T
2026-04-13 23:14:05 +02:00

153 lines
5.3 KiB
Python

"""
Implémentation de la capture vidéo pour webcam via OpenCV (cv2).
Dépendance : opencv-python (pip install opencv-python)
OpenCV (cv2) avec import local pour éviter une dépendance globale
Résolution configurable, qualité JPEG réglable à chaud, accès V4L2 par index
get_resolution() pour lire la résolution effective appliquée par le pilote
"""
import os
os.environ['OPENCV_LOG_LEVEL']="0"
os.environ['OPENCV_FFMPEG_LOGLEVEL']="0"
import cv2
import logging
from typing import Optional
from .capture_interface import CaptureError, VideoCaptureInterface
logger = logging.getLogger(__name__)
class WebcamCapture(VideoCaptureInterface):
"""
Capture JPEG depuis une webcam USB/intégrée via OpenCV.
Exemple d'utilisation ::
cam = WebcamCapture(device_index=0, fps=5)
cam.set_frame_callback(lambda data, ts: print(f"{ts}: {len(data)} octets"))
cam.start()
time.sleep(10)
cam.stop()
"""
def __init__(
self,
device_index: int = 0,
fps: float = VideoCaptureInterface.DEFAULT_FPS,
jpeg_quality: int = 85,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
:param device_index: Index du périphérique V4L2 (0 = première webcam)
:param fps: Cadence cible en images par seconde
:param jpeg_quality: Qualité de compression JPEG [0-100]
:param width: Largeur souhaitée (None = valeur par défaut du pilote)
:param height: Hauteur souhaitée (None = valeur par défaut du pilote)
"""
super().__init__(fps=fps)
self._device_index: int = device_index
self._jpeg_quality: int = jpeg_quality
self._width: Optional[int] = width
self._height: Optional[int] = height
self._cap = None # Instance cv2.VideoCapture
# ------------------------------------------------------------------
# Implémentation des méthodes abstraites
# ------------------------------------------------------------------
def open(self) -> None:
"""Ouvre le flux V4L2 via OpenCV et configure la résolution."""
self._cap = cv2.VideoCapture(self._device_index)
if not self._cap.isOpened():
raise CaptureError(
f"Impossible d'ouvrir la webcam (index={self._device_index})"
)
# Application de la résolution demandée
if self._width:
self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, self._width)
if self._height:
self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self._height)
# Lecture de la résolution effectivement appliquée par le pilote
actual_w = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
logger.info(
"Webcam ouverte : index=%d résolution=%dx%d",
self._device_index, actual_w, actual_h,
)
def close(self) -> None:
"""Libère le flux OpenCV."""
if self._cap and self._cap.isOpened():
self._cap.release()
logger.info("Webcam fermée (index=%d)", self._device_index)
self._cap = None
def capture_frame(self) -> bytes:
"""
Lit une trame brute depuis OpenCV et l'encode en JPEG.
:return: Données JPEG brutes
:raises CaptureError: Si la lecture ou l'encodage échoue
"""
#import cv2
#import numpy as np # noqa: F401 — utilisé implicitement par cv2
if not self._cap or not self._cap.isOpened():
raise CaptureError("La webcam n'est pas ouverte")
ret, frame = self._cap.read()
if not ret or frame is None:
raise CaptureError("Échec de lecture de la trame")
# Encodage BGR → JPEG avec la qualité configurée
encode_params = [cv2.IMWRITE_JPEG_QUALITY, self._jpeg_quality]
success, buffer = cv2.imencode(".jpg", frame, encode_params)
if not success:
raise CaptureError("Échec d'encodage JPEG")
return buffer.tobytes()
def is_available(self) -> bool:
"""Retourne True si le flux OpenCV est ouvert et prêt."""
return self._cap is not None and self._cap.isOpened()
# ------------------------------------------------------------------
# Accesseurs spécifiques à la webcam
# ------------------------------------------------------------------
@property
def device_index(self) -> int:
"""Index du périphérique V4L2."""
return self._device_index
@property
def jpeg_quality(self) -> int:
"""Qualité JPEG [0-100]."""
return self._jpeg_quality
@jpeg_quality.setter
def jpeg_quality(self, value: int) -> None:
if not 0 <= value <= 100:
raise ValueError("La qualité JPEG doit être comprise entre 0 et 100")
self._jpeg_quality = value
def get_resolution(self) -> Optional[tuple[int, int]]:
"""
Retourne la résolution effective du flux.
:return: Tuple (largeur, hauteur) ou None si la webcam est fermée
"""
if not self.is_available():
return None
w = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
return (w, h)