How to Extend the Multi-Camera Face Tracker with New Features: A Developer's Guide

You can extend the multi-cam face tracker by implementing new classes that conform to existing interfaces—such as adding alert channels in AlertSystem, camera sources in CameraManager, or UI tabs in MainWindow—then wiring them through the YAML configuration and dependency injection points in ui/main_window.py.

The multi-camera face tracker from the aarambhdevhub/multi-cam-face-tracker repository is built on a modular, loosely-coupled architecture that makes it straightforward to extend with new features. Whether you need to add support for RTSP cameras, integrate a custom face recognition model, or build new notification channels, the system provides clear extension points that minimize changes to existing code.

Core Architecture Overview

Understanding the layered architecture is essential before extending the multi-cam face tracker. Each layer has a specific responsibility and exposes well-defined Python interfaces.

Layer Responsibility Main Files
Application entry point Loads configuration, starts logging, creates the main window main.py
Configuration YAML files drive cameras, recognition thresholds, and UI assets config/config.yaml, config/camera_config.yaml
Camera management Handles capture threads, frame queues, rotation, and start/stop logic core/camera_manager.py
Face detection & recognition Wraps InsightFace, loads known faces, provides detect_faces and recognize_faces core/face_detection.py
Alert system Plays sounds, writes screenshots, and sends Telegram messages core/alert_system.py
Database Persists face events and known-face embeddings in SQLite core/database.py
Utility helpers Image drawing, conversion, and resizing core/utils.py
GUI PyQt5 windows, tabs, controls, and live feed rendering ui/main_window.py and ancillary dialogs (ui/face_manager.py, ui/alert_panel.py, ui/history_viewer.py)

All components are instantiated in MainWindow.__init__ (lines 38-44 of ui/main_window.py). The main update loop (MainWindow.update) pulls frames from CameraManager, passes them to FaceDetector, and forwards recognized faces to AlertSystem and FaceDatabase. This clear data flow makes it easy to plug in new functionality at well-known extension points.

Extension Points for New Features

The multi-cam face tracker provides specific hooks for common extension scenarios. You can add new capabilities by implementing the appropriate interface and registering it in the configuration or initialization code.

Feature Extension Point Implementation Strategy
New camera source (RTSP, IP cameras) CameraManager._capture_frames Extend the source field logic (line 41) to handle authentication or custom stream wrappers
Alternative face detector (OpenCV Haar, MediaPipe) FaceDetector class Replace or subclass FaceDetector, keeping public methods detect_faces(image) and recognize_faces(faces)
Additional alert channels (email, Slack, webhook) AlertSystem class Add a new method send_<channel> and call it from AlertSystem.trigger_alert (lines 48-72)
Custom UI tab (statistics, settings) MainWindow.tab_widget Create a new QWidget and add it to MainWindow following the pattern of setup_monitor_tab (lines 14-31)
Extended database schema (pose, landmarks) core/database.py Add new columns to face_logs in _init_db (around line 41) and expose them via FaceLogEntry
Plugin system (runtime module loading) MainWindow.__init__ Use Python's importlib to discover files in a plugins/ folder and invoke a known interface like initialize(app_context)

Step-by-Step Implementation Examples

Adding a Slack Alert Channel

You can extend the multi-cam face tracker to send Slack notifications by modifying the configuration and the AlertSystem class.

First, update the YAML configuration:


# config/config.yaml

slack:
  enabled: true
  webhook_url: "https://hooks.slack.com/services/XXXX/XXXX/XXXX"

Next, extend core/alert_system.py to handle Slack messaging:


# core/alert_system.py

import json
import urllib.request
from dataclasses import dataclass

@dataclass
class AlertEvent:
    timestamp: str
    camera_id: int
    face_name: str
    confidence: float
    screenshot_path: str | None

