From cfab3c908acb19cd8d5112692d11d6a64191d0e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Wed, 13 May 2026 11:46:01 +0200 Subject: [PATCH] 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. --- data_only_viz/_camera_select.py | 53 ++++++++++++++++++++++++++++--- data_only_viz/multi_hmr_worker.py | 15 +++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/data_only_viz/_camera_select.py b/data_only_viz/_camera_select.py index fbf45f6..20c16f1 100644 --- a/data_only_viz/_camera_select.py +++ b/data_only_viz/_camera_select.py @@ -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) - return requested +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 diff --git a/data_only_viz/multi_hmr_worker.py b/data_only_viz/multi_hmr_worker.py index 00e9b1c..ed56dfe 100644 --- a/data_only_viz/multi_hmr_worker.py +++ b/data_only_viz/multi_hmr_worker.py @@ -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)