SPOTTED
A retail-safety CV platform that turns camera feeds into reviewable candidate events — heavy processing stays local, and a human decides.
This is the full engineering dossier behind the project card. Internally the codebase is SPOTTER. It chains YOLOv8 detection, ByteTrack tracking, ResNet re-identification, and a hand-built concealment heuristic into a live pipeline, then closes the loop with a locally-generated spoken announcement — the piece that won Best Use of Gemma 4. Every claim here is verified against the source, and the design's defining feature is a deliberate human-in-the-loop, privacy-first posture.
How to read this. The most important section is §4 — Threat & Ethics, because a system that watches people and flags "theft" is judged first on how it can be wrong, not on its frame rate. The design's honest answer — candidate events, local processing, and a mandatory human review before any accusation — is the thing to lead with. §12 tracks the gap between the shipped prototype and that intent.
What SPOTTED is, in one screen
SPOTTED watches a camera or video source, tracks the people and objects in frame, and surfaces candidate concealment events for a human to review rather than acting on its own. When a reviewer confirms one, the system can generate a spoken de-escalation announcement locally and speak it aloud. Events flow to a database and an analytics warehouse so a manager can ask questions about them in plain language.
An honest note on what ships
The repository is candid that it is a reviewable prototype, not a production surveillance product. The trained theft classifier's weights are intentionally not included, so as shipped both backends run in detection-and-heuristic mode: YOLO finds people and objects, and a geometric concealment heuristic — not a learned theft model — raises the candidate flags. That is a feature of the framing, not a bug hidden by it: the design assumes a person, never the model, makes the call.
The stack, precisely labeled
| Layer | Technology | What it does |
|---|---|---|
| Detection | YOLOv8 · ByteTrack | Person + object detection and multi-object tracking per camera. |
| Re-ID | ResNet18 embeddings | Per-track appearance features, averaged over a history buffer. |
| Classifier | ResNet18 + LSTM (weights not shipped) | Temporal theft classifier; absent → heuristic fallback. |
| Backends | Flask (main) · FastAPI (stream) | Detection engine + an MJPEG stream that writes events to Mongo. |
| Announce | Gemma 4 (Ollama) → ElevenLabs | Local model drafts the announcement; ElevenLabs speaks it. |
| Clip review | Gemini 2.5 (cloud, opt-in) | Reviews exported candidate clips only — never raw streams. |
| Data + analytics | MongoDB → Snowflake Cortex | Event store synced to Cortex Search (RAG) + Cortex Complete (text-to-SQL). |
| Dashboard | Next.js 16 · React 19 · Three.js / R3F | Cameras, alerts, events, analytics, operator review, 3D view. |
How the pieces fit together
Heavy pixels stay local. A Python backend does all frame processing on the host and emits only lightweight event records; the Next.js dashboard, the announcement loop, the cloud clip review, and the analytics warehouse all work from those records and exported clips — never a raw video stream.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','secondaryColor':'#111','tertiaryColor':'#0a0a0a','clusterBkg':'#0c0c0e','clusterBorder':'#262626','fontFamily':'JetBrains Mono, monospace','fontSize':'13px','textColor':'#c0c0c0'}}}%%
flowchart TB
subgraph EDGE["Local host (heavy pixels stay here)"]
CAM["Camera / video"]
CV["YOLOv8 + ByteTrack
ResNet re-ID + concealment heuristic"]
end
subgraph BACK["Backends"]
ST["FastAPI stream
MJPEG + event writes"]
end
subgraph STORE["Event store"]
MG[("MongoDB
events · alerts · cameras")]
end
subgraph WEB["Next.js 16 dashboard (R3F)"]
OPS["Operator review · alerts · events"]
ANA["Analytics chat"]
end
subgraph LOOP["Announce loop"]
GEM["Gemma 4 (Ollama)
draft announcement"]
EL["ElevenLabs TTS
speak deterrent"]
end
subgraph CLOUD["Cloud (candidate clips + analytics)"]
GEMINI["Gemini 2.5
clip verdict (opt-in)"]
SNOW[("Snowflake Cortex
Search + Complete")]
end
CAM --> CV --> ST --> MG
MG --> OPS
OPS -->|"confirm"| GEM --> EL
OPS -->|"export clip"| GEMINI
MG -->|"sync"| SNOW --> ANA
Fig. 1 — Component architecture. Only event records and exported clips ever leave the host.
The detection-to-deterrent loop
The live path is a chain of models feeding a heuristic, gated by a human before anything is spoken.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','actorBkg':'#151517','actorBorder':'#00ff41','actorTextColor':'#e6e6e6','signalColor':'#666','signalTextColor':'#c0c0c0','noteBkgColor':'#111','noteTextColor':'#c0c0c0','noteBorderColor':'#333','fontFamily':'JetBrains Mono, monospace','fontSize':'12px'}}}%%
sequenceDiagram
participant F as Frame
participant Y as YOLO + ByteTrack
participant H as Concealment heuristic
participant DB as MongoDB
participant OP as Operator
participant G as Gemma (local)
participant E as ElevenLabs
F->>Y: detect persons + items, track IDs
Y->>H: item enters torso/hand zone, moves, vanishes
H->>DB: write candidate event (+ optional alert)
DB->>OP: surface for review
Note over OP: human decides — no auto-accusation
OP->>G: confirm → draft announcement
G->>E: announcement text
E-->>OP: spoken de-escalation audio
Fig. 2 — Live loop. The human review step (highlighted) is the design's safety keystone.
Design decisions & the trade-offs behind them
Four vision stages, not one model
Detection is a pipeline. YOLOv8 finds people and a curated set of carry-able objects; ByteTrack gives each a persistent ID across frames; a ResNet18 feature extractor builds an appearance embedding per track, averaged over a rolling buffer for re-identification; and a ResNet18+LSTM temporal model classifies clip sequences as normal or theft when its weights are present. Layering cheap detection under expensive temporal reasoning keeps the common case fast.
The concealment heuristic — the real shipped logic
Because the learned classifier's weights aren't in the repo, the flags you actually see come from a geometric heuristic, and it is genuinely considered engineering. It defines per-person concealment zones (torso, legs) and hand zones, tracks each item's motion and displacement, and raises a candidate only on a specific transition: an item that was visible outside the body, interacted with near a hand, then moved into the torso zone and stayed — or vanished near the torso while last seen in-hand. It deliberately requires movement, persistence, and a plausible body region to fire, precisely because a naive "item overlaps person" rule would be a false-positive machine.
Two backends, one detector
The original detector is a Flask app with an in-browser analysis endpoint; a second FastAPI service wraps the same detection function to expose a clean MJPEG stream and write events to MongoDB. It is honest to note this duplication — the temporal model class is defined in three places — and to treat consolidating on one service as future work.
Local-first AI, cloud only for review
The announcement is drafted by a local Gemma 4 model via Ollama and voiced by ElevenLabs — the award-winning piece, and the reason the live loop needs no cloud LLM. Cloud vision (Gemini) is reserved for reviewing exported candidate clips, never live streams, and it is opt-in. Analytics run on Snowflake Cortex: events sync from Mongo, and a manager can use Cortex Search for RAG-style questions and Cortex Complete for natural-language-to-SQL over the event history.
The real risk is being wrong about a person
Security threat-modeling matters here, but it is secondary. The defining risk of a system that watches people and outputs "theft" is a false accusation of a real human being — and the way that risk distributes unequally across who gets watched and flagged. A senior review will probe this first, so it belongs first.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','clusterBkg':'#0c0c0e','clusterBorder':'#262626','fontFamily':'JetBrains Mono, monospace','fontSize':'12px','textColor':'#c0c0c0'}}}%%
flowchart LR
subgraph Z1["Subjects (no consent flow)"]
P["People in frame"]
end
subgraph Z2["Local host"]
CV["CV pipeline"]
BIO[("Track snapshots
crops + embeddings")]
end
subgraph Z3["Decision"]
HUM["Human reviewer
(required gate)"]
end
subgraph Z4["Downstream"]
MG[("MongoDB")]
CLOUD["Gemini clip / Snowflake"]
ACT["Announcement / action"]
end
P ==>|"TB① surveillance, no consent"| CV
CV --> BIO
CV --> MG --> HUM
HUM ==>|"TB② the accountability gate"| ACT
MG -.->|"TB③ clips + records to cloud"| CLOUD
Fig. 3 — Trust boundaries. TB② — the human gate — is what separates a review tool from an automated accuser.
Ethics & privacy
| Concern | How it lives in SPOTTED | Design response |
|---|---|---|
| False accusation | A heuristic (or a classifier) can flag an innocent action. As shipped, the "Shoplifting" label is heuristic and its confidence is a fixed value. | Never auto-act. Every flag is a candidate a human confirms; the announcement is de-escalation, not detention. |
| Demographic bias | Theft-detection models and heuristics can encode and amplify bias in who is scrutinized. | Treat bias as a first-class metric: measure flag rates across groups, and never ship the learned classifier without a fairness audit. |
| Biometric data | Per-track crops and ResNet embeddings are written to disk as re-ID snapshots — biometric identifiers of individuals. | Encrypt at rest, set short retention, and scope re-ID to a session rather than a durable identity. |
| Consent & notice | People in frame have no consent or notice flow. | Signage, a documented lawful basis, and jurisdiction-aware configuration before any real deployment. |
| Cloud exposure | Exported clips of real people can be sent to Gemini; events sync to Snowflake. | Keep it opt-in and clips-only; redact where possible; make the local path the default. |
Security
On the classic axis: the MJPEG stream binds all interfaces with wildcard CORS and no authentication, so an open video feed of a monitored space is a real exposure; a MongoDB connection string (host + user) sits in the committed env examples; and the biometric snapshots are unencrypted on disk. The fixes are conventional — authenticate the stream, bind loopback or put it behind a gateway, move secrets out of the repo, and encrypt the snapshot store.
The question you must answer well: "How do you keep this from wrongly accusing someone, and from doing it unequally?" The strong answer is structural, not a promise to be careful: the system produces candidates, a human is the required decision-maker, the output is de-escalation rather than detention, and bias is a measured metric you'd gate the classifier on. That is the difference between a responsible prototype and a liability.
Running it
# Local detector (Flask) — YOLO-only if video_model.pth is absent pip install -r requirements.txt # ultralytics, opencv, torch, Flask, google-genai python main.py # http://localhost:8000 # Streaming backend (FastAPI) + Next.js dashboard python backend/stream.py # MJPEG + Mongo writes cd web && npm install && npm run dev
Optional services activate through environment variables and each degrades gracefully if unset: MONGODB_URI for the event store, GEMMA_API_URL/GEMMA_MODEL for the local announcement model, ELEVENLABS_API_KEY for voice, GEMINI_API_KEY for opt-in clip review, and Snowflake credentials for analytics. Without the trained video_model.pth, both backends run detection-and-heuristic mode.
Reality to state up front: this runs against a webcam or a bundled video source on a developer machine, not a camera fleet. RTSP fleet management, durable workers, and auth are explicitly out of scope for the prototype — and that honesty is part of the pitch, not a hole in it.
What verification looks like for a CV system
Detection quality here is validated visually and by labeled-video playback — the evaluation script runs the classifier over folders of shoplift and normal clips with ground-truth overlays. That is the right instinct, but for a system whose errors land on people, the strategy has to be more formal than eyeballing.
The strategy to articulate
The honest plan has three tiers. First, a labeled benchmark with real precision/recall and, critically, a false-positive rate for the concealment heuristic — the metric that matters most when a false flag means a wrongful stop. Second, unit tests around the deterministic geometry (zone membership, track transitions, IoU) that drives the flags. Third, and non-negotiable before the learned classifier ships, a fairness evaluation of flag rates across demographic groups. Being able to say that the current evidence is qualitative and to name the metrics you'd hold it to is the senior move.
A five-minute live walkthrough
Run against the bundled video source so behavior is deterministic. Lead with the pipeline, land on the human gate and the local announcement — the two things that make it defensible and the thing that won the award.
- 0:00Open the live view. Point out YOLO boxes + track IDs following people and items. "Detection and tracking are running locally on the host — no video leaves this machine."
- 0:45Walk an item into a torso zone. Show the candidate concealment flag fire, and stress the wording: "candidate, not verdict."
- 1:30Switch to the dashboard — the event appears in the operator review queue with its clip. "A person reviews this. The system never accuses on its own."
- 2:30Confirm the event and trigger the announcement: a local Gemma model drafts a de-escalation line, ElevenLabs speaks it. "This is the Best-Use-of-Gemma piece — and it runs locally."
- 3:30Show optional Gemini clip review on the exported clip. "Cloud vision only ever sees a short candidate clip, never the stream — that's the privacy line."
- 4:15Open analytics: ask a plain-language question and show Cortex answering over synced event data. Close on §4 — "the design's real feature is the human in the loop."
The line that lands: "It produces candidates, not accusations — a person decides, and the only automated action is a spoken de-escalation." Volunteering the ethics posture before you're asked is exactly what separates a thoughtful CV engineer from someone who just wired up a model.
Ninety seconds, no jargon
Retail theft is a real problem, but the tech built to catch it is usually a black box that just points a finger — and gets people wrongly accused. SPOTTED is my answer to doing it responsibly: it flags things worth a second look and hands the decision to a human.
What it does · 35sIt watches a camera, tracks the people and items in view, and notices when something looks like it's being concealed. Instead of raising an alarm on its own, it puts that moment in a review queue for a staff member. If they confirm it, the system speaks a calm de-escalation announcement out loud — and that announcement is written by an AI model running right there on the machine.
Why it's mine to defend · 25sI built the vision pipeline, the review dashboard, and the announcement loop — and the local model piece won Best Use of Gemma at the hackathon. The whole thing is designed to keep the heavy video on-site and only ever send a short clip to the cloud, never a live feed.
The close · 15sIt runs against a demo video end to end, so I can show the detect-review-announce loop in about five minutes.
The technical explanation
SPOTTED is a retail-safety computer-vision system with a deliberate human-in-the-loop design. A Python backend does all the frame processing on the host and emits only event records; a Next.js dashboard, the announcement loop, and a Snowflake analytics layer all work off those records, so raw video never leaves the machine.
The vision pipeline · ~75sDetection is four stages. YOLOv8 finds people and carry-able objects, ByteTrack assigns persistent track IDs, a ResNet18 builds an appearance embedding per track for re-identification, and a ResNet-plus-LSTM temporal model classifies sequences as normal or theft when its weights are present. Those weights aren't in the repo on purpose, so what actually fires the flags is a geometric concealment heuristic I wrote: it models torso and hand zones per person, tracks each item's motion, and only raises a candidate on a specific transition — an item that was visible, handled near a hand, then moved into the torso zone and stayed or vanished there. It requires movement and persistence specifically to avoid the false positives a naive overlap rule would produce.
The loop & data · ~75sWhen a candidate fires, it's written to MongoDB and surfaced to an operator. Nothing is automated until a human confirms. On confirmation, a local Gemma model via Ollama drafts a de-escalation announcement and ElevenLabs voices it — that local-model piece is what won Best Use of Gemma. Optionally, an exported clip — never the live stream — can go to Gemini for a second-opinion verdict. Events sync to Snowflake, and a manager can query them with Cortex Search for retrieval and Cortex Complete for natural-language-to-SQL.
Honest edges · ~45sTwo things I'd flag. The learned classifier isn't shipped, so as-is it's detection plus a heuristic, not a trained theft model — and I wouldn't ship that model without a fairness audit. And the honest center of gravity of this project isn't accuracy, it's the ethics: the human gate, the candidates-not-verdicts framing, and keeping video local are the design, not decoration.
The questions this project invites
Grouped by where the pressure comes from. The hardest questions here are ethical, and the answers meet them head-on — a serious panel is testing whether you saw the risk before they had to raise it.
On responsibility
QHow do you stop this from wrongly accusing someone?
Structural, not a promise. "The system never accuses. It produces candidate events; a human reviewer is the required decision-maker; and the only automated action is a spoken de-escalation, not a detention. The architecture makes 'the model was wrong' a reviewer's catch, not a customer's problem."
QWhat about bias — who gets flagged more?
Name it as a metric. "That's the risk I take most seriously. Theft heuristics and classifiers can encode bias in who gets scrutinized. I'd treat flag rate across demographic groups as a first-class metric, measure it on a benchmark, and I would not ship the learned classifier without a fairness audit. For the prototype, the human gate is the backstop."
QYou store crops and face-adjacent embeddings. Isn't that biometric data?
Yes — treat it that way. "The re-ID snapshots are biometric identifiers and they're currently unencrypted on disk with no retention limit. That's a finding. The fix is encryption at rest, short retention, and scoping re-ID to a session rather than a durable identity."
On the vision engineering
QIs the shoplifting detection a trained model?
Be precise. "There's a ResNet+LSTM temporal classifier in the code, but its weights aren't in the repo, so what ships raises flags with a geometric concealment heuristic. I won't claim a trained theft model is doing the work when it isn't — and the heuristic is deliberately conservative to control false positives."
QWalk me through the concealment heuristic's false positives.
Show the reasoning. "A naive 'item over person' rule fires constantly — people hold things. So I require a transition: the item was visible outside the body, was handled near a hand, then moved into the torso zone and either stayed or vanished there, with minimum movement and frame persistence. Each condition exists to kill a common false positive, and I'd still measure the residual FP rate on a benchmark."
On architecture
QWhy keep video local and only send clips to the cloud?
Privacy by construction. "Streaming raw video of a public space to a third-party API is a privacy and cost problem. So the heavy processing is local, the live announcement uses a local Gemma model, and cloud vision only ever sees a short exported candidate clip, opt-in. It keeps the sensitive data on-site by default."
QYou have a Flask app and a FastAPI app. Why?
Own the duplication. "The Flask app was the original detector; the FastAPI service wraps the same detection function to give the dashboard a clean stream and Mongo writes. The temporal-model class ends up defined in a few places — consolidating on one service with a shared detector module is straightforward cleanup I'd do next."
The honest roadmap
Classifier not shipped
Runs detection + heuristic as-is. Next: ship the trained model behind a fairness audit and a measured false-positive rate.
Bias unmeasured
No fairness metrics yet. Next: flag-rate-by-group as a gating metric before any learned model deploys.
Biometric data at rest
Unencrypted snapshots, no retention. Next: encrypt, expire, and session-scope re-ID.
Unauthenticated stream
Open MJPEG, wildcard CORS. Next: auth the feed, bind loopback / gateway, secrets out of the repo.
Two backends
Flask + FastAPI with a duplicated model class. Next: one service over a shared detector module.
Prototype scope
Webcam/video, no fleet. Next: RTSP fleet management, durable workers, deployment hardening.
Genuine strengths to lead with: a real four-stage vision pipeline (YOLOv8, ByteTrack, ResNet re-ID, temporal classifier) plus a genuinely thoughtful concealment heuristic; a working detect-review-announce loop with an award-winning local-Gemma announcement generator and ElevenLabs voice; a privacy-first architecture that keeps video local and sends only opt-in candidate clips to the cloud; a full analytics path from MongoDB to Snowflake Cortex Search and Complete; and — most importantly — a mature, human-in-the-loop ethical posture the project states honestly.
Known limitations, tracked as findings
Documented like any other assessment — each limitation logged as a severity-tagged finding with a fix. For a system that watches people, the top findings are as much about ethics as engineering.
- Detail
- Theft detection can distribute scrutiny unequally. There is currently no measurement of flag rates across groups.
- Fix
- Make flag-rate-by-group a first-class, gating metric; do not ship the learned classifier without a fairness audit.
- Detail
- As shipped, both backends run detection-only and the "Shoplifting" label comes from the concealment heuristic with a fixed confidence.
- Fix
- Describe it precisely; measure the heuristic's false-positive rate; version and publish the classifier weights when audited.
- Detail
- Person crops and ResNet embeddings are written to disk as durable identifiers.
- Fix
- Encrypt at rest, set short retention, scope re-ID to a session.
- Detail
- The stream binds all interfaces with no auth — an open live view of a monitored space. Env examples carry a MongoDB host + user.
- Fix
- Authenticate the feed, bind loopback or a gateway, move secrets out of the repo.
- Detail
- Flask (main) and FastAPI (stream) both define the temporal model; the card says FastAPI while the primary detector is Flask.
- Fix
- Consolidate on one service backed by a shared detector module; align the described stack.
- Detail
- Collaborator research scripts carry absolute local paths; model weights and a video are committed to the repo.
- Fix
- Parameterize paths; move large assets to a release or object store.
- Detail
- Validation is visual/playback; there is no benchmark precision/recall or false-positive rate.
- Fix
- Build a labeled benchmark and report the metrics, foregrounding the false-positive rate.
Sequenced: F1 and F2 are the responsibility work — measure bias and be exact about what model actually runs — and they gate any real deployment. F3–F5 are the privacy and security hardening. F6–F7 are hygiene and evaluation that turn a hackathon winner into something a store could pilot without becoming a liability.