"""Streaming CSV detection logger.""" import csv from pathlib import Path from typing import TextIO from .config_loader import DetectionLoggingSettings from .models import Detection, FrameInfo class DetectionLogger: """Write one CSV row per detection, with optional empty-frame rows.""" FIELDNAMES = [ "experiment_id", "frame_number", "timestamp", "elapsed_time_sec", "frame_width", "frame_height", "measured_fps", "detection_index", "class_id", "class_name", "confidence", "bbox_x1", "bbox_y1", "bbox_x2", "bbox_y2", "bbox_width", "bbox_height", "center_x", "center_y", "foot_x", "foot_y", "inside_evaluation_region", ] def __init__(self, config: DetectionLoggingSettings, experiment_id: str, path: Path) -> None: self.config = config self.experiment_id = experiment_id self.path = path self._file: TextIO | None = None self._writer: csv.DictWriter[str] | None = None self._frames_since_flush = 0 def open(self) -> None: self.path.parent.mkdir(parents=True, exist_ok=True) self._file = self.path.open("w", encoding="utf-8-sig", newline="") self._writer = csv.DictWriter(self._file, fieldnames=self.FIELDNAMES) self._writer.writeheader() def log_frame(self, info: FrameInfo, detections: list[Detection]) -> None: if self._writer is None: return base = { "experiment_id": self.experiment_id, "frame_number": info.frame_number, "timestamp": info.timestamp_iso, "elapsed_time_sec": f"{info.elapsed_time_sec:.6f}", "frame_width": info.width, "frame_height": info.height, "measured_fps": f"{info.measured_fps:.3f}", } if detections: for index, detection in enumerate(detections): b = detection.bbox self._writer.writerow(base | { "detection_index": index, "class_id": detection.class_id, "class_name": detection.class_name, "confidence": f"{detection.confidence:.6f}", "bbox_x1": b.x1, "bbox_y1": b.y1, "bbox_x2": b.x2, "bbox_y2": b.y2, "bbox_width": b.width, "bbox_height": b.height, "center_x": b.center_x, "center_y": b.center_y, "foot_x": b.foot_x, "foot_y": b.foot_y, "inside_evaluation_region": detection.inside_evaluation_region, }) elif self.config.log_empty_frames: self._writer.writerow(base) self._frames_since_flush += 1 if self._frames_since_flush >= self.config.flush_interval_frames: self._file.flush() self._frames_since_flush = 0 def close(self) -> None: if self._file is not None: self._file.close() self._file = None self._writer = None def __enter__(self) -> "DetectionLogger": self.open() return self def __exit__(self, *_args: object) -> None: self.close()