Files
L'électron rare cfab3c908a fix(multihmr): cv2 brightness probe + heartbeat
cv2.VideoCapture indices on Mac don't follow the AVFoundation
discovery order, so the previous auto-pick still ended up on
the iPhone Continuity stream. resolve_camera_index now probes
each cv2 index, reads a frame, and prefers the one whose mean
luminance is >= 50 (rejects black / standby streams from a
locked iPhone or Desk View).

multi_hmr_worker emits a 5 s heartbeat with the observed fps
and the average detected persons per frame, so 'no detection'
becomes visible in the log instead of staying silent.
2026-05-13 11:46:01 +02:00

103 lines
3.7 KiB
Python

"""Helper de selection de camera macOS : enumere les devices via
AVFoundation et retourne l'index OpenCV qui correspond a la webcam
built-in (BuiltInWideAngleCamera), en evitant Continuity Camera
(iPhone), Desk View, et External."""
from __future__ import annotations
import logging
from typing import Iterable
LOG = logging.getLogger("camera_select")
def list_cameras() -> list[tuple[int, str, str]]:
"""Retourne [(index, localized_name, device_type_short), ...]."""
try:
import objc
from Foundation import NSBundle
b = NSBundle.bundleWithPath_(
"/System/Library/Frameworks/AVFoundation.framework")
b.load()
ns: dict = {}
objc.loadBundle("AVFoundation", ns, b.bundlePath())
DiscoverySession = ns["AVCaptureDeviceDiscoverySession"]
session = (DiscoverySession
.discoverySessionWithDeviceTypes_mediaType_position_(
["AVCaptureDeviceTypeBuiltInWideAngleCamera",
"AVCaptureDeviceTypeContinuityCamera",
"AVCaptureDeviceTypeExternal",
"AVCaptureDeviceTypeDeskViewCamera"],
"vide", 0))
devices = session.devices() or []
result = []
for i, d in enumerate(devices):
name = str(d.localizedName())
dtype = str(d.deviceType() if hasattr(d, "deviceType") else "")
result.append((i, name, dtype.split(".")[-1]))
return result
except Exception as e: # noqa: BLE001
LOG.warning("camera enum failed: %s", e)
return []
def pick_builtin_camera(fallback: int = 0) -> int:
"""Retourne l'index du BuiltInWideAngleCamera, sinon fallback."""
devices = list_cameras()
for i, name, dtype in devices:
LOG.info("camera [%d] %s (%s)", i, name, dtype)
for i, _, dtype in devices:
if "BuiltInWideAngleCamera" in dtype:
LOG.info("camera Mac built-in -> index %d", i)
return i
return fallback
def probe_cv2_indices(max_idx: int = 4) -> list[tuple[int, int, int, float]]:
"""Pour chaque index cv2 0..max_idx, retourne (idx, w, h, mean) ou
None pour les indices indisponibles. Le 'mean' est la luminance
moyenne d'une frame — un capture standby (iPhone verrouille,
Continuity inactive) a mean ~0-30 ; une cam active ~80+."""
try:
import cv2
import numpy as np
except ImportError:
return []
result = []
for idx in range(max_idx):
cap = cv2.VideoCapture(idx, cv2.CAP_AVFOUNDATION)
if not cap.isOpened():
cap.release()
continue
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Drain 2 frames pour passer l'auto-exposure
for _ in range(2):
cap.read()
ok, frame = cap.read()
mean = float(np.mean(frame)) if (ok and frame is not None) else -1.0
result.append((idx, w, h, mean))
cap.release()
return result
def resolve_camera_index(requested: int, min_mean: float = 50.0) -> int:
"""`requested=-1` -> probe cv2, prefere l'index avec frame_mean
>= min_mean (rejette les flux noirs / standby). Sinon retourne
`requested`."""
if requested >= 0:
return requested
probes = probe_cv2_indices()
for idx, w, h, mean in probes:
LOG.info("cv2 probe [%d] %dx%d mean=%.1f", idx, w, h, mean)
bright = [p for p in probes if p[3] >= min_mean]
if bright:
idx = bright[0][0]
LOG.info("cv2 auto-pick index %d (mean %.1f >= %.1f)",
idx, bright[0][3], min_mean)
return idx
if probes:
LOG.warning("no bright cv2 stream — defaulting to index %d",
probes[0][0])
return probes[0][0]
return 0