feat(demo): add export and silhouette visualization outputs
Add preprocess-only silhouette export and configurable result exporters so demo runs can be persisted for offline analysis and reproducible evaluation. Include optional parquet support and CLI visualization dumps while updating tests and tracking notes for the verified pipeline/debug workflow.
This commit is contained in:
+410
-1
@@ -1,6 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Final, cast
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from opengait.demo.sconet_demo import ScoNetDemo
|
||||
|
||||
import json
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -105,7 +120,6 @@ def _assert_prediction_schema(prediction: dict[str, object]) -> None:
|
||||
|
||||
assert isinstance(prediction["timestamp_ns"], int)
|
||||
|
||||
|
||||
def test_pipeline_cli_fps_benchmark_smoke(
|
||||
compatible_checkpoint_path: Path,
|
||||
) -> None:
|
||||
@@ -277,3 +291,398 @@ def test_pipeline_cli_invalid_checkpoint_path_returns_user_error() -> None:
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "Error: Checkpoint not found" in result.stderr
|
||||
|
||||
|
||||
|
||||
def test_pipeline_cli_preprocess_only_requires_export_path(
|
||||
compatible_checkpoint_path: Path,
|
||||
) -> None:
|
||||
"""Test that --preprocess-only requires --silhouette-export-path."""
|
||||
_require_integration_assets()
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--preprocess-only",
|
||||
"--max-frames",
|
||||
"10",
|
||||
timeout_seconds=30,
|
||||
)
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "--silhouette-export-path is required" in result.stderr
|
||||
|
||||
|
||||
def test_pipeline_cli_preprocess_only_exports_pickle(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test preprocess-only mode exports silhouettes to pickle."""
|
||||
_require_integration_assets()
|
||||
|
||||
export_path = tmp_path / "silhouettes.pkl"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--preprocess-only",
|
||||
"--silhouette-export-path",
|
||||
str(export_path),
|
||||
"--silhouette-export-format",
|
||||
"pickle",
|
||||
"--max-frames",
|
||||
"30",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify export file exists and contains silhouettes
|
||||
assert export_path.is_file(), f"Export file not found: {export_path}"
|
||||
|
||||
with open(export_path, "rb") as f:
|
||||
silhouettes = pickle.load(f)
|
||||
|
||||
assert isinstance(silhouettes, list)
|
||||
assert len(silhouettes) > 0, "Expected at least one silhouette"
|
||||
|
||||
# Verify silhouette schema
|
||||
for item in silhouettes:
|
||||
assert isinstance(item, dict)
|
||||
assert "frame" in item
|
||||
assert "track_id" in item
|
||||
assert "timestamp_ns" in item
|
||||
assert "silhouette" in item
|
||||
assert isinstance(item["frame"], int)
|
||||
assert isinstance(item["track_id"], int)
|
||||
assert isinstance(item["timestamp_ns"], int)
|
||||
|
||||
|
||||
def test_pipeline_cli_result_export_json(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that results can be exported to JSON file."""
|
||||
_require_integration_assets()
|
||||
|
||||
export_path = tmp_path / "results.jsonl"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--window",
|
||||
"10",
|
||||
"--stride",
|
||||
"10",
|
||||
"--result-export-path",
|
||||
str(export_path),
|
||||
"--result-export-format",
|
||||
"json",
|
||||
"--max-frames",
|
||||
"60",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify export file exists
|
||||
assert export_path.is_file(), f"Export file not found: {export_path}"
|
||||
|
||||
# Read and verify JSON lines
|
||||
predictions: list[dict[str, object]] = []
|
||||
with open(export_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
predictions.append(cast(dict[str, object], json.loads(line)))
|
||||
|
||||
assert len(predictions) > 0, "Expected at least one prediction in export"
|
||||
|
||||
for prediction in predictions:
|
||||
_assert_prediction_schema(prediction)
|
||||
|
||||
|
||||
def test_pipeline_cli_result_export_pickle(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that results can be exported to pickle file."""
|
||||
_require_integration_assets()
|
||||
|
||||
export_path = tmp_path / "results.pkl"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--window",
|
||||
"10",
|
||||
"--stride",
|
||||
"10",
|
||||
"--result-export-path",
|
||||
str(export_path),
|
||||
"--result-export-format",
|
||||
"pickle",
|
||||
"--max-frames",
|
||||
"60",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify export file exists
|
||||
assert export_path.is_file(), f"Export file not found: {export_path}"
|
||||
|
||||
# Read and verify pickle
|
||||
with open(export_path, "rb") as f:
|
||||
predictions = pickle.load(f)
|
||||
|
||||
assert isinstance(predictions, list)
|
||||
assert len(predictions) > 0, "Expected at least one prediction in export"
|
||||
|
||||
for prediction in predictions:
|
||||
_assert_prediction_schema(prediction)
|
||||
|
||||
|
||||
def test_pipeline_cli_silhouette_and_result_export(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test exporting both silhouettes and results simultaneously."""
|
||||
_require_integration_assets()
|
||||
|
||||
silhouette_export = tmp_path / "silhouettes.pkl"
|
||||
result_export = tmp_path / "results.jsonl"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--window",
|
||||
"10",
|
||||
"--stride",
|
||||
"10",
|
||||
"--silhouette-export-path",
|
||||
str(silhouette_export),
|
||||
"--silhouette-export-format",
|
||||
"pickle",
|
||||
"--result-export-path",
|
||||
str(result_export),
|
||||
"--result-export-format",
|
||||
"json",
|
||||
"--max-frames",
|
||||
"60",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify both export files exist
|
||||
assert silhouette_export.is_file(), f"Silhouette export not found: {silhouette_export}"
|
||||
assert result_export.is_file(), f"Result export not found: {result_export}"
|
||||
|
||||
# Verify silhouette export
|
||||
with open(silhouette_export, "rb") as f:
|
||||
silhouettes = pickle.load(f)
|
||||
assert isinstance(silhouettes, list)
|
||||
assert len(silhouettes) > 0
|
||||
|
||||
# Verify result export
|
||||
with open(result_export, "r", encoding="utf-8") as f:
|
||||
predictions = [cast(dict[str, object], json.loads(line)) for line in f if line.strip()]
|
||||
assert len(predictions) > 0
|
||||
|
||||
|
||||
def test_pipeline_cli_parquet_export_requires_pyarrow(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that parquet export fails gracefully when pyarrow is not available."""
|
||||
_require_integration_assets()
|
||||
|
||||
# Skip if pyarrow is actually installed
|
||||
if importlib.util.find_spec("pyarrow") is not None:
|
||||
pytest.skip("pyarrow is installed, skipping missing dependency test")
|
||||
try:
|
||||
import pyarrow # noqa: F401
|
||||
pytest.skip("pyarrow is installed, skipping missing dependency test")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
export_path = tmp_path / "results.parquet"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--window",
|
||||
"10",
|
||||
"--stride",
|
||||
"10",
|
||||
"--result-export-path",
|
||||
str(export_path),
|
||||
"--result-export-format",
|
||||
"parquet",
|
||||
"--max-frames",
|
||||
"30",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
# Should fail with RuntimeError about pyarrow
|
||||
assert result.returncode == 1
|
||||
assert "parquet" in result.stderr.lower() or "pyarrow" in result.stderr.lower()
|
||||
|
||||
|
||||
|
||||
def test_pipeline_cli_silhouette_visualization(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that silhouette visualization creates PNG files."""
|
||||
_require_integration_assets()
|
||||
|
||||
visualize_dir = tmp_path / "silhouette_viz"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--window",
|
||||
"10",
|
||||
"--stride",
|
||||
"10",
|
||||
"--silhouette-visualize-dir",
|
||||
str(visualize_dir),
|
||||
"--max-frames",
|
||||
"30",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify visualization directory exists and contains PNG files
|
||||
assert visualize_dir.is_dir(), f"Visualization directory not found: {visualize_dir}"
|
||||
|
||||
png_files = list(visualize_dir.glob("*.png"))
|
||||
assert len(png_files) > 0, "Expected at least one PNG visualization file"
|
||||
|
||||
# Verify filenames contain frame and track info
|
||||
for png_file in png_files:
|
||||
assert "silhouette_frame" in png_file.name
|
||||
assert "_track" in png_file.name
|
||||
|
||||
|
||||
def test_pipeline_cli_preprocess_only_with_visualization(
|
||||
compatible_checkpoint_path: Path,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test preprocess-only mode with both export and visualization."""
|
||||
_require_integration_assets()
|
||||
|
||||
export_path = tmp_path / "silhouettes.pkl"
|
||||
visualize_dir = tmp_path / "silhouette_viz"
|
||||
|
||||
result = _run_pipeline_cli(
|
||||
"--source",
|
||||
str(SAMPLE_VIDEO_PATH),
|
||||
"--checkpoint",
|
||||
str(compatible_checkpoint_path),
|
||||
"--config",
|
||||
str(CONFIG_PATH),
|
||||
"--device",
|
||||
_device_for_runtime(),
|
||||
"--yolo-model",
|
||||
str(YOLO_MODEL_PATH),
|
||||
"--preprocess-only",
|
||||
"--silhouette-export-path",
|
||||
str(export_path),
|
||||
"--silhouette-visualize-dir",
|
||||
str(visualize_dir),
|
||||
"--max-frames",
|
||||
"30",
|
||||
timeout_seconds=180,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, (
|
||||
f"Expected exit code 0, got {result.returncode}. stderr:\n{result.stderr}"
|
||||
)
|
||||
|
||||
# Verify export file exists
|
||||
assert export_path.is_file(), f"Export file not found: {export_path}"
|
||||
|
||||
# Verify visualization files exist
|
||||
assert visualize_dir.is_dir(), f"Visualization directory not found: {visualize_dir}"
|
||||
png_files = list(visualize_dir.glob("*.png"))
|
||||
assert len(png_files) > 0, "Expected at least one PNG visualization file"
|
||||
|
||||
# Load and verify pickle export
|
||||
with open(export_path, "rb") as f:
|
||||
silhouettes = pickle.load(f)
|
||||
assert isinstance(silhouettes, list)
|
||||
assert len(silhouettes) > 0
|
||||
# Number of exported silhouettes should match number of PNG files
|
||||
assert len(silhouettes) == len(png_files), (
|
||||
f"Mismatch: {len(silhouettes)} silhouettes exported but {len(png_files)} PNG files created"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user