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.
- stereo_charuco_pipeline-0.1.0/.gitignore +0 -0
- stereo_charuco_pipeline-0.1.0/PKG-INFO +10 -0
- stereo_charuco_pipeline-0.1.0/README.md +0 -0
- stereo_charuco_pipeline-0.1.0/configs/default.yaml +38 -0
- stereo_charuco_pipeline-0.1.0/pyproject.toml +36 -0
- stereo_charuco_pipeline-0.1.0/requirements.txt +4 -0
- stereo_charuco_pipeline-0.1.0/scripts/_test_config.py +32 -0
- stereo_charuco_pipeline-0.1.0/scripts/_test_full_pipeline.py +101 -0
- stereo_charuco_pipeline-0.1.0/scripts/_test_intrinsic.py +80 -0
- stereo_charuco_pipeline-0.1.0/scripts/convert_to_mp4.py +128 -0
- stereo_charuco_pipeline-0.1.0/scripts/probe_camera.py +110 -0
- stereo_charuco_pipeline-0.1.0/scripts/record_and_split.py +272 -0
- stereo_charuco_pipeline-0.1.0/scripts/record_raw.py +154 -0
- stereo_charuco_pipeline-0.1.0/scripts/run_calibration_ui.py +19 -0
- stereo_charuco_pipeline-0.1.0/scripts/run_calibration_ui_advanced.py +26 -0
- stereo_charuco_pipeline-0.1.0/scripts/run_pipeline_ui.py +39 -0
- stereo_charuco_pipeline-0.1.0/scripts/run_unified.py +95 -0
- stereo_charuco_pipeline-0.1.0/scripts/split_lr.py +163 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/__init__.py +90 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/auto_calibrate.py +493 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/calibration_ui.py +1106 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/calibration_ui_advanced.py +1013 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/camera.py +51 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/cli.py +122 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/config.py +75 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/configs/default.yaml +38 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/ffmpeg.py +137 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/paths.py +87 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/pipeline_ui.py +1838 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/project_manager.py +329 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/smart_recorder.py +478 -0
- stereo_charuco_pipeline-0.1.0/src/recorder/ui.py +136 -0
- 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,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
|
+
)
|