"""Ask each camera what it can actually deliver, without the app in the way. The studio's sensor configs pin the capture at 1920x1080 @ 30 fps (the tile grid depends on it), and nothing downstream can raise a frame rate the driver never offered. When a session runs at 16.7 fps, the question is which of these it is: * the camera cannot do 1080p30 at all — then the config has to change, and the tile geometry with it; * it can, but only in a particular pixel format or backend — then the SDK's request sequence/format is what to fix; * it can, and the rate really is ~30 — then the frames are being lost after the capture, and the rail's camera rate will say so. Run it with the venv interpreter, from anywhere:: python apps/studio/tools/camera_probe.py # every camera, every combo python apps/studio/tools/camera_probe.py --index 0 # just one Each row is what OpenCV/the driver negotiated for that request, plus the rate measured from 60 grabs (so "negotiated 30" and "actually delivers 30" are two separate facts). """ from __future__ import annotations import argparse import sys import time import cv2 #: What the sensor bundles ask for, and the alternatives worth trying when the #: negotiated rate falls short of the requested one. REQUESTS: tuple[tuple[str, int, int, float], ...] = ( ("MJPG", 1920, 1080, 30.0), ("MJPG", 1920, 1080, 0.0), # "whatever the driver prefers at this size" ("MJPG", 1280, 720, 30.0), ("YUYV", 1920, 1080, 30.0), ("YUYV", 1280, 720, 30.0), ) BACKENDS: tuple[tuple[str, int], ...] = ( ("DSHOW", cv2.CAP_DSHOW), ("MSMF", cv2.CAP_MSMF), ) SAMPLES = 60 def _fourcc_code(text: str) -> int: return cv2.VideoWriter_fourcc(*text) def _decode(code: float) -> str: value = int(code) if value <= 0: return "-" letters = "".join(chr((value >> (8 * index)) & 0xFF) for index in range(4)) return letters if letters.isprintable() else f"0x{value:08x}" def probe(index: int, backend_name: str, backend: int, request) -> str | None: fourcc, width, height, fps = request cap = cv2.VideoCapture(index, backend) if not cap.isOpened(): cap.release() return None try: cap.set(cv2.CAP_PROP_FOURCC, _fourcc_code(fourcc)) cap.set(cv2.CAP_PROP_FRAME_WIDTH, float(width)) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, float(height)) if fps > 0: cap.set(cv2.CAP_PROP_FPS, float(fps)) cap.set(cv2.CAP_PROP_FOURCC, _fourcc_code(fourcc)) for _ in range(5): # let the format settle before believing anything cap.grab() started = time.perf_counter() taken = 0 for _ in range(SAMPLES): ok, frame = cap.read() if not ok or frame is None: break taken += 1 span = time.perf_counter() - started rate = taken / span if span > 0 else 0.0 got_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) got_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) got_fps = cap.get(cv2.CAP_PROP_FPS) got_cc = _decode(cap.get(cv2.CAP_PROP_FOURCC)) finally: cap.release() asked = f"{fourcc} {width}x{height}" + (f"@{fps:g}" if fps else "@auto") return ( f" {asked:<20} -> {got_cc} {got_w}x{got_h} @{got_fps:5.1f} " f"measured {rate:5.1f} fps ({taken}/{SAMPLES}) [{backend_name}]" ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--index", type=int, default=None, help="camera index to probe") parser.add_argument("--max-index", type=int, default=4, help="highest index to try") args = parser.parse_args(argv) indices = [args.index] if args.index is not None else list(range(args.max_index + 1)) found = 0 for index in indices: header = False for backend_name, backend in BACKENDS: for request in REQUESTS: line = probe(index, backend_name, backend, request) if line is None: continue if not header: print(f"\ncamera {index}:") header = True print(line) if header: found += 1 if not found: print("No camera answered on indices 0..%d." % args.max_index) print( "That is expected on a machine with no camera attached. On the machine " "with the sensor, run it with the app closed: two processes cannot own " "the same camera." ) return 1 print( "\nRead it this way: a row whose measured rate tracks its negotiated rate " "is the truth about that camera; a row that negotiates 30 and measures ~17 " "is dropping frames, and the request sequence is what to fix." ) return 0 if __name__ == "__main__": sys.exit(main())