Contributing to Argus¶
Project Structure¶
argus/
├── main.py # Entry point — CLI parsing, component wiring
├── config/
│ ├── cameras.toml # Camera URLs, detection settings
│ └── webhooks.toml # Webhook endpoints
├── targets/ # Face reference data (per-person directories)
├── argus/
│ ├── __init__.py
│ ├── models.py # Dataclasses: Settings, CameraConfig, MatchEvent, etc.
│ ├── config.py # TOML config loading and validation
│ ├── stream.py # RTSPStream — one thread per camera, auto-reconnect
│ ├── detection.py # FaceDetector — HOG detection + encoding + matching
│ ├── alert.py # AlertHandler — logging, screenshots, webhooks
│ ├── display.py # GUI display windows (OpenCV)
│ ├── manager.py # StreamManager — orchestrates the full pipeline
│ └── targets.py # Target loading and encoding index construction
├── screenshots/ # Output — annotated match images (git-ignored)
├── logs/ # Output — structured JSON logs (git-ignored)
├── pyproject.toml # Project metadata, dependencies
└── LICENSE # GPL-3.0
Code Style¶
Python 3.12+¶
Use modern Python. Type hints on everything. F-strings over .format(). match statements where appropriate. No compatibility shims for older versions.
# Good
def get_frame(self) -> tuple[bool, np.ndarray | None]:
...
# Bad
def get_frame(self) -> Tuple[bool, Optional[np.ndarray]]:
...
No comments unless asked¶
The code should be self-documenting. If it needs a comment, rewrite the code. The one exception: docstrings on all public methods.
Docstrings — Google style¶
def detect(
self,
frame: np.ndarray,
camera_id: str,
camera_name: str,
) -> list[MatchEvent]:
"""Run face detection and recognition on a single frame.
Args:
frame: BGR numpy array from OpenCV (full resolution).
camera_id: Camera identifier.
camera_name: Human-readable camera name.
Returns:
List of MatchEvent for each recognized face.
"""
Logging — loguru only¶
Never print(). Never logging. Always loguru.
from loguru import logger
logger.info("[{}] Connected to '{}'", self.camera.id, self.camera.name)
logger.warning("{} consecutive read failures — reconnecting", failures)
Use structured logging for match events:
logger.bind(person=name, camera=camera, confidence=score).success(
"MATCH: '{}' on '{}' (confidence: {:.1%})", name, camera, score
)
Type hints on all function signatures¶
No exceptions. Parameter types and return types. Always.
Development Setup¶
Prerequisites¶
- Python 3.12 (required — uses
tomllibfrom stdlib, modern type syntax) - System dependencies for dlib:
# Debian/Ubuntu
sudo apt install cmake libboost-all-dev libdlib-dev
# Fedora/RHEL
sudo dnf install cmake boost-devel dlib-devel
# Arch/Manjaro
sudo pacman -S cmake boost dlib
# Void
sudo xbps-install cmake boost-devel dlib-devel
# Gentoo
sudo emerge --ask dev-libs/boost dev-libs/dlib dev-util/cmake
# macOS
brew install cmake boost dlib
Install¶
This creates a .venv with all dependencies. Use uv run python main.py to execute.
Architecture Decisions¶
These patterns are not optional. They exist for specific reasons.
Thread safety: _DETECTION_LOCK¶
The face_recognition library (and dlib underneath) uses global singleton objects that are not thread-safe. All calls to face_recognition.face_locations(), face_recognition.face_encodings(), and face_recognition.face_distance() must go through the module-level _DETECTION_LOCK in argus/detection.py.
Do not call face_recognition functions outside this lock. Do not create a second detector. Do not try to parallelize detection across cameras — the global lock serializes it anyway.
Fire-and-forget webhooks¶
Webhook calls never block the detection pipeline. They run in daemon threads. If a webhook times out or fails, the detection loop does not care. It already moved on.
# This is the pattern
thread = threading.Thread(target=self._send_webhook, args=(...), daemon=True)
thread.start()
# No .join(), no return value, no error handling in the caller
Cooldown system¶
Cooldowns are keyed on (target_name, camera_id) tuples. This means:
- Same person, same camera → cooldown applies (don't spam alerts)
- Same person, different cameras → independent cooldowns (alert on both)
- Different people, same camera → independent cooldowns (alert on both)
The cooldown dictionary is protected by _cooldown_lock. Use time.monotonic() — never wall clock time — for interval calculations.
Frame copying under lock¶
When a consumer calls stream.latest_frame(), it receives a copy of the frame, not a reference. The lock is held only during the copy. This prevents race conditions between the reader thread (writing frames) and the detection thread (reading frames).
with self._lock:
if self._has_frame and self._frame is not None:
return True, self._frame.copy() # Always copy
return False, None
Main thread constraint¶
The GUI (Display) must be updated from the main thread. StreamManager.start() runs the main loop on the calling thread specifically for this reason. Never move GUI operations to a background thread — OpenCV's imshow and waitKey require the main thread.
Adding a New Module¶
Where it fits¶
| Module | Responsibility |
|---|---|
models.py |
Data structures only — dataclasses, no logic |
config.py |
Loading and validation of config files |
stream.py |
RTSP stream reading and reconnection |
detection.py |
Face detection and recognition (dlib) |
alert.py |
Response to match events (log, screenshot, webhook) |
display.py |
GUI rendering (OpenCV windows) |
manager.py |
Orchestration — ties everything together |
targets.py |
Target data loading and encoding construction |
If your module handles a new output type (e.g., database storage, MQTT), it belongs alongside alert.py. If it handles a new input type (e.g., ONVIF discovery), it belongs alongside config.py or stream.py.
Connecting to StreamManager¶
The StreamManager is the central orchestrator. Components interact with it in one of two ways:
- Direct injection — The manager receives components via
__init__(detector, alert handler, display). Add constructor parameters for new components. - Event flow — Data flows:
RTSPStream→FaceDetector.detect()→AlertHandler.handle(). To add a new event type, intercept at the manager's main loop.
Threading considerations¶
- New output handlers must be non-blocking (fire-and-forget threads or queues)
- Never acquire
_DETECTION_LOCKfrom a webhook handler - Never call GUI operations from a background thread
Adding a New Alert Type¶
Option 1: Extend AlertHandler¶
Add a new method to AlertHandler and call it from handle():
def handle(self, event: MatchEvent) -> None:
if self._is_on_cooldown(event.target_name, event.camera_id):
return
self._update_cooldown(event.target_name, event.camera_id)
self._save_screenshot(event)
self._log_match(event)
self._new_alert_type(event) # Add your handler here
self._fire_webhooks(event)
Option 2: Create a new handler class¶
If the alert type has its own state, lifecycle, or configuration, create a separate class. Accept it in StreamManager.__init__ and call it from the main loop.
Requirements¶
- Must not block the detection pipeline
- Must integrate with the cooldown system (check before acting, update after)
- Must handle its own errors internally — never let an alert failure crash the pipeline
- Must use loguru for logging
Testing¶
No test suite exists yet. Testing is manual.
Creating test RTSP streams¶
Use ffmpeg to create synthetic test streams:
# Generate a test pattern stream on localhost
ffmpeg -f lavfi -i testsrc=size=1280x720:rate=30 \
-c:v libx264 -preset ultrafast \
-f rtsp rtsp://127.0.0.1:8554/test
# Re-stream a video file as RTSP
ffmpeg -re -i sample_video.mp4 \
-c:v libx264 -preset ultrafast \
-f rtsp rtsp://127.0.0.1:8554/test
Testing face recognition¶
- Create a target directory with reference images
- Run a test stream with faces visible (use your webcam or a video of faces)
- Verify matches appear in logs
- Verify screenshots are saved with correct annotations
- Verify webhooks fire (check your receiver logs)
What to test¶
- Connection to multiple cameras simultaneously
- Reconnection after killing a test stream (Ctrl+C the ffmpeg process)
- Cooldown behavior (same person, same camera)
- Cooldown independence (same person, different cameras)
- Webhook failure handling (point webhook at a down server)
- Graceful shutdown (SIGTERM via
kill <pid>)
Commit Style¶
Short, imperative. Match the existing history.
No conventional commits prefix required. No body text required. Just say what changed.