stereo-charuco-pipeline 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. stereo_charuco_pipeline-0.1.0/.gitignore +0 -0
  2. stereo_charuco_pipeline-0.1.0/PKG-INFO +10 -0
  3. stereo_charuco_pipeline-0.1.0/README.md +0 -0
  4. stereo_charuco_pipeline-0.1.0/configs/default.yaml +38 -0
  5. stereo_charuco_pipeline-0.1.0/pyproject.toml +36 -0
  6. stereo_charuco_pipeline-0.1.0/requirements.txt +4 -0
  7. stereo_charuco_pipeline-0.1.0/scripts/_test_config.py +32 -0
  8. stereo_charuco_pipeline-0.1.0/scripts/_test_full_pipeline.py +101 -0
  9. stereo_charuco_pipeline-0.1.0/scripts/_test_intrinsic.py +80 -0
  10. stereo_charuco_pipeline-0.1.0/scripts/convert_to_mp4.py +128 -0
  11. stereo_charuco_pipeline-0.1.0/scripts/probe_camera.py +110 -0
  12. stereo_charuco_pipeline-0.1.0/scripts/record_and_split.py +272 -0
  13. stereo_charuco_pipeline-0.1.0/scripts/record_raw.py +154 -0
  14. stereo_charuco_pipeline-0.1.0/scripts/run_calibration_ui.py +19 -0
  15. stereo_charuco_pipeline-0.1.0/scripts/run_calibration_ui_advanced.py +26 -0
  16. stereo_charuco_pipeline-0.1.0/scripts/run_pipeline_ui.py +39 -0
  17. stereo_charuco_pipeline-0.1.0/scripts/run_unified.py +95 -0
  18. stereo_charuco_pipeline-0.1.0/scripts/split_lr.py +163 -0
  19. stereo_charuco_pipeline-0.1.0/src/recorder/__init__.py +90 -0
  20. stereo_charuco_pipeline-0.1.0/src/recorder/auto_calibrate.py +493 -0
  21. stereo_charuco_pipeline-0.1.0/src/recorder/calibration_ui.py +1106 -0
  22. stereo_charuco_pipeline-0.1.0/src/recorder/calibration_ui_advanced.py +1013 -0
  23. stereo_charuco_pipeline-0.1.0/src/recorder/camera.py +51 -0
  24. stereo_charuco_pipeline-0.1.0/src/recorder/cli.py +122 -0
  25. stereo_charuco_pipeline-0.1.0/src/recorder/config.py +75 -0
  26. stereo_charuco_pipeline-0.1.0/src/recorder/configs/default.yaml +38 -0
  27. stereo_charuco_pipeline-0.1.0/src/recorder/ffmpeg.py +137 -0
  28. stereo_charuco_pipeline-0.1.0/src/recorder/paths.py +87 -0
  29. stereo_charuco_pipeline-0.1.0/src/recorder/pipeline_ui.py +1838 -0
  30. stereo_charuco_pipeline-0.1.0/src/recorder/project_manager.py +329 -0
  31. stereo_charuco_pipeline-0.1.0/src/recorder/smart_recorder.py +478 -0
  32. stereo_charuco_pipeline-0.1.0/src/recorder/ui.py +136 -0
  33. stereo_charuco_pipeline-0.1.0/src/recorder/viz_3d.py +220 -0
