perf: throttle tracker to every 2nd frame, sup interval 10, 720p output option
- detect_interval=2 on both pipelines; stabilizer 10-frame hold bridges skipped frames (GPU inference cut ~half during active batch) - sup_det_interval 5 -> 10 (still <= stabilizer hold / synthetic max_age) - preview_every_n default 2 -> 5 - AnnotatedVideoWriter: codec auto-chain gstreamer_nvenc -> avc1 -> mp4v, max_height downscale (even dims, INTER_AREA); backend logged - output_max_height plumbed Job -> web checkbox (720p) -> CLI --output-height - README: 720p option, CLI flag - 153 tests pass (+13)
This commit is contained in:
1 parent
ad41bf01e1
commit
07289a419a
10 files changed
+345
-19
No files matched your search
@@ -235,3 +235,128 @@ def test_run_job_merged_propagates_box_counts(tmp_path, monkeypatch):
|
||||
assert r.box_loading_count == 6
|
||||
assert r.box_unloading_count == 2
|
||||
assert r.box_net_count == 4
|
||||
|
||||
|
||||
# ── output_max_height plumbing ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_job_output_max_height_default_none():
|
||||
"""Job.output_max_height defaults to None."""
|
||||
job = Job(
|
||||
job_id="test-oh",
|
||||
video_path="/tmp/test.mp4",
|
||||
model_configs=[],
|
||||
output_dir="/tmp/output",
|
||||
)
|
||||
assert job.output_max_height is None
|
||||
|
||||
|
||||
def test_queue_add_job_output_max_height():
|
||||
"""add_job passes output_max_height through to the Job."""
|
||||
q = JobQueue(output_dir="/tmp/output")
|
||||
job = q.add_job(video_path="/tmp/test.mp4", model_configs=[],
|
||||
output_max_height=720)
|
||||
assert job.output_max_height == 720
|
||||
|
||||
|
||||
def test_queue_add_job_output_max_height_default():
|
||||
"""add_job without output_max_height keeps None."""
|
||||
q = JobQueue(output_dir="/tmp/output")
|
||||
job = q.add_job(video_path="/tmp/test.mp4", model_configs=[])
|
||||
assert job.output_max_height is None
|
||||
|
||||
|
||||
def _fake_single_job(tmp_path, output_max_height, monkeypatch):
|
||||
import src.job as job_mod
|
||||
from src.model_registry import ModelConfig
|
||||
from src.pipeline import PipelineResult
|
||||
|
||||
seen = {}
|
||||
|
||||
def fake_run_pipeline(**kwargs):
|
||||
seen.update(kwargs)
|
||||
return PipelineResult(
|
||||
output_path=str(tmp_path / "out.mp4"),
|
||||
frame_count=10,
|
||||
loading_count=1,
|
||||
unloading_count=0,
|
||||
batch_count=1,
|
||||
duration_seconds=1.0,
|
||||
model_name="a.pt",
|
||||
class_filter=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(job_mod, "run_pipeline", fake_run_pipeline)
|
||||
|
||||
q = JobQueue(output_dir=str(tmp_path))
|
||||
job = Job(
|
||||
job_id="job-oh-single",
|
||||
video_path="/tmp/test.mp4",
|
||||
model_configs=[
|
||||
ModelConfig(filename="a.pt", path=str(tmp_path / "a.pt"), stem="a",
|
||||
known_classes=["sack", "truck"])
|
||||
],
|
||||
output_dir=str(tmp_path / "job-oh-single"),
|
||||
output_max_height=output_max_height,
|
||||
)
|
||||
q._jobs[job.job_id] = job
|
||||
q._run_job(job.job_id)
|
||||
return job, seen
|
||||
|
||||
|
||||
def test_run_job_single_passes_output_max_height(tmp_path, monkeypatch):
|
||||
"""_run_job single branch forwards output_max_height=720 to run_pipeline."""
|
||||
job, seen = _fake_single_job(tmp_path, 720, monkeypatch)
|
||||
assert job.status == JobStatus.COMPLETED
|
||||
assert seen.get("output_max_height") == 720
|
||||
|
||||
|
||||
def test_run_job_single_output_max_height_none(tmp_path, monkeypatch):
|
||||
"""_run_job single branch forwards output_max_height=None."""
|
||||
job, seen = _fake_single_job(tmp_path, None, monkeypatch)
|
||||
assert job.status == JobStatus.COMPLETED
|
||||
assert seen.get("output_max_height") is None
|
||||
|
||||
|
||||
def test_run_job_merged_passes_output_max_height(tmp_path, monkeypatch):
|
||||
"""_run_job merged branch forwards output_max_height to run_merged_pipeline."""
|
||||
import src.job as job_mod
|
||||
from src.job import Job, JobQueue, JobStatus
|
||||
from src.model_registry import ModelConfig
|
||||
from src.pipeline import MergedPipelineResult
|
||||
|
||||
seen = {}
|
||||
|
||||
def fake_run_merged(**kwargs):
|
||||
seen.update(kwargs)
|
||||
return MergedPipelineResult(
|
||||
output_path=str(tmp_path / "merged.mp4"),
|
||||
frame_count=10,
|
||||
loading_count=3,
|
||||
unloading_count=1,
|
||||
batch_count=1,
|
||||
duration_seconds=1.0,
|
||||
model_names=["a.pt", "b.pt"],
|
||||
class_filter=None,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(job_mod, "run_merged_pipeline", fake_run_merged)
|
||||
|
||||
q = JobQueue(output_dir=str(tmp_path))
|
||||
job = Job(
|
||||
job_id="job-oh-merged",
|
||||
video_path="/tmp/test.mp4",
|
||||
model_configs=[
|
||||
ModelConfig(filename="a.pt", path=str(tmp_path / "a.pt"), stem="a",
|
||||
known_classes=["sack", "truck"]),
|
||||
ModelConfig(filename="b.pt", path=str(tmp_path / "b.pt"), stem="b",
|
||||
known_classes=["sack"]),
|
||||
],
|
||||
output_dir=str(tmp_path / "job-oh-merged"),
|
||||
output_max_height=720,
|
||||
)
|
||||
q._jobs[job.job_id] = job
|
||||
q._run_job(job.job_id)
|
||||
|
||||
assert job.status == JobStatus.COMPLETED
|
||||
assert seen.get("output_max_height") == 720
|
||||
@@ -327,3 +327,21 @@ def test_run_merged_pipeline_propagates_box_counts(tmp_path, monkeypatch):
|
||||
assert result.box_loading_count == 4
|
||||
assert result.box_unloading_count == 1
|
||||
assert result.box_net_count == 3
|
||||
|
||||
|
||||
# ── Perf/output tuning params (signature contracts) ────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fn_name", ["run_pipeline", "run_merged_pipeline"]
|
||||
)
|
||||
def test_pipeline_perf_params_signature(fn_name):
|
||||
"""Both runners expose detect_interval=2, output_max_height=None, preview_every_n=5."""
|
||||
import inspect
|
||||
|
||||
from src import pipeline as pipe_mod
|
||||
|
||||
sig = inspect.signature(getattr(pipe_mod, fn_name))
|
||||
assert sig.parameters["detect_interval"].default == 2
|
||||
assert sig.parameters["output_max_height"].default is None
|
||||
assert sig.parameters["preview_every_n"].default == 5
|
||||
@@ -6,6 +6,18 @@ import pytest
|
||||
from src.video_writer import AnnotatedVideoWriter
|
||||
|
||||
|
||||
def probe(path):
|
||||
cap = cv2.VideoCapture(str(path))
|
||||
assert cap.isOpened(), f"cannot reopen {path}"
|
||||
info = (
|
||||
int(cap.get(cv2.CAP_PROP_FRAME_COUNT)),
|
||||
int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
|
||||
int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
|
||||
)
|
||||
cap.release()
|
||||
return info
|
||||
|
||||
|
||||
def test_writer_creates_output_file(tmp_path):
|
||||
out = tmp_path / "test_output.mp4"
|
||||
writer = AnnotatedVideoWriter(str(out), fps=25.0, frame_size=(640, 480))
|
||||
@@ -37,3 +49,60 @@ def test_writer_close_idempotent(tmp_path):
|
||||
def test_writer_invalid_fps():
|
||||
with pytest.raises(ValueError):
|
||||
AnnotatedVideoWriter("/tmp/x.mp4", fps=0.0, frame_size=(640, 480))
|
||||
|
||||
|
||||
def test_max_height_downscale(tmp_path):
|
||||
out = tmp_path / "down.mp4"
|
||||
writer = AnnotatedVideoWriter(
|
||||
str(out), fps=25.0, frame_size=(640, 480), max_height=240
|
||||
)
|
||||
assert writer.written_size == (320, 240)
|
||||
for _ in range(3):
|
||||
writer.write_frame(np.zeros((480, 640, 3), dtype=np.uint8))
|
||||
writer.finish()
|
||||
assert out.exists() and out.stat().st_size > 0
|
||||
count, w, h = probe(out)
|
||||
assert count == 3
|
||||
assert (w, h) == writer.written_size
|
||||
|
||||
|
||||
def test_no_max_height_keeps_native_size(tmp_path):
|
||||
out = tmp_path / "native.mp4"
|
||||
writer = AnnotatedVideoWriter(str(out), fps=25.0, frame_size=(640, 480))
|
||||
assert writer.written_size == (640, 480)
|
||||
writer.write_frame(np.zeros((480, 640, 3), dtype=np.uint8))
|
||||
writer.finish()
|
||||
assert probe(out)[1:] == (640, 480)
|
||||
|
||||
|
||||
def test_even_dimensions_after_downscale(tmp_path):
|
||||
out = tmp_path / "even.mp4"
|
||||
writer = AnnotatedVideoWriter(
|
||||
str(out), fps=25.0, frame_size=(641, 481), max_height=240
|
||||
)
|
||||
tw, th = writer.written_size
|
||||
assert tw % 2 == 0 and th % 2 == 0
|
||||
assert th <= 240
|
||||
writer.write_frame(np.zeros((481, 641, 3), dtype=np.uint8))
|
||||
writer.finish()
|
||||
assert probe(out)[1:] == writer.written_size
|
||||
|
||||
|
||||
def test_auto_codec_records_backend(tmp_path):
|
||||
out = tmp_path / "auto.mp4"
|
||||
writer = AnnotatedVideoWriter(str(out), fps=25.0, frame_size=(640, 480))
|
||||
assert isinstance(writer.backend, str)
|
||||
assert writer.backend
|
||||
writer.write_frame(np.zeros((480, 640, 3), dtype=np.uint8))
|
||||
writer.finish()
|
||||
|
||||
|
||||
def test_explicit_codec_forces_fourcc(tmp_path):
|
||||
out = tmp_path / "explicit.mp4"
|
||||
writer = AnnotatedVideoWriter(
|
||||
str(out), fps=25.0, frame_size=(640, 480), codec="mp4v"
|
||||
)
|
||||
assert writer.backend == "mp4v"
|
||||
writer.write_frame(np.zeros((480, 640, 3), dtype=np.uint8))
|
||||
writer.finish()
|
||||
assert out.exists() and out.stat().st_size > 0
|
||||
Reference in new issue
Block a user