138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""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())
|