81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
import csv
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.config_loader import FaceLoggingSettings
|
|
from src.face_logger import FaceLogger
|
|
from src.models import BoundingBox, FaceROI, HeadPoseResult
|
|
from src.yaw_noise_report import (
|
|
ema, load_tracks, main, median_then_ema, one_euro, percentile, summarize,
|
|
)
|
|
|
|
|
|
def test_percentile_interpolates() -> None:
|
|
assert percentile([0.0, 10.0], 50) == pytest.approx(5.0)
|
|
assert percentile([1.0, 2.0, 3.0], 0) == 1.0
|
|
assert percentile([1.0, 2.0, 3.0], 100) == 3.0
|
|
|
|
|
|
def test_summarize_constant_and_alternating_series() -> None:
|
|
constant = summarize([2.0] * 5)
|
|
assert constant is not None and constant["sd"] == 0.0 and constant["p2p"] == 0.0
|
|
alternating = summarize([0.0, 2.0, 0.0, 2.0])
|
|
assert alternating is not None
|
|
assert alternating["p2p"] == 2.0 and alternating["mean"] == 1.0
|
|
assert summarize([1.0]) is None
|
|
|
|
|
|
def test_filters_preserve_length_and_constant_signal() -> None:
|
|
values = [3.0] * 6
|
|
samples = [(i * 0.05, 3.0) for i in range(6)]
|
|
assert ema(values, 0.4) == pytest.approx(values)
|
|
assert median_then_ema(values, 0.4) == pytest.approx(values)
|
|
assert one_euro(samples, 1.0, 0.0) == pytest.approx(values)
|
|
assert ema([1.0, 5.0], 1.0) == [1.0, 5.0]
|
|
|
|
|
|
def test_ema_reduces_alternating_noise() -> None:
|
|
noisy = [0.0, 4.0] * 20
|
|
raw_sd = summarize(noisy)["sd"] # type: ignore[index]
|
|
assert summarize(ema(noisy, 0.2))["sd"] < raw_sd # type: ignore[index]
|
|
|
|
|
|
def _result(frame: int, t: float, yaw: float, raw: float | None) -> HeadPoseResult:
|
|
roi = FaceROI(1, 10, 10, 110, 110, BoundingBox(0, 0, 200, 400), .9)
|
|
return HeadPoseResult(1, frame, "now", t, True, True, 0.0, yaw, 0.0, 468, roi, .9,
|
|
raw_yaw=raw, reprojection_error=1.5, interocular_px=60.0)
|
|
|
|
|
|
def test_logger_writes_diagnostic_columns_and_report_reads_them(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
logger = FaceLogger(FaceLoggingSettings(True, True, True, 1, str(tmp_path)), "exp", "20260101_000000")
|
|
logger.open()
|
|
raws = [0.0, 2.0, -2.0, 1.0, -1.0, 0.5]
|
|
logger.write_results([_result(i + 1, i * 0.1, r * 0.5, r) for i, r in enumerate(raws)])
|
|
logger.close()
|
|
path = next(tmp_path.glob("*face_frames.csv"))
|
|
with path.open(encoding="utf-8-sig", newline="") as stream:
|
|
rows = list(csv.DictReader(stream))
|
|
assert rows[1]["raw_yaw"] == "2.000000" and rows[1]["interocular_px"] == "60.000000"
|
|
assert rows[0]["bbox_center_x"] == "100" and rows[0]["reprojection_error"] == "1.500000"
|
|
|
|
tracks = load_tracks(path)
|
|
assert len(tracks[1]["raw"]) == 6
|
|
trimmed = load_tracks(path, track_id=1, t_start=0.2)
|
|
assert len(trimmed[1]["raw"]) == 4
|
|
assert main([str(path), "--track-id", "1", "--csv", str(tmp_path / "out.csv")]) == 0
|
|
assert "raw + OneEuro" in capsys.readouterr().out
|
|
assert (tmp_path / "out.csv").is_file()
|
|
|
|
|
|
def test_report_handles_old_logs_without_raw_columns(tmp_path: Path) -> None:
|
|
path = tmp_path / "old_face_frames.csv"
|
|
path.write_text(
|
|
"track_id,pose_estimated,elapsed_time_sec,yaw\n1,True,0.0,1.0\n1,True,0.1,2.0\n1,False,0.2,\n",
|
|
encoding="utf-8-sig",
|
|
)
|
|
tracks = load_tracks(path)
|
|
assert len(tracks[1]["logged"]) == 2 and tracks[1]["raw"] == []
|
|
assert main([str(path)]) == 0
|
|
assert main([str(tmp_path / "missing.csv")]) == 2
|