This project implements a simple multi‑process motion detector over a video file using OpenCV.
It is structured into three processes:
Streamer- reads frames from a video file and sends them downstream with presentation timestamps.Detector- applies the provided motion‑detection algorithm and outputs bounding boxes.Displayer- renders frames with bounding boxes and plays them back at (approximately) real‑time speed.
Use Python 3.11 (or another version supported by opencv-python):
python3.11 --versionYou can use either Poetry (recommended) or a plain virtual environment with requirements.txt.
Using Poetry
cd /path/to/axon-vision
poetry installThen run the app with:
poetry run python main.pyUsing pip / requirements
Create and activate a virtual environment, then install dependencies:
python3.11 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRun the app with:
python main.pyThe input video path is defined in constants.py:
VIDEO_PATH = "People - 6387.mp4"Update this constant to point to any local video file you want to process, for example:
VIDEO_PATH = "/absolute/path/to/your_video.mp4"-
Streamer(instreamer.py)- Opens
VIDEO_PATHwith OpenCV. - For each frame, reads both the image and its presentation timestamp in milliseconds (
pts_ms) usingcv2.CAP_PROP_POS_MSEC. - Wraps them in a
FramePacketdataclass and pushes it onto aframes_queue.
- Opens
-
Detector(indetector.py)- Runs the provided motion‑detection algorithm:
- Convert frame to grayscale.
- Compute absolute difference (
absdiff) between the current and previous grayscale frame. - Apply a binary threshold.
- Dilate the thresholded image to connect regions.
- Find contours and convert them into bounding boxes.
- Bounding boxes are represented as
BoundingBoxdataclasses. - Produces a
DetectionResult(frame, list of bounding boxes, originalpts_ms) and puts it onto aresults_queue.
- Runs the provided motion‑detection algorithm:
-
Displayer(indisplayer.py)- Reads
DetectionResultobjects fromresults_queue. - Draws bounding boxes and a timestamp overlay on each frame.
- Displays the frames in an OpenCV window while trying to match the original video timing.
- Reads
Inter‑process communication uses multiprocessing.Queue, and a shared stop_event plus SENTINEL values to signal a clean shutdown.
The key to “real‑time” playback is the pts_ms (presentation timestamp in milliseconds) that comes from OpenCV:
-
Streamer reads
pts_msfor each frame using:pts_ms = cap.get(cv2.CAP_PROP_POS_MSEC)
and passes it along unchanged.
-
Detector preserves this timestamp and attaches it to each
DetectionResultso the display process knows when the frame should appear. -
Displayer anchors video time to wall‑clock time on the first frame:
- When the first
DetectionResultarrives, it stores:first_pts_ms– the first frame’spts_ms.start_wall– the current wall‑clock time fromtime.perf_counter().
- When the first
-
For every subsequent frame with timestamp
pts_ms, it computes a target wall‑clock time:target = start_wall + (float(pts_ms) - first_pts_ms) / 1000.0
(pts_ms - first_pts_ms) / 1000.0is “how many seconds into the video this frame is, relative to the first frame”.- Adding that to
start_wallgives “the exact wall‑clock time when this frame should be shown”.
-
If the display is behind schedule (current time is already past
target), the displayer may drop older frames in the queue and use newer ones to catch up, keeping playback closer to real‑time rather than slowly drifting behind. -
Otherwise, it sleeps in small increments until
time.perf_counter()reachestarget, then it renders and shows the frame.
This combination of timestamps and scheduling makes the playback speed match the original video’s timing as closely as possible, even though processing happens across multiple processes.
- Press
qin the video window to stop the display and signal all processes to shut down. - Closing the window also triggers a graceful shutdown.
- The main process waits on the
Displayerprocess first, then signals the others and joins them with a timeout to ensure a clean exit.