Skip to content

Architecture

CamReview is a synchronous, stateless batch pipeline. It reads recordings and optional configuration, streams sampled frames through motion and classification components, and writes reports or extracted files. No server, database, queue, or persistent job state is involved.

Overview

The pipeline keeps footage local and passes bounded streams or domain records between stages:

graph LR
  A[Recording files] -->|discover and select| B[Timeline]
  B -->|sample by PTS| C[PyAV decoder]
  C -->|BGR frames| D[MOG2 detector]
  D -->|motion samples| E[Event builder]
  E -->|events and spans| F[Report writer]
  E -->|event frames| G[YOLO classifier]
  G -->|classified events| F
  E -->|selected spans| H[Extractor]

The filename timestamp is the recording's authoritative local start. Frame presentation timestamps become offsets from that start. This avoids dependence on filesystem metadata and preserves nonuniform frame timing without introducing timezone conversion.

Components

Discovery and timeline

filenames.py recognizes supported extensions and parses the camera, date, and time from the right side of each filename. timeline.py discovers files, selects exactly one camera-day, detects gaps using probed durations or inferred cadence, clips requested ranges, and maps an event window back to recording-relative source segments.

Discovery is top-level by default. Paths are expanded and resolved before enumeration so mapped-drive and UNC semantics do not leak into PyAV or FFmpeg. Unrecognized, unsettled, and inaccessible files remain explicit processing issues instead of silently disappearing.

Decode and motion layers

decoding/base.py defines the decoder boundary. PyAVDecoder probes video metadata and streams frames at requested sample times using PTS. It retries transient network failures, resumes beyond the last emitted sample, falls back from broken seeks to sequential decode, and can validate a hardware backend before committing to it.

motion/base.py defines the motion boundary. MOG2MotionDetector downsizes without upscaling, converts to blurred grayscale, applies an optional binary mask, removes MOG2 shadow values, performs morphology, and measures foreground ratio and contours. Its background model persists across continuous files and resets on resolution changes or large timeline gaps. Scene-change frames are suppressed while the model adapts rapidly.

EventBuilder requires consecutive positive samples, closes after quiet time, merges nearby events, and deliberately breaks continuity around failed or discontinuous footage. Source segments are attached only after event construction.

Classification

detection/base.py isolates object-detector implementations. UltralyticsDetector lazily imports the optional stack, resolves model storage, selects CPU or CUDA, and converts YOLO boxes into domain objects and broad categories.

Classification revisits only event source spans. Consecutive sampled frames produce a dilated difference mask. A visible object contributes to an event category only if enough of its box overlaps that mask; otherwise it remains in visible_objects. Batches are cleared after inference, keeping memory bounded by batch_size plus decoder/model state.

Reports and extraction

models.py contains the serializable domain records. reports/io.py treats JSON schema 1 as canonical and derives CSV and text views. Writes use a temporary sibling, flush, and same-directory atomic replacement, including safe handling for shares that reject fsync.

extraction/ffmpeg.py has two paths. Source mode deduplicates and atomically copies original files. Event mode maps pre/post-roll windows across recordings, then either re-encodes video accurately or uses FFmpeg concat and stream copy for speed. Temporary work is bounded to one event rather than a day-long intermediate.

Data flow

A run resolves the source root, discovers candidate videos, and selects exactly one camera and date. It probes durations before applying the requested time range. The decoder samples frames in chronological order while the motion detector retains background state across continuous files. The event builder closes, merges, and maps events to source-relative spans.

The report writer serializes every canonical event before filtered CSV, text, or extraction views are created. When classification is enabled, the scan writes a motion checkpoint, classifies only event spans, and then replaces the report with the complete result.

Failure and recovery boundaries

In non-strict mode, a bad file breaks event continuity and becomes an issue and timeline gap; the scan continues. In strict mode it aborts. A scan interrupted during processing writes an interrupted JSON report. A scan requesting classification first writes a motion_complete_classification_pending checkpoint so classification can be resumed with the standalone command.

Directory layout

camreview/
├── cli.py              Argument parsing, dispatch, and exit-code mapping
├── commands/           Workflow orchestration
├── decoding/           Video decoder interface and PyAV implementation
├── motion/             Motion interface, MOG2, and event construction
├── detection/          Object detector interface, YOLO, and category logic
├── extraction/         Source copying and FFmpeg event extraction
├── reports/            Canonical serialization and human-facing projections
├── config.py           TOML merge and settings validation
├── filesystem.py       Retried network-safe filesystem operations
├── filenames.py        Recording naming contract
├── timeline.py         Discovery, selection, gaps, and source spans
└── models.py           Domain and report dataclasses

The command layer depends on interfaces and domain models; optional vendor integrations stay behind detection and decoding boundaries. See the Development guide for how those boundaries are tested.

Design decisions

CamReview treats JSON as the durable handoff between machines and optional stages. It keeps the scanner stateless instead of introducing a database. Filename timestamps win over file metadata so copied recordings retain their camera timeline. Iterator-based decoding and bounded detector batches avoid a day-sized frame cache, while destination-side temporary files keep partial output from replacing the previous successful result.