SMART-CAM (Smart AI Camera) — Full Documentation
Documents an edge AI surveillance pipeline that detects people, extracts ReID embeddings, and tracks identities locally on a Raspberry Pi with optional Hailo NPU acceleration.
What this file does
Documents an edge AI surveillance pipeline that detects people, extracts ReID embeddings, and tracks identities locally on a Raspberry Pi with optional Hailo NPU acceleration.
When to use it
- Building a privacy-focused local camera system that never sends video to the cloud
- Implementing person re-identification across multiple camera frames on low-power hardware
- Creating a hybrid tracker that combines spatial overlap and appearance similarity
- Setting up a FastAPI web dashboard with MJPEG streaming for live surveillance
Assumes this stack
SMART-CAM (Smart AI Camera) — Full Documentation
Repository: WajeehAlamoudi/Smart_AI_Camera
Docs generated on: 2026-03-11
1) Project Overview
SMART-CAM is an edge-first smart surveillance pipeline designed to run fully locally on devices like a Raspberry Pi (Pi 5 recommended) and optionally accelerate inference using a Hailo NPU through Picamera2’s Hailo integration.
The system does four major things in real time:
- Capture frames from a camera source (Pi CSI camera, USB webcam, RTSP, video file, etc.).
- Detect people using a Hailo-compiled YOLOv8 object detection model (
yolov8m.hef). - Generate Re-Identification embeddings using a Hailo-compiled RepVGG ReID model (
repvgg_a0_person_reid_512.hef). - Track identities across frames using a hybrid method:
- Spatial consistency (IoU with Kalman-predicted boxes)
- Appearance matching (cosine similarity on embeddings)
- Persist identities and snapshots in an SQLite database, and optionally update the best snapshot when a face is detected (Haar Cascade “bouncer”).
It supports two main run modes:
- Web dashboard mode using FastAPI (
app.py) with MJPEG streaming + database APIs. - Local desktop window mode using OpenCV display (
main.py) for direct monitor output.
2) Repository Structure (Key Files)
Note: I fetched the most important files; if more files exist beyond what was returned by search, they may not be shown here.
README.md— high-level description + install instructionsrequirements.txt— minimal Python deps (fastapi,uvicorn,opencv-python,numpy==1.26.4)app.py— FastAPI server + background AI loop + REST APIsmain.py— “local monitor” loop (imshow) pipeline
Packages / Modules
camera/stream.py—SmartCamera(robust camera input abstraction)ai/detector.py—SmartDetector(Hailo YOLO detector)ai/embedder.py—SmartEmbedder(Hailo ReID embedding extractor)ai/tracker.py—SmartTracker(hybrid tracker: Kalman + cosine + IoU)ai/face_detector.py—SmartFaceDetector(CPU Haar cascade face check)ai/events.py—handle_new_person()(snapshot + DB insert)database/db_manager.py—DatabaseManager(SQLite + embeddings persistence)core/logger.py— log formatting + file/console logging helperstemplates/index.html— dashboard UI (HTML/CSS/JS)
Models / Assets
models/coco.txt— COCO labels for detection classes- AI model:
models/yolov8m.hefmodels/repvgg_a0_person_reid_512.hefmodels/haarcascade_frontalface_default.xml
3) How the Whole Pipeline Works (Conceptual)
Think of SMART-CAM like an assembly line running on every frame:
Stage A — Frame acquisition (SmartCamera)
- A background thread continuously captures frames and stores the newest one.
- The rest of the pipeline always pulls the “latest frame” when ready.
Stage B — Object detection (SmartDetector)
- Runs YOLOv8 (compiled to Hailo
.hef) to produce bounding boxes. - Output becomes a list of detection dictionaries (each has at least
bbox, plus metadata likescore, etc.).
Stage C — ReID embedding (SmartEmbedder)
- For each detected person box:
- Crop the person region from the frame.
- Resize to model input.
- Run embedding model on Hailo.
- Flatten and L2-normalize the vector (critical for cosine similarity).
- Attach it to the detection dict as
det["embedding"].
Stage D — Tracking & identity assignment (SmartTracker)
- Maintains a gallery of known IDs (loaded from SQLite on startup).
- For each new detection, it tries to match it to an existing ID using:
- IoU with Kalman predicted box (spatial continuity)
- Cosine similarity with stored embedding (appearance continuity)
- If match succeeds → reuse existing ID and update memory.
- If match fails → create a new ID, save snapshot + embedding to DB.
Stage E — “Smart face capture”
- For known IDs, the tracker checks:
- If the crop contains a frontal face (Haar cascade)
- OR if the person box got significantly larger than the best snapshot
- Then it overwrites snapshot with a higher-quality crop and updates
last_seen.
Stage F — Output
SmartTracker.draw()draws bounding boxes + IDs.- In
app.pythe annotated frames are encoded as JPEG and streamed to browser via MJPEG.
4) Detailed Module Documentation
4.1 camera/stream.py — SmartCamera
Class: SmartCamera
A resilient camera abstraction that supports different input types:
"auto": try PiCamera first, then fall back to USB camera index 0"picamera": force Raspberry Pi camera pipeline- integer index
0,1,...for USB cameras "/dev/video0"device path- URLs like
rtsp://,http(s)://,udp:// - video files (
.mp4,.avi, ...)
Key constructor parameters
source: input source selectorresolution: desired capture resolution (e.g.,(1280,720))target_fps: software throttling targetreconnect_delay: how long to wait between reconnect attemptsjpeg_quality: output quality for MJPEG usageflip_method: optional transforms ("h","v","cw","ccw","180")
Core idea
SmartCamera runs capture on its own thread and exposes:
get_frame()→ returns latest frame (numpy array) safelystop()→ shuts down threads/capture cleanly
This design prevents the AI loop from blocking camera capture.
4.2 ai/detector.py SmartDetector (YOLO on Hailo)
Class: SmartDetector
Responsible for people detection (default: person-only).
Parameters
confidence_threshold(default0.8)only_person(defaultTrue): if true, it filters to COCO class0(person)model_name(defaultyolov8m.hef)labels_name(defaultcoco.txt)
How it loads models
- It builds paths relative to repository root:
models/<model_name>models/<labels_name>
- Uses:
from picamera2.devices import Hailo - Calls
Hailo(self.model_path)and enters context (__enter__()). - Reads model input shape via
get_input_shape().
Detection extraction logic (_extract_detections)
The Hailo output is assumed to be in the Picamera2 Hailo example format:
hailo_output[class_id]is a list of detections- each detection:
[:4] = [y0, x0, y1, x1]in normalized coords;[4] = scoreThen it: - applies threshold filtering
- optionally filters class id (person-only)
- converts normalized coords to pixel coords safely
Output format
Returns list of dicts, typically like:
{
"bbox": [x0, y0, x1, y1],
"score": float,
"class_id": int,
"label": str
}
(Exact keys depend on the rest of the file, but bbox is definitely used downstream.)
4.3 ai/embedder.py — SmartEmbedder (ReID embeddings on Hailo)
Class: SmartEmbedder
Computes a “visual fingerprint” vector for each person detection.
Parameters
model_name(defaultrepvgg_a0_person_reid_512.hef)
Steps inside process_detections(frame, detections)
For each detection:
- Read
det["bbox"] - Clamp bbox to frame bounds (avoid crashes)
- Crop person region
- Resize to model input size
(model_w, model_h) - Run NPU inference:
raw_results = self.hailo.run(infer_crop) - Flatten results:
embedding = np.array(raw_results).flatten() - Normalize:
embedding = embedding / ||embedding|| - Attach:
det["embedding"] = embedding
Why L2 normalize?
Because cosine similarity becomes stable and comparable across frames and lighting changes.
4.4 ai/face_detector.py — SmartFaceDetector (Haar cascade bouncer)
Class: SmartFaceDetector
A CPU-based face presence checker used as a quality gate for updating snapshots.
Model dependency
models/haarcascade_frontalface_default.xml must exist.
Method: has_face(frame, person_bbox) -> bool
- Crops the person region using bbox
- Rejects too-small crops
- Converts to grayscale
- Runs:
detectMultiScale(scaleFactor=1.1, minNeighbors=4, minSize=(60,60))
- Returns
Trueif at least one face detected
This does not do recognition; it’s only “is there a frontal face right now?”.
4.5 database/db_manager.py — DatabaseManager (SQLite memory)
Class: DatabaseManager
Stores:
- embeddings (as BLOB)
- snapshot file paths
- timestamps (
last_seen) - name field (default
'Unknown')
Table schema (people)
id INTEGER PRIMARY KEY AUTOINCREMENTname TEXT DEFAULT 'Unknown'embedding BLOB NOT NULLsnapshot_path TEXTlast_seen TIMESTAMP
Method: add_new_person(embedding, snapshot_path=None) -> int
- converts numpy embedding to bytes
- inserts row
- returns new
id
Method: update_snapshot(person_id, embedding, snapshot_path, new_frame, bbox)
- crops bbox from
new_frame - overwrites existing snapshot image on disk
- updates
last_seenin DB
(Note: embedding parameter is currently not used to update embedding in DB in the shown code.)
Method: get_all_embeddings() -> dict[int, np.ndarray]
Loads every embedding:
SELECT id, embedding FROM people- converts each BLOB using
np.frombuffer(..., dtype=np.float32)
This provides the initial “gallery” for the tracker at startup.
4.6 ai/events.py — Events: saving new identities
Function: handle_new_person(db, person_id, embedding, frame, bbox) -> str
Triggered only once when a new identity is created.
Steps:
- Clamp bbox and crop the detection from the frame
- Save crop to
data/snapshots/person_<id>_<timestamp>.jpg - Insert the embedding + snapshot_path into SQLite via
db.add_new_person() - Returns the saved filepath (used by the tracker later)
4.7 ai/tracker.py — SmartTracker (Hybrid IoU + Cosine + Kalman)
Class: SmartTracker
Purpose
Assign stable IDs to detections over time, even with occlusions and crossing paths, by combining:
- Kalman filter predictions (motion model)
- IoU (spatial overlap)
- Cosine similarity (appearance match)
Constructor
SmartTracker(
db_manager,
face_detector,
similarity_threshold=0.65,
iou_threshold=0.40,
alpha=0.2,
max_lost_frames=300
)
- Loads database embeddings:
db.get_all_embeddings() - Builds
self.gallery:{ id: { "embedding": np.array, "kf": None, "last_seen": int, "best_area": float, "snapshot_path": str|None } } next_idstarts at max existing + 1colorsassigns consistent per-ID colors for drawing
Tracking algorithm, step-by-step (update(detections, raw_frame))
Phase 0 — bookkeeping
- increments
frame_count - creates
used_gallery_idsto prevent assigning two detections to the same ID in one frame
Phase 1 — Kalman predict
For every known person with a Kalman filter:
kf.predict()
This estimates where the bbox should be before matching.
Phase 2 — Matching cascade (per detection)
For each incoming detection:
- If
embeddingmissing → markUnknown - Else compute a best match among all gallery IDs not used yet:
iou_scorebetween incoming bbox and predicted bbox (from KF)cos_scorebetween incoming embedding and stored embedding
- Decision rule:
- If IoU passes
iou_threshold, it can dominate and force match - Else choose highest cosine score
- If IoU passes
Phase 3 — Assignment
If best score >= similarity_threshold:
- Assign existing
id - Update:
- Kalman filter with measurement
zbased on incoming bbox geometry - Embedding memory with EMA:
new_emb = alpha * incoming + (1-alpha) * old new_emb = new_emb / norm(new_emb) - Update
last_seen
- Kalman filter with measurement
Then snapshot upgrade logic:
- compute bbox area
- run face check:
face_detector.has_face(raw_frame, incoming_box) - if face present or area improved significantly:
- update best_area
- if snapshot_path exists, overwrite snapshot on disk & update
last_seenin DB
If not matched:
- Create new ID:
- call
handle_new_person()(saves snapshot + inserts DB record) - initialize Kalman filter for this new bbox
- create gallery entry with snapshot_path from event
- call
Phase 4 — Housekeeping (forget lost tracks)
Removes any ID not seen for more than max_lost_frames.
Kalman Filter Design (_init_kalman)
State: 7D, Measurement: 4D
It uses the common bbox tracker trick with measurement as:
- center x, center y, scale (area), ratio (w/h)
Includes tuning:
Rmeasurement noise (trust in detector)Pinitial covarianceQprocess noise (how much motion can deviate)
Also includes NaN/invalid protection in _kf_to_bbox() by forcing:
s = max(1.0, s)r = max(0.1, r)and usingsqrt(abs(s*r)).
Drawing (draw(frame, detections))
- draws rectangle + filled label background +
ID: <id> - uses deterministic random colors per integer ID
- gray if ID is
"Unknown"
4.8 app.py — FastAPI Web Server + MJPEG stream + DB API
Global runtime state
current_frame: most recent JPEG bytescurrent_stats:{"detected":0,"tracked":0,"unknown":0}- thread lock to protect frame buffer
Lifespan (lifespan(app))
On startup:
- Create DB:
DatabaseManager() - Create camera:
SmartCamera(source="auto", resolution=(1280,720), target_fps=30, flip_method="ccw") - Load detector:
SmartDetector(model_name="yolov8m.hef") - Load embedder:
SmartEmbedder(model_name="repvgg_a0_person_reid_512.hef") - Load face detector:
SmartFaceDetector() - Load tracker:
SmartTracker(db_manager=db, face_detector=face_detector) - Start background thread:
threading.Thread(target=ai_loop, daemon=True).start()
On shutdown:
- stop camera
- close face detector
AI loop (ai_loop())
Runs forever:
frame = camera.get_frame()detections = detector.detect(frame)detections = embedder.process_detections(frame, detections)tracked_objects = tracker.update(detections, frame)- Update stats:
- known = IDs that are
int - unknown = rest
- known = IDs that are
- Draw + encode JPEG into
current_frame
HTTP routes
GET /→ renderstemplates/index.htmlGET /video_feed→ MJPEG stream (multipart/x-mixed-replace)GET /api/stats→ live countsGET /api/records→ paginated/filterable/sortable list of DB people sightingsGET /api/record/{person_id}→ all sightings for a single person ID
4.9 main.py — Local “monitor mode” pipeline
This is the same AI pipeline but displayed using OpenCV:
- It sets:
DISPLAY=":0"QT_QPA_PLATFORM="xcb"so the window opens on the Pi’s HDMI output.
Loop:
camera.get_frame()- detector → embedder → tracker
- overlay real-time FPS computation
cv2.imshow(...)- press
qto quit
Clean shutdown closes camera, detector, embedder, face detector, and windows.
5) Algorithms Explained
5.1 YOLO Object Detection (on Hailo)
YOLO is a single-shot detector that predicts bounding boxes + classes in one forward pass. In SMART-CAM:
- It’s used primarily to detect
personclass. - The model is a Hailo-compiled
.heffile.
5.2 Person Re-Identification (ReID)
ReID creates a vector embedding such that:
- same person across frames → vectors are close (high cosine similarity)
- different people → vectors are far
SMART-CAM normalizes vectors to make cosine similarity stable.
5.3 Cosine Similarity
For embeddings a and b:
cos(a,b) = (a·b) / (||a|| ||b||)
After normalization, ||a|| = ||b|| = 1, so cosine becomes just a dot product.
5.4 IoU (Intersection over Union)
IoU measures overlap between two boxes:
IoU = area(intersection) / area(union)
SMART-CAM uses it to strongly favor spatial continuity when the predicted box still overlaps the new detection.
5.5 Kalman Filter (Constant Velocity)
A Kalman filter is a Bayesian estimator that predicts object motion. Here it smooths the bounding box center/size over time and helps maintain stable tracking when detections jitter.
5.6 Hybrid Matching Cascade (Why it works)
- IoU handles “same position” continuity even if appearance changes slightly.
- Cosine similarity handles identity continuity even if objects cross or boxes shift.
- Kalman predicts where to look next, stabilizing IoU and handling momentary gaps.
6) Running the Project
Install dependencies
pip install -r requirements.txt
Add models to models/
You must add:
models/yolov8m.hefmodels/repvgg_a0_person_reid_512.hefmodels/haarcascade_frontalface_default.xml
Web dashboard mode
python app.py
Then open:
http://<PI_IP>:5000
Local monitor mode
python main.py
7) Data Outputs
- SQLite DB:
data/database.sqlite - Snapshots:
data/snapshots/*.jpg
Each new person creates:
- 1 DB row with embedding + snapshot_path + last_seen
- 1 image crop file saved on disk
Snapshots may be overwritten later with a better image (face found or larger bbox).
8) Notes / Limitations
- Model files are required and are not committed in the repo by default.
- The Hailo/Picamera2 integration requires Raspberry Pi + correct system packages.
DatabaseManager.get_all_embeddings()assumes embeddings are stored asfloat32bytes.- The tracker removes identities after
max_lost_framessince last seen. - Web streaming uses MJPEG which is simple and fast, but not as bandwidth-efficient as modern codecs.
What's inside
8 module descriptions, 6 algorithm explanations, 2 run modes, 1 database schema, and 1 repository structure overview.
Change this for your project
- Replace
models/yolov8m.hefwith your own Hailo-compiled YOLO model path - Replace
models/repvgg_a0_person_reid_512.hefwith your own ReID model path - Replace
models/haarcascade_frontalface_default.xmlwith your own face cascade file - Replace
WajeehAlamoudi/Smart_AI_Camerain the header with your repository name
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Background thread for camera capture decouples frame acquisition from AI processing
- Kalman filter predicts bounding box position to stabilise tracking across jittery detections
- Exponential moving average on embeddings smooths appearance features over time
Related Documents
Building SupportX AI Assist: A Multi-Agent IT Support System
Describes building a multi-agent IT support system with AutoGen, Azure AI Search, and Gemini embeddings for instant issue resolution and automatic escalation.
Intelligent Document Query Platform — GitHub-ready Low-Level Design (LLD)
Provides a copy-ready low-level design for a serverless document query platform with vector search and LLM integration.
Graph Matching with Topological Features
Teaches enhanced graph matching by combining spatial distances with node2vec and commute times embeddings, then applying the Hungarian algorithm.
Pulse — Life Cofounder | Build Log
Documents a full-stack monorepo that ingests LinkedIn and GitHub data, generates embeddings in-browser, and provides a RAG chat with an AI cofounder.