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.
This commit is contained in:
L'électron rare
2026-05-13 11:46:01 +02:00
parent 511881f56d
commit cfab3c908a
2 changed files with 63 additions and 5 deletions
+47 -4
View File
@@ -52,8 +52,51 @@ def pick_builtin_camera(fallback: int = 0) -> int:
return fallback
def resolve_camera_index(requested: int) -> int:
"""`requested=-1` -> built-in auto, sinon valeur passee."""
if requested < 0:
return pick_builtin_camera(fallback=0)
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
+15
View File
@@ -138,6 +138,9 @@ class MultiHMRWorker:
return
LOG.info("camera ouverte index=%d %dx%d",
cam_idx, IMG_SIZE, IMG_SIZE)
frame_count = 0
persons_count = 0
next_heartbeat = time.monotonic() + 5.0
while not self._stop.is_set():
t0 = time.monotonic()
@@ -235,6 +238,18 @@ class MultiHMRWorker:
self.state.persons_smplx = persons
self.state.smplx_last_t = t_now
frame_count += 1
persons_count += len(persons)
if t_now >= next_heartbeat:
fps = frame_count / 5.0
avg = persons_count / max(1, frame_count)
LOG.info(
"hb: %.1f fps, %.2f persons/frame (%d frames)",
fps, avg, frame_count)
frame_count = 0
persons_count = 0
next_heartbeat = t_now + 5.0
dt = time.monotonic() - t0
if dt < self.period:
time.sleep(self.period - dt)