完善 Autocal 多测点标定流程及会话复核
统一测点坐标、保持窗口和试验目录,补齐逐点初步分析。定向测试与真实视频闭环通过;两项轮询失败在修改前版本同样复现。
This commit is contained in:
@@ -147,6 +147,9 @@ class RecordConfig:
|
||||
#: ``preroll_bytes`` so a large frame does not turn into a large buffer.
|
||||
preroll_frames: int = 30
|
||||
preroll_bytes: int = 192 * 1024 * 1024
|
||||
#: Keep each delivered image for calibration. Container fps then describes
|
||||
#: nominal playback; frames.csv carries the actual acquisition timing.
|
||||
preserve_all_frames: bool = False
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
return {
|
||||
@@ -159,6 +162,7 @@ class RecordConfig:
|
||||
"max_queue_bytes": int(self.max_queue_bytes),
|
||||
"preroll_frames": int(self.preroll_frames),
|
||||
"preroll_bytes": int(self.preroll_bytes),
|
||||
"preserve_all_frames": bool(self.preserve_all_frames),
|
||||
"lossy_note": (
|
||||
"MP4 is a lossy container: this is the pre-processing camera "
|
||||
"image, not a lossless record of the raw sensor data."
|
||||
@@ -346,13 +350,12 @@ class ExperimentRecorder:
|
||||
calibration_version: str = "",
|
||||
preroll=(),
|
||||
directory_label: str = "",
|
||||
directory_name: str = "",
|
||||
) -> Path:
|
||||
"""Create the experiment directory and start the writer thread."""
|
||||
if directory_label and (
|
||||
directory_label in {".", ".."}
|
||||
or not all(char.isalnum() or char in "-_" for char in directory_label)
|
||||
):
|
||||
raise ValueError("directory label must contain only letters, numbers, - or _")
|
||||
for value in (directory_label, directory_name):
|
||||
if value and not all(char.isalnum() or char in "-_" for char in value):
|
||||
raise ValueError("directory names must contain only letters, numbers, - or _")
|
||||
# A previous writer that has not reached a terminal state still owns the
|
||||
# files and the statistics; reusing it would silently mix two sessions.
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
@@ -370,7 +373,9 @@ class ExperimentRecorder:
|
||||
self.config = replace(config if config is not None else self.config)
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
label = f"_{directory_label}" if directory_label else ""
|
||||
directory = Path(root) / f"{stamp}{label}_session{session_id:04d}_{uuid4().hex}"
|
||||
directory = Path(root) / (
|
||||
directory_name or f"{stamp}{label}_session{session_id:04d}_{uuid4().hex}"
|
||||
)
|
||||
try:
|
||||
directory.mkdir(parents=True, exist_ok=False)
|
||||
except Exception as exc: # noqa: BLE001 - reported to the caller
|
||||
@@ -599,7 +604,11 @@ class ExperimentRecorder:
|
||||
|
||||
A skipped frame is not lost data: every row is still in
|
||||
``frames.csv``/``forces.csv``, and the count is reported separately.
|
||||
Calibration may preserve every image instead and use the frame index's
|
||||
host timestamps for time alignment rather than container playback time.
|
||||
"""
|
||||
if self.config.preserve_all_frames:
|
||||
return True
|
||||
try:
|
||||
stamp = float(mono_t)
|
||||
except (TypeError, ValueError):
|
||||
|
||||
@@ -288,3 +288,39 @@ if __name__ == "__main__": # pragma: no cover - manual runner
|
||||
print(f" FAIL {name}: {type(exc).__name__}: {exc}")
|
||||
print(f"failures: {failures}")
|
||||
raise SystemExit(1 if failures else 0)
|
||||
|
||||
|
||||
def test_named_trial_directory_is_safe_and_never_overwritten(tmp_path):
|
||||
recorder = ExperimentRecorder(RecordConfig(video=False))
|
||||
for value in ("../trial", "a/b", "..", "C:\\trial"):
|
||||
with pytest.raises(ValueError):
|
||||
recorder.start(root=tmp_path, session_id=1, metadata={}, directory_name=value)
|
||||
directory = recorder.start(root=tmp_path, session_id=1, metadata={}, directory_name="trial-001")
|
||||
recorder.on_packet(_packet(0))
|
||||
recorder.stop(wait=True)
|
||||
assert directory == tmp_path / "trial-001"
|
||||
with pytest.raises(FileExistsError):
|
||||
recorder.start(root=tmp_path, session_id=1, metadata={}, directory_name="trial-001")
|
||||
assert (directory / "session.json").is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preserve", [False, True])
|
||||
def test_calibration_can_keep_burst_frames_without_changing_default_pacing(tmp_path, preserve):
|
||||
from dataclasses import replace
|
||||
|
||||
recorder = ExperimentRecorder(RecordConfig(fps=30, preserve_all_frames=preserve))
|
||||
directory = recorder.start(root=tmp_path, session_id=1, metadata={})
|
||||
for index in range(10):
|
||||
recorder.on_packet(replace(_packet(index), mono_t=index / 300.0))
|
||||
report = recorder.stop(wait=True)
|
||||
assert report.stats.frames == 10
|
||||
if preserve:
|
||||
assert report.stats.video_frames == 10
|
||||
assert report.stats.resampled_video == 0
|
||||
assert report.ok
|
||||
else:
|
||||
assert report.stats.video_frames < 10
|
||||
assert report.stats.resampled_video > 0
|
||||
with (directory / "frames.csv").open(encoding="utf-8") as handle:
|
||||
rows = list(csv.DictReader(handle))
|
||||
assert sum(row["in_video"] == "1" for row in rows) == report.stats.video_frames
|
||||
|
||||
Reference in New Issue
Block a user