File without changes
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: stereo-charuco-pipeline
3
+ Version: 0.1.0
4
+ Summary: Stereo ChArUco 3D motion capture pipeline with auto-calibration
5
+ Requires-Python: <3.13,>=3.10
6
+ Requires-Dist: caliscope>=0.6.9
7
+ Requires-Dist: matplotlib>=3.7.0
8
+ Requires-Dist: opencv-contrib-python>=4.8.0.74
9
+ Requires-Dist: pillow>=10.0.0
10
+ Requires-Dist: pyyaml>=6.0
File without changes
@@ -0,0 +1,38 @@
1
+ camera:
2
+ device_name: "3D USB Camera"
3
+ backend: "dshow"
4
+ video_size: "3200x1200"
5
+ fps: 60
6
+
7
+ split:
8
+ full_w: 3200
9
+ full_h: 1200
10
+ xsplit: 1600
11
+
12
+ recording:
13
+ min_seconds: 30
14
+ target_seconds: 120
15
+ default_seconds: 120
16
+
17
+ output:
18
+ base_dir: "../../projects/caliscope/caliscope_project/recordings"
19
+ keep_avi: true
20
+
21
+ ffmpeg:
22
+ executable: "bin/ffmpeg/ffmpeg.exe"
23
+
24
+ charuco:
25
+ columns: 10
26
+ rows: 16
27
+ board_height_cm: 80.0
28
+ board_width_cm: 50.0
29
+ dictionary: "DICT_4X4_100"
30
+ aruco_scale: 0.7
31
+ square_size_cm: 5.0
32
+
33
+ mp4:
34
+ preset: "veryfast"
35
+ crf: 18
36
+
37
+ project:
38
+ project_dir: "../../projects/caliscope/caliscope_project"
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "stereo-charuco-pipeline"
3
+ version = "0.1.0"
4
+ description = "Stereo ChArUco 3D motion capture pipeline with auto-calibration"
5
+ requires-python = ">=3.10,<3.13"
6
+
7
+ dependencies = [
8
+ "caliscope>=0.6.9",
9
+ "pyyaml>=6.0",
10
+ "opencv-contrib-python>=4.8.0.74",
11
+ "Pillow>=10.0.0",
12
+ "matplotlib>=3.7.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ stereo-pipeline = "recorder.cli:main"
17
+ stereo-calibrate = "recorder.cli:calibrate_only"
18
+ stereo-record = "recorder.cli:pipeline_only"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/recorder"]
26
+
27
+ [tool.hatch.build.targets.wheel.sources]
28
+ "src" = ""
29
+
30
+ [tool.hatch.build.targets.sdist]
31
+ exclude = [
32
+ "bin/**",
33
+ "_test/**",
34
+ "**/*.mp4",
35
+ "**/*.avi",
36
+ ]
@@ -0,0 +1,4 @@
1
+ caliscope==0.6.9
2
+ pyyaml==6.0.2
3
+ opencv-python>=4.8.0
4
+ Pillow>=10.0.0
@@ -0,0 +1,32 @@
1
+ """Quick test: verify AutoCalibConfig from YAML."""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ tool_root = Path(__file__).resolve().parents[1]
6
+ sys.path.insert(0, str(tool_root / "src"))
7
+
8
+ from recorder.auto_calibrate import AutoCalibConfig
9
+ from recorder.config import load_yaml_config
10
+
11
+ yaml_data = load_yaml_config(tool_root / "configs" / "default.yaml")
12
+ project_dir = Path(r"D:\partition3\PHD_reserch\stereo-charuco-3d-pipeline\projects\caliscope\caliscope_project")
13
+ config = AutoCalibConfig.from_yaml(yaml_data, project_dir)
14
+
15
+ print(f"project_dir: {config.project_dir}")
16
+ print(f"charuco: {config.charuco_columns}x{config.charuco_rows}")
17
+ print(f"image_size: {config.image_size}")
18
+ print(f"ports: {config.ports}")
19
+ print(f"board: {config.board_width_cm}x{config.board_height_cm}cm")
20
+ print(f"dictionary: {config.dictionary}")
21
+ print(f"square_size: {config.square_size_cm}cm")
22
+
23
+ # Verify video files exist
24
+ intrinsic_dir = config.project_dir / "calibration" / "intrinsic"
25
+ extrinsic_dir = config.project_dir / "calibration" / "extrinsic"
26
+ for port in config.ports:
27
+ i_path = intrinsic_dir / f"port_{port}.mp4"
28
+ e_path = extrinsic_dir / f"port_{port}.mp4"
29
+ print(f"port_{port} intrinsic: {'OK' if i_path.exists() else 'MISSING'} ({i_path})")
30
+ print(f"port_{port} extrinsic: {'OK' if e_path.exists() else 'MISSING'} ({e_path})")
31
+
32
+ print("\nAll checks passed!")
@@ -0,0 +1,101 @@
1
+ """Test: run the full auto-calibration pipeline end-to-end."""
2
+ import sys
3
+ import logging
4
+ import time
5
+ from pathlib import Path
6
+
7
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
8
+
9
+ tool_root = Path(__file__).resolve().parents[1]
10
+ sys.path.insert(0, str(tool_root / "src"))
11
+
12
+ from recorder.auto_calibrate import AutoCalibConfig, run_auto_calibration
13
+ from recorder.config import load_yaml_config
14
+
15
+ yaml_data = load_yaml_config(tool_root / "configs" / "default.yaml")
16
+ project_dir = Path(r"D:\partition3\PHD_reserch\stereo-charuco-3d-pipeline\projects\caliscope\caliscope_project")
17
+ config = AutoCalibConfig.from_yaml(yaml_data, project_dir)
18
+
19
+ print("=" * 60)
20
+ print("FULL AUTO-CALIBRATION PIPELINE TEST")
21
+ print("=" * 60)
22
+ print(f"Project dir: {config.project_dir}")
23
+ print(f"Charuco: {config.charuco_columns}x{config.charuco_rows}")
24
+ print(f"Image size: {config.image_size}")
25
+ print(f"Ports: {config.ports}")
26
+ print(f"Intrinsic subsample: {config.intrinsic_subsample}")
27
+ print(f"Extrinsic subsample: {config.extrinsic_subsample}")
28
+ print()
29
+
30
+ # Verify all required videos exist
31
+ intrinsic_dir = config.project_dir / "calibration" / "intrinsic"
32
+ extrinsic_dir = config.project_dir / "calibration" / "extrinsic"
33
+ all_ok = True
34
+ for port in config.ports:
35
+ i_path = intrinsic_dir / f"port_{port}.mp4"
36
+ e_path = extrinsic_dir / f"port_{port}.mp4"
37
+ i_ok = i_path.exists()
38
+ e_ok = e_path.exists()
39
+ print(f" port_{port} intrinsic: {'OK' if i_ok else 'MISSING'}")
40
+ print(f" port_{port} extrinsic: {'OK' if e_ok else 'MISSING'}")
41
+ if not i_ok or not e_ok:
42
+ all_ok = False
43
+
44
+ if not all_ok:
45
+ print("\nERROR: Missing video files! Cannot run full pipeline test.")
46
+ sys.exit(1)
47
+
48
+ print("\nStarting pipeline...")
49
+ start_time = time.time()
50
+
51
+
52
+ def progress_callback(stage, msg, pct):
53
+ elapsed = time.time() - start_time
54
+ print(f" [{elapsed:6.1f}s] [{stage:15s}] ({pct:3d}%) {msg}")
55
+
56
+
57
+ result = run_auto_calibration(config, on_progress=progress_callback)
58
+
59
+ elapsed = time.time() - start_time
60
+ print()
61
+ print("=" * 60)
62
+ print(f"Pipeline finished in {elapsed:.1f}s")
63
+ print(f"Success: {result.success}")
64
+
65
+ if not result.success:
66
+ print(f"ERROR: {result.error_message}")
67
+ sys.exit(1)
68
+
69
+ print(f"\nIntrinsic RMSE:")
70
+ for port, rmse in result.intrinsic_rmse.items():
71
+ print(f" Port {port}: {rmse:.3f}px")
72
+
73
+ print(f"\nExtrinsic cost: {result.extrinsic_cost:.4f}")
74
+ print(f"Origin sync index: {result.origin_sync_index}")
75
+
76
+ print(f"\nCamera array ({len(result.camera_array.cameras)} cameras):")
77
+ for port, cam in result.camera_array.cameras.items():
78
+ print(f" Port {port}:")
79
+ print(f" Size: {cam.size}")
80
+ print(f" Matrix:\n {cam.matrix}")
81
+ print(f" Distortions: {cam.distortions}")
82
+ if cam.translation is not None:
83
+ print(f" Translation: {cam.translation.flatten()}")
84
+ if cam.rotation is not None:
85
+ print(f" Rotation: {cam.rotation.flatten()}")
86
+
87
+ # Verify saved files
88
+ print("\nVerifying saved files:")
89
+ saved_files = [
90
+ project_dir / "charuco.toml",
91
+ project_dir / "camera_array.toml",
92
+ extrinsic_dir / "frame_timestamps.csv",
93
+ extrinsic_dir / "CHARUCO" / "image_points.csv",
94
+ extrinsic_dir / "CHARUCO" / "camera_array.toml",
95
+ extrinsic_dir / "CHARUCO" / "world_points.csv",
96
+ ]
97
+ for f in saved_files:
98
+ status = "OK" if f.exists() else "MISSING"
99
+ print(f" {f.name}: {status}")
100
+
101
+ print("\nFull pipeline test PASSED!")
@@ -0,0 +1,80 @@
1
+ """Test: verify charuco detection + intrinsic calibration pipeline."""
2
+ import sys
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
7
+
8
+ tool_root = Path(__file__).resolve().parents[1]
9
+ sys.path.insert(0, str(tool_root / "src"))
10
+
11
+ from recorder.auto_calibrate import (
12
+ AutoCalibConfig,
13
+ _collect_charuco_points_from_video,
14
+ _build_image_points_from_packets,
15
+ )
16
+ from recorder.config import load_yaml_config
17
+ from caliscope.core.charuco import Charuco
18
+ from caliscope.trackers.charuco_tracker import CharucoTracker
19
+ from caliscope.cameras.camera_array import CameraData
20
+ from caliscope.core.calibrate_intrinsics import run_intrinsic_calibration
21
+
22
+ yaml_data = load_yaml_config(tool_root / "configs" / "default.yaml")
23
+ project_dir = Path(r"D:\partition3\PHD_reserch\stereo-charuco-3d-pipeline\projects\caliscope\caliscope_project")
24
+ config = AutoCalibConfig.from_yaml(yaml_data, project_dir)
25
+
26
+ print("Creating charuco + tracker...")
27
+ charuco = Charuco(
28
+ columns=config.charuco_columns,
29
+ rows=config.charuco_rows,
30
+ board_height=config.board_height_cm,
31
+ board_width=config.board_width_cm,
32
+ dictionary=config.dictionary,
33
+ units=config.units,
34
+ aruco_scale=config.aruco_scale,
35
+ square_size_overide_cm=config.square_size_cm,
36
+ )
37
+ tracker = CharucoTracker(charuco)
38
+ print(f"Charuco board: {charuco.columns}x{charuco.rows}, square_size={charuco.square_size_overide_cm}cm")
39
+
40
+ # Test charuco detection on port 1 intrinsic video (subsample=50 for speed)
41
+ intrinsic_dir = config.project_dir / "calibration" / "intrinsic"
42
+ port = 1
43
+
44
+ print(f"\nCollecting charuco points from port {port} (subsample=50 for test)...")
45
+ collected = _collect_charuco_points_from_video(
46
+ intrinsic_dir, port, tracker, subsample=50,
47
+ )
48
+ print(f"Collected {len(collected)} frames with detections")
49
+
50
+ if not collected:
51
+ print("ERROR: No charuco corners detected!")
52
+ sys.exit(1)
53
+
54
+ # Show sample detection
55
+ first_frame_idx, first_points = collected[0]
56
+ print(f"First detection at frame {first_frame_idx}: {len(first_points.point_id)} corners")
57
+ print(f" point_ids: {first_points.point_id[:5]}...")
58
+ print(f" img_loc shape: {first_points.img_loc.shape}")
59
+ print(f" obj_loc shape: {first_points.obj_loc.shape if first_points.obj_loc is not None else 'None'}")
60
+
61
+ # Build ImagePoints
62
+ print("\nBuilding ImagePoints DataFrame...")
63
+ image_points = _build_image_points_from_packets(collected, port)
64
+ print(f"ImagePoints: {len(image_points.df)} rows")
65
+ print(f"Columns: {list(image_points.df.columns)}")
66
+ print(f"Unique sync_indices: {image_points.df['sync_index'].nunique()}")
67
+
68
+ # Run intrinsic calibration
69
+ print("\nRunning intrinsic calibration...")
70
+ camera = CameraData(port=port, size=config.image_size, rotation_count=0)
71
+ output = run_intrinsic_calibration(camera, image_points)
72
+
73
+ print(f"\nCalibration result for port {port}:")
74
+ print(f" RMSE: {output.report.rmse:.3f}px")
75
+ print(f" Frames used: {output.report.frames_used}")
76
+ print(f" Coverage: {output.report.coverage_fraction:.0%}")
77
+ print(f" Matrix:\n {output.camera.matrix}")
78
+ print(f" Distortions: {output.camera.distortions}")
79
+
80
+ print("\nIntrinsic calibration test PASSED!")
@@ -0,0 +1,128 @@
1
+ import argparse
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+ try:
7
+ import yaml
8
+ except ImportError:
9
+ yaml = None
10
+
11
+
12
+ def _load_yaml(path: str) -> dict:
13
+ if not path:
14
+ return {}
15
+ if yaml is None:
16
+ raise RuntimeError("PyYAML is not installed. Please: pip install pyyaml")
17
+ if not os.path.exists(path):
18
+ raise FileNotFoundError(f"Config file not found: {path}")
19
+ with open(path, "r", encoding="utf-8") as f:
20
+ data = yaml.safe_load(f) or {}
21
+ if not isinstance(data, dict):
22
+ raise ValueError("Config YAML root must be a mapping (dict).")
23
+ return data
24
+
25
+
26
+ def _get(cfg: dict, keys: list, default=None):
27
+ cur = cfg
28
+ for k in keys:
29
+ if not isinstance(cur, dict) or k not in cur:
30
+ return default
31
+ cur = cur[k]
32
+ return cur
33
+
34
+
35
+ def _resolve_tool_root() -> str:
36
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
37
+
38
+
39
+ def _resolve_ffmpeg(ffmpeg_exe: str, tool_root: str) -> str:
40
+ if not ffmpeg_exe:
41
+ return "ffmpeg"
42
+ if os.path.isabs(ffmpeg_exe):
43
+ return ffmpeg_exe
44
+ candidate = os.path.abspath(os.path.join(tool_root, ffmpeg_exe))
45
+ if os.path.exists(candidate):
46
+ return candidate
47
+ return ffmpeg_exe
48
+
49
+
50
+ def run():
51
+ ap = argparse.ArgumentParser(description="Convert AVI (MJPEG) to MP4 (H.264). Supports configs/default.yaml.")
52
+ ap.add_argument("--config", default="configs/default.yaml", help="path to YAML config (relative to tool root ok)")
53
+
54
+ ap.add_argument("--in", dest="inp", required=True, help="input AVI path")
55
+ ap.add_argument("--out", required=True, help="output MP4 path")
56
+
57
+ ap.add_argument("--ffmpeg", default=None, help="ffmpeg executable or relative path")
58
+ ap.add_argument("--preset", default=None, help="x264 preset")
59
+ ap.add_argument("--crf", type=int, default=None, help="x264 CRF (lower=better)")
60
+
61
+ args = ap.parse_args()
62
+
63
+ tool_root = _resolve_tool_root()
64
+
65
+ cfg_path = args.config
66
+ if cfg_path and not os.path.isabs(cfg_path):
67
+ cfg_path = os.path.join(tool_root, cfg_path)
68
+ cfg = _load_yaml(cfg_path)
69
+
70
+ cfg_ffmpeg = _get(cfg, ["ffmpeg", "executable"], "ffmpeg")
71
+ cfg_ffmpeg = _resolve_ffmpeg(cfg_ffmpeg, tool_root)
72
+
73
+ cfg_preset = _get(cfg, ["mp4", "preset"], _get(cfg, ["encode", "preset"], "veryfast"))
74
+ cfg_crf = int(_get(cfg, ["mp4", "crf"], _get(cfg, ["encode", "crf"], 18)))
75
+
76
+ ffmpeg_exe = args.ffmpeg if args.ffmpeg is not None else cfg_ffmpeg
77
+ ffmpeg_exe = _resolve_ffmpeg(ffmpeg_exe, tool_root)
78
+
79
+ preset = args.preset if args.preset is not None else cfg_preset
80
+ crf = args.crf if args.crf is not None else cfg_crf
81
+
82
+ inp = args.inp
83
+ outp = args.out
84
+
85
+ # resolve output directory
86
+ outdir = os.path.dirname(os.path.abspath(outp))
87
+ if outdir:
88
+ os.makedirs(outdir, exist_ok=True)
89
+
90
+ cmd = [
91
+ ffmpeg_exe, "-hide_banner", "-y",
92
+ "-i", inp,
93
+ "-c:v", "libx264", "-preset", preset, "-crf", str(crf),
94
+ "-pix_fmt", "yuv420p",
95
+ outp
96
+ ]
97
+
98
+ print("=== Effective Settings ===")
99
+ print(f"config : {cfg_path}")
100
+ print(f"ffmpeg : {ffmpeg_exe}")
101
+ print(f"preset: {preset}")
102
+ print(f"crf : {crf}")
103
+ print(f"in : {inp}")
104
+ print(f"out : {outp}")
105
+ print("==========================")
106
+
107
+ print("[INFO] Converting AVI -> MP4 (H.264).")
108
+ print("[INFO] Command:")
109
+ print(" " + " ".join(cmd))
110
+
111
+ r = subprocess.run(cmd, check=False)
112
+ if r.returncode != 0:
113
+ print(f"[ERROR] convert failed, code={r.returncode}")
114
+ sys.exit(r.returncode)
115
+
116
+ print("[INFO] Done.")
117
+ print(outp)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ try:
122
+ run()
123
+ except FileNotFoundError:
124
+ print("[ERROR] ffmpeg not found. Check default.yaml ffmpeg.executable or pass --ffmpeg.")
125
+ sys.exit(1)
126
+ except Exception as e:
127
+ print(f"[ERROR] {e}")
128
+ sys.exit(1)
@@ -0,0 +1,110 @@
1
+ import cv2
2
+ import time
3
+
4
+ def fourcc_to_str(v):
5
+ v = int(v)
6
+ return "".join([chr((v >> 8 * i) & 0xFF) for i in range(4)])
7
+
8
+ def probe_camera_force(
9
+ camera_index=1,
10
+ backend=cv2.CAP_DSHOW,
11
+ req_w=3200,
12
+ req_h=1200,
13
+ req_fps=60,
14
+ req_fourcc="MJPG",
15
+ probe_seconds=5.0,
16
+ warmup_seconds=0.7,
17
+ ):
18
+ print("=== Camera Probe (Forced Mode) ===")
19
+ print(f"camera_index = {camera_index}")
20
+ print(f"backend = {backend}")
21
+ print(f"request = {req_w}x{req_h} @ {req_fps}fps, FOURCC={req_fourcc}")
22
+
23
+ cap = cv2.VideoCapture(camera_index, backend)
24
+ if not cap.isOpened():
25
+ print("[ERROR] Cannot open camera")
26
+ return
27
+
28
+ # Force codec (critical for your camera to reach 3200x1200@60)
29
+ cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*req_fourcc))
30
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, req_w)
31
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, req_h)
32
+ cap.set(cv2.CAP_PROP_FPS, req_fps)
33
+
34
+ # Read one frame to finalize negotiation
35
+ ok, frame = cap.read()
36
+ if not ok:
37
+ print("[ERROR] Cannot read frame after setting properties")
38
+ cap.release()
39
+ return
40
+
41
+ H, W = frame.shape[:2]
42
+ got_fps = cap.get(cv2.CAP_PROP_FPS)
43
+ got_fourcc = fourcc_to_str(cap.get(cv2.CAP_PROP_FOURCC))
44
+
45
+ print(f"[NEGOTIATED] size = {W} x {H}")
46
+ print(f"[NEGOTIATED] fps = {got_fps} (often 0.0 on some drivers; measured fps below is more reliable)")
47
+ print(f"[NEGOTIATED] fourcc = {got_fourcc}")
48
+
49
+ print("[INFO] Probing... press 'q' to quit")
50
+
51
+ t0 = time.perf_counter()
52
+ n_total = 0
53
+ n_measured = 0
54
+ t_measure_start = None
55
+
56
+ while True:
57
+ ok, frame = cap.read()
58
+ if not ok:
59
+ print("[WARN] Frame read failed")
60
+ break
61
+
62
+ n_total += 1
63
+ now = time.perf_counter()
64
+ elapsed = now - t0
65
+
66
+ # Warmup: ignore initial frames for FPS measurement
67
+ if elapsed >= warmup_seconds:
68
+ if t_measure_start is None:
69
+ t_measure_start = now
70
+ n_measured += 1
71
+
72
+ # Draw split line
73
+ mid_x = frame.shape[1] // 2
74
+ cv2.line(frame, (mid_x, 0), (mid_x, frame.shape[0]-1), (0, 255, 0), 2)
75
+
76
+ # Overlay
77
+ cv2.putText(frame, f"{frame.shape[1]}x{frame.shape[0]} total={n_total}",
78
+ (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), 2)
79
+
80
+ cv2.imshow("Camera Probe Forced (press q)", frame)
81
+
82
+ if cv2.waitKey(1) & 0xFF == ord("q"):
83
+ break
84
+
85
+ if elapsed >= probe_seconds:
86
+ break
87
+
88
+ if t_measure_start is not None:
89
+ t_end = time.perf_counter()
90
+ measured_fps = n_measured / max(t_end - t_measure_start, 1e-6)
91
+ print(f"[MEASURED] FPS ~ {measured_fps:.2f} (after {warmup_seconds:.1f}s warmup)")
92
+ else:
93
+ print("[MEASURED] Not enough time to measure FPS (warmup too long or too short probe)")
94
+
95
+ cap.release()
96
+ cv2.destroyAllWindows()
97
+ print("=== Probe Finished ===")
98
+
99
+
100
+ if __name__ == "__main__":
101
+ # 你已经确认 FFmpeg 列表支持 3200x1200 mjpeg 60fps,所以这里直接按该模式 probe
102
+ probe_camera_force(
103
+ camera_index=1,
104
+ backend=cv2.CAP_DSHOW,
105
+ req_w=3200,
106
+ req_h=1200,
107
+ req_fps=60,
108
+ req_fourcc="MJPG",
109
+ probe_seconds=5.0,
110
+ )