class AlertSystem:
    def __init__(self, config: dict):
        # Existing initialization...

        self.slack_cfg = config.get('slack', {})
        self.slack_enabled = self.slack_cfg.get('enabled', False)
        self.slack_webhook = self.slack_cfg.get('webhook_url', '')

    def _post_to_slack(self, message: str, image_path: str | None = None) -> None:
        """Post a text message to Slack webhook."""
        if not self.slack_enabled or not self.slack_webhook:
            return
        
        payload = {"text": message}
        data = json.dumps(payload).encode('utf-8')
        
        try:
            req = urllib.request.Request(
                self.slack_webhook,
                data=data,
                headers={'Content-Type': 'application/json'},
                method='POST'
            )
            urllib.request.urlopen(req, timeout=5)
        except Exception as e:
            print(f"Slack alert failed: {e}")

    def trigger_alert(self, camera_id: int, camera_name: str, 
                     face_name: str, face, confidence: float, frame) -> AlertEvent:
        """Trigger all alert channels including the new Slack integration."""
        # Existing alert logic (sound, screenshot, telegram)...

        event = AlertEvent(
            timestamp=datetime.now().isoformat(),
            camera_id=camera_id,
            face_name=face_name,
            confidence=confidence,
            screenshot_path=None  # Set by existing screenshot logic

        )
        
        # Build and send Slack notification

        msg = f"*Face detected!* {face_name} on *{camera_name}* confidence: {confidence:.1%}"
        self._post_to_slack(msg, event.screenshot_path)
        
        return event

This implementation follows the existing pattern in AlertSystem where self.telegram is stored as an attribute and invoked during trigger_alert. The Slack webhook receives a formatted message whenever a face is recognized.

Supporting RTSP Camera Sources

To extend the multi-cam face tracker for RTSP streams with authentication, modify the camera initialization logic in core/camera_manager.py:


# core/camera_manager.py

import cv2

class CameraManager:
    def _capture_frames(self, cam_id: int) -> None:
        """Capture loop with extended RTSP support."""
        cam_config = self.cameras[cam_id]
        source = cam_config.source
        
        # Handle RTSP URLs with embedded credentials

        if isinstance(source, str) and source.startswith("rtsp://"):
            # OpenCV supports RTSP directly; for advanced options use GStreamer

            cap = cv2.VideoCapture(source, cv2.CAP_FFMPEG)
            
            # Optional: Set buffer size to reduce latency

            cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
        else:
            # Original integer device index handling

            source = int(source) if str(source).isdigit() else source
            cap = cv2.VideoCapture(source)
        
        # Continue with existing frame processing loop...

        while self.running:
            ret, frame = cap.read()
            if ret:
                # Process frame...

                pass

The original source at lines 40-45 of camera_manager.py handles generic source values. This extension adds specific logic for RTSP strings while maintaining backward compatibility with integer device indices.

Creating a Plugin-Based Face Detector

You can extend the multi-cam face tracker with alternative detection models using a plugin architecture:


# plugins/custom_detector.py

from core.face_detection import Face, KnownFace
import numpy as np

class CustomDetector:
    """
    Example plugin detector that implements the required interface.
    Replace the embedding logic with your own model (Haar, MediaPipe, etc.).
    """
    
    def __init__(self, config: dict):
        self.recognition_threshold = config['recognition']['recognition_threshold']
        # Initialize your custom model here

        # self.model = cv2.CascadeClassifier('haarcascade_frontalface_default.xml')

    
    def detect_faces(self, image: np.ndarray) -> list[Face]:
        """
        Detect faces in the image and return list of Face objects.
        """
        # Example: Return a dummy face at center for demonstration

        h, w = image.shape[:2]
        bbox = np.array([w//4, h//4, 3*w//4, 3*h//4])
        
        return [Face(
            bbox=bbox,
            kps=np.zeros((5, 2)),  # 5 keypoints (eyes, nose, mouth)

            det_score=1.0,
            embedding=np.random.rand(512),  # 512-dim embedding

            age=None,
            gender=None
        )]
    
    def recognize_faces(self, faces: list[Face]) -> list[tuple[Face, KnownFace | None, float]]:
        """
        Match detected faces against known faces database.
        Returns tuples of (Face, KnownFace or None, confidence).
        """
        # Example: Always return unknown (None) with 0 confidence

        return [(face, None, 0.0) for face in faces]

To load this plugin at runtime, modify the initialization in ui/main_window.py:


# ui/main_window.py

import importlib.util
import pathlib

class MainWindow:
    def __init__(self, config):
        # Existing initialization...

        
        # Option to load custom detector plugin

        if config.get('detection', {}).get('use_plugin', False):
            self.face_detector = self._load_plugin_detector(config)
        else:
            self.face_detector = FaceDetector(config)
    
    def _load_plugin_detector(self, config):
        """Dynamically load a detector from the plugins folder."""
        plugin_path = pathlib.Path('plugins/custom_detector.py')
        spec = importlib.util.spec_from_file_location('custom_detector', plugin_path)
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        return mod.CustomDetector(config)

This approach allows you to swap detection backends without modifying the core FaceDetector class, following the existing instantiation pattern seen in MainWindow.__init__ (lines 38-40).

Key Files for Extension

File Role Extension Use Case
main.py Application entry point Add CLI arguments for new features
config/config.yaml Global settings Define thresholds for new detectors or alert channels
config/camera_config.yaml Camera definitions Add new source types (RTSP, IP cameras)
core/camera_manager.py Camera thread management Extend _capture_frames for new protocols
core/face_detection.py Detection/recognition logic Replace or subclass FaceDetector
core/alert_system.py Notification dispatch Add methods like _post_to_slack
core/database.py SQLite persistence Extend schema in _init_db
ui/main_window.py Central UI controller Add tabs and wire new components
ui/face_manager.py Face enrollment dialog Add bulk import features

Summary

Extending the multi-cam face tracker requires understanding its modular architecture and well-defined interfaces. The key takeaways for adding new features include:

  • Hook into existing managers by extending classes like AlertSystem or CameraManager rather than modifying the core update loop in MainWindow.update.
  • Respect public interfaces when replacing components; FaceDetector must expose detect_faces() and recognize_faces() methods to work with the UI.
  • Use configuration-driven setup by adding new sections to config/config.yaml and reading them in component __init__ methods.
  • Leverage runtime plugin loading via importlib to swap detection backends or alert channels without restarting the application.
  • Maintain separation of concerns by placing camera logic in core/, UI components in ui/, and configuration in config/.

Frequently Asked Questions

How do I add a new notification channel like email or Discord?

Create a new private method in core/alert_system.py (for example, _send_email or _post_to_discord) that accepts the alert message and optional image path. Then invoke this method inside trigger_alert (lines 48-72) alongside the existing Telegram and sound alerts. Add the necessary configuration keys (API keys, endpoints) to config/config.yaml and read them in AlertSystem.__init__.

Can I replace InsightFace with a different detection model?

Yes. Implement a new class in plugins/custom_detector.py or modify core/face_detection.py that exposes two required methods: detect_faces(image) returning a list of Face objects, and recognize_faces(faces) returning tuples of (Face, KnownFace|None, confidence). In ui/main_window.py, instantiate your custom class instead of the default FaceDetector in MainWindow.__init__ (lines 38-40), or use the importlib plugin pattern to load it dynamically.

Where should I add support for IP cameras or RTSP streams?

Extend the _capture_frames method in core/camera_manager.py. The existing logic at lines 40-45 handles integer device indices; you can add a conditional branch to detect RTSP URLs (strings starting with rtsp://) and initialize cv2.VideoCapture with cv2.CAP_FFMPEG flags. Add any authentication parameters or stream-specific settings (like buffer size) in this block, and define the RTSP URLs in config/camera_config.yaml under the source field.

How do I create a new settings tab in the user interface?

Add a new method setup_settings_tab in ui/main_window.py that creates a QWidget, configures a layout (such as QVBoxLayout), and adds your custom controls. Follow the pattern established by setup_monitor_tab (lines 14-31). Finally, call your new method inside MainWindow.__init__ after the existing tab setup calls (around line 75). The new tab will automatically appear in the main interface and can interact with self.alert_system or self.camera_manager via the shared instance variables.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →