96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""Accès simple à la caméra d'Alfred, avec aperçu MJPEG activé à la demande.
|
||
|
||
Une seule lecture par instance Alfred. La vidéo ne démarre que lorsque
|
||
l'utilisateur clique sur Activer, pour éviter de monopoliser la caméra.
|
||
"""
|
||
import os
|
||
import threading
|
||
import time
|
||
|
||
_CAMERA_LOCK = threading.Lock()
|
||
|
||
# DONA_VISION_V95 : partage une image récente avec la détection YOLOv8.
|
||
_FRAME_LOCK = threading.Lock()
|
||
_LAST_FRAME = None
|
||
_FRAME_TIMESTAMP = 0.0
|
||
|
||
def _cache_frame(frame):
|
||
global _LAST_FRAME, _FRAME_TIMESTAMP
|
||
with _FRAME_LOCK:
|
||
_LAST_FRAME = frame.copy()
|
||
_FRAME_TIMESTAMP = time.monotonic()
|
||
|
||
def read_frame(index=None, max_age=1.4):
|
||
"""Retourne une image de l'aperçu actif ou capture sans collision de caméra."""
|
||
import cv2
|
||
with _FRAME_LOCK:
|
||
if _LAST_FRAME is not None and time.monotonic() - _FRAME_TIMESTAMP <= max_age:
|
||
return _LAST_FRAME.copy()
|
||
if not _CAMERA_LOCK.acquire(blocking=False):
|
||
raise RuntimeError('Caméra occupée : active l’aperçu et réessaie, ou ferme l’autre application.')
|
||
cap = None
|
||
try:
|
||
cam_index = int(os.getenv('CAMERA_INDEX', '0')) if index is None else index
|
||
cap = cv2.VideoCapture(cam_index)
|
||
if not cap.isOpened():
|
||
raise RuntimeError('Caméra indisponible sur Dona.')
|
||
ok, frame = cap.read()
|
||
if not ok: raise RuntimeError('Capture caméra impossible.')
|
||
_cache_frame(frame)
|
||
return frame
|
||
finally:
|
||
if cap is not None: cap.release()
|
||
_CAMERA_LOCK.release()
|
||
|
||
|
||
|
||
def snapshot(path, index=0):
|
||
import cv2
|
||
frame = read_frame(index=index)
|
||
if not cv2.imwrite(path,frame): raise RuntimeError('Impossible d’enregistrer la capture.')
|
||
return path
|
||
|
||
|
||
def video_stream(index=0):
|
||
"""Ouvre la caméra, retourne un générateur de trames JPEG multipart.
|
||
|
||
L'appelant doit retourner ce générateur dans une réponse Flask en streaming.
|
||
L'absence de cv2 ou de webcam déclenche une exception avant HTTP 200.
|
||
"""
|
||
import cv2
|
||
if not _CAMERA_LOCK.acquire(blocking=False):
|
||
raise RuntimeError('Prévisualisation vidéo déjà active')
|
||
|
||
cap = None
|
||
try:
|
||
cap = cv2.VideoCapture(index)
|
||
if not cap.isOpened():
|
||
cap.release()
|
||
raise RuntimeError(f'Caméra {index} indisponible')
|
||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, int(os.getenv('CAMERA_WIDTH', '640')))
|
||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, int(os.getenv('CAMERA_HEIGHT', '480')))
|
||
fps = min(15.0, max(1.0, float(os.getenv('CAMERA_FPS', '8'))))
|
||
quality = min(95, max(35, int(os.getenv('CAMERA_JPEG_QUALITY', '75'))))
|
||
except Exception:
|
||
if cap is not None:
|
||
cap.release()
|
||
_CAMERA_LOCK.release()
|
||
raise
|
||
|
||
def frames():
|
||
try:
|
||
while True:
|
||
ok, frame = cap.read()
|
||
if not ok:
|
||
break
|
||
_cache_frame(frame) # DONA_VISION_V95 : vidéo et YOLO partagent la capture.
|
||
ok_jpeg, encoded = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, quality])
|
||
if ok_jpeg:
|
||
yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + encoded.tobytes() + b'\r\n'
|
||
time.sleep(1.0 / fps)
|
||
finally:
|
||
cap.release()
|
||
_CAMERA_LOCK.release()
|
||
|
||
return frames()
|