Back to Projects

    KC CCTV Monitor — YOLOv8 Loss Prevention on Existing Cameras

    KC CCTV Monitor is an on-premise computer vision system that turns a retail or food-service site's existing CCTV cameras into loss-prevention and productivity alerts. YOLOv8 detects people and objects, pose estimation scores posture over time, and flagged moments become a snapshot plus a short clip for a human to review.

    2026
    Computer Vision Engineer
    12 Technologies
    PythonYOLOv8 (Ultralytics)YOLOv8-poseMediaPipe Pose LandmarkerOpenCVInsightFaceONNX RuntimeCompreFaceFastAPIFlask + Flask-SocketIOSQLAlchemyDocker Compose
    KC CCTV Monitor — YOLOv8 Loss Prevention on Existing Cameras

    Introduction

    Almost every shop, cafe and warehouse already has cameras. What they do not have is anyone watching them, and footage only gets pulled after something has already gone missing. KC CCTV Monitor is my attempt to close that gap with the cameras already on the wall, no new hardware except a machine on site to run the models.

    The Challenge

    The constraint that shaped this build was that the cameras are fixed. I do not get to reposition them, upgrade the sensors or add a POS integration on day one. I get RTSP streams at whatever angle and resolution the installer chose years ago, and one site had 23 of them. That rules out anything that needs clean frontal framing or high frame rates. It also means the honest ceiling on accuracy is lower than a demo video suggests, so the product has to be designed around review rather than around automated decisions.

    The Solution

    I built it in two parts, in two repositories. The MVP repository is a Flask app with a detection pipeline that runs YOLOv8 for objects, a centroid tracker for identity across frames, and MediaPipe Pose Landmarker for activity, wired to two modules: a cash-drawer monitor and a productivity tracker. It writes alerts, detections, drawer events and productivity snapshots to SQLite and pushes them to a dashboard over Socket.IO. The second repository is a FastAPI service built for the site's Dahua NVR: a shared frame hub gives every worker one connection per camera, a YOLOv8-pose engine scores theft-like behaviour per tracked person, and a face engine matches enrolled offenders. Both write an annotated snapshot and a short rolling clip when they flag something.

    Technical Deep Dive

    1

    Detection versus inference is the line I care most about. What the system actually detects is narrow: YOLOv8 person and object boxes with confidence scores, 17 COCO keypoints per person from YOLOv8-pose, 33-point MediaPipe landmarks in the MVP, face embeddings and their cosine similarity to enrolled subjects, and raw pixel change inside a region. Everything else is inference layered on top of that. Idle, on a long break, theft-like behaviour and drawer open are all heuristics over geometry and time, and the code says so in its own comments.

    2

    Pose heuristics are normalised by torso length rather than pixels, and scored over time rather than per frame. The engine measures the shoulder-centre to hip-centre distance per person and expresses every threshold as a fraction of it, so a wrist within 0.55 torso-lengths of a hip and at or below it counts as hand-to-pocket regardless of how far the person stands from the camera. Each track then carries a smoothed suspicion score that decays toward zero at 0.80 every frame and blends in the fresh score at 0.45. A flag needs that smoothed score over 0.70, sustained for 1.25 seconds, with a two-frame grace so tracker jitter does not reset the streak, plus concealment-class evidence in at least 2 of the last 10 processed frames. Without that last gate, three weak posture signals could saturate the score on someone who is just browsing.

    3

    False positives got their own filters, because in a clothing shop the baseline behaviour looks exactly like the theft signals. A jacket on a rack that YOLO reads as a person yields few confident keypoints, so tracks under six valid keypoints are dropped. A real person's bounding box centre always jitters, so a track that stays within 0.008 of the frame diagonal for ten seconds is treated as a static object and excluded until it moves. A hand parked in a pocket loses most of its weight after four seconds, since only a fresh dive is interesting. Loitering is 30 seconds, not 8, because standing at a rack is shopping.

    4

    The MVP's cash module does not use an object detector for the drawer. It takes a normalised region of interest, converts to grayscale, blurs it, and compares consecutive frames: if more than 15 percent of pixels shift by more than 30 intensity levels, it toggles drawer state. That is deliberately crude and cheap, and it is why the module attributes the event to the nearest tracked person rather than claiming to see whose hand it was. Thresholds for how long a drawer may stay open and how long someone may linger near it live in one config file.

    5

    Compute is rationed at three points, because 23 streams on one box is the real engineering problem. Cameras are sampled at a configured frames-per-second rather than read at full rate, detection defaults to the NVR sub-stream with the main stream left as a documented switch for when face quality matters more than load, and a motion gate skips pose inference entirely when fewer than 0.4 percent of pixels changed. A shared frame hub means one RTSP connection per camera no matter how many workers consume it, and each behaviour engine owns its own model instance so one consumer's tracker state cannot corrupt another's.

    6

    Face recognition is quality-gated rather than single-threshold. The local InsightFace engine raises the similarity bar for small or weakly detected faces: below a minimum pixel height the embedding is not trusted at all, and faces under the strong-height cutoff must clear a higher cosine bar before the system will assert a match. A CompreFace service is also wired in behind a REST client as an alternative recognition backend, with its own configurable threshold and per-subject alert cooldown.

    7

    Privacy posture is in the schema, not just the docs. Enrolled offender records carry an expiry, and a retention job marks anything past it as expired while writing an audit row for each. Offender enrolment and edits, alert review and the retention run all write through one audit helper into an audit-log table, users carry admin, enroller, reviewer or viewer roles enforced by a route-prefix rule table, and alerts carry a pending, confirmed or dismissed status with the reviewer recorded. The PRD states the operating rules plainly: store embeddings and minimal images, no face data leaves the local network, no tracking of non-enrolled people beyond transient detection, and no enrolment before signage, a documented lawful basis and legal sign-off.

    Key Features

    Evidence, not just notifications

    When a flag fires, the worker saves an annotated snapshot and writes a rolling clip of the seconds leading up to it, then persists an alert row linking camera, timestamp, score and clip path. A reviewer opens the clip and marks the alert confirmed or dismissed. Cooldowns suppress repeats per camera and per tracked person so one incident does not become fifty notifications.

    Operator-drawn zones

    Zones are rectangles drawn on a camera view and stored in normalised coordinates so they survive a resolution change. Two kinds exist: intrusion zones that fire when a person's feet stay inside for a set linger time, and object zones that hold a reference patch of a till or stock area and fire object-removed if the patch stays different while nobody is standing in front of it.

    Productivity as measured time, not a judgement

    The MVP tracks per-person active and idle seconds from wrist and elbow movement between frames, records which configured zone each person is in, counts food and beverage classes appearing in the prep zone, and flushes a snapshot every five minutes. The output is a time series in a database table. Interpreting it is a manager's job, and the attribution of items to a specific person is admittedly the weakest link in that chain.

    Camera-agnostic ingest

    The URL builder resolves per-channel overrides first, then a single custom RTSP URL, then the Dahua NVR format. That lets the same pipelines drive an NVR, a standalone ONVIF or Xiaomi camera, or a mix of brands bridged through go2rtc, with no code change. Channels get human-readable names so the dashboard says Till 1 rather than channel 2.

    Honest worker telemetry

    Each camera worker tracks frames processed, last frame timestamp, last score, current motion ratio and last error, all exposed through a status endpoint. That is enough to tell a camera that is connected but static apart from one that is genuinely scoring frames. Workers catch per-frame exceptions and keep running instead of dying silently, which is what goes wrong on a site with 23 cameras.

    Results & Impact

    • Two codebases exist and are structured around different jobs: the Flask MVP is what I put in front of people to show detection working on a laptop, and the FastAPI build is the one written for the site's 23-channel Dahua NVR, shipping with Docker Compose, a launchd service definition and a deployment runbook.
    • The pipeline demonstrably produces what it claims to produce: person and object boxes with track IDs, per-person pose keypoints, zone membership, drawer state changes, per-window productivity rows, and alerts with a snapshot and a clip attached. Those are things you can open and check.
    • The compliance surface is implemented rather than promised. Roles with a route-level gate, an audit-log table that offender changes, alert review and the retention run write through, alert review states, and a runnable retention job that expires enrolled records and logs the expiry all exist in code.
    • I have not published accuracy numbers and will not. The heuristics are geometric approximations, the module docstring says so, and the correct claim for a system like this is that it narrows what a human has to watch, not that it identifies theft.
    • A static demo site under the same repository lets a prospect click through the dashboard, alerts log, heatmap and cash monitor without me installing anything. It is a simulation driven by sample clips and precomputed track data, and I label it that way rather than passing it off as live inference.

    Lessons Learned

    "Tuning is site-specific and I stopped pretending otherwise. The thresholds that work in a clothing shop are wrong in a cafe, because holding a garment to your chest is normal in one and not the other. So the constants moved into a documented block with the reasoning written next to each value, and the sensitive ones read from environment variables. A vision product like this is a configuration exercise as much as a modelling one."

    "The theft grammar beats any single pose. Hand-to-pocket on its own fires on people with cold hands. Bending fires on anyone tying a shoelace. What carries real signal is the sequence: an extended reach toward a rack followed within a few seconds by a hand going to a pocket or torso. I ended up weighting that sequence above every individual posture, and demoting the poses that a shop's normal behaviour triggers constantly."

    "Writing the false-positive story into the code comments changed how the product got sold. The behaviour module opens by stating that returning an item to a shelf, reaching into your own bag and putting your phone away all look identical to theft from pose alone. Once that is the first thing a reader sees, the conversation with a client becomes about review workflow and staffing rather than about a promised detection rate, which is the conversation that actually leads to a working deployment."

    "The MVP's architecture diagram claims ByteTrack, and the MVP code ships a centroid tracker with Hungarian assignment. The second repository's behaviour engine does use the ByteTrack tracker built into YOLOv8-pose. Small gaps like that are how a README stops being trustworthy, and I would rather flag it here than let a technical buyer find it during due diligence."

    Conclusion

    The interesting part of this project was never the model. It was deciding what the system is allowed to assert, and then building the review workflow, the retention rules and the audit trail that a claim about a person requires. If you have cameras and no one watching them, that is the shape of the problem worth solving.

    Frequently asked questions

    Does this actually detect theft?
    No. It detects people, objects and pose keypoints, then scores geometric patterns over time that often accompany concealment. A flag means a human should glance at a ten-second clip. The behaviour module's own documentation states that returning an item to a shelf or putting a phone away produces the same signals, and the system is built around that limitation.
    Do I need new cameras or a cloud subscription?
    No. It ingests RTSP from what you already have, including a Dahua NVR, standalone ONVIF cameras, or a mix bridged through go2rtc. What you do need is a machine on site to run inference. Video is never streamed to a cloud vision service. The only thing leaving the network is an alert, optionally with its snapshot and clip attached.
    What actually gets stored, and what is discarded?
    Live frames are sampled and processed in memory, not archived by this system. Persisted records are alerts with an annotated snapshot and a short clip, detection rows, zone and drawer events, and productivity time windows. On the face side, enrolled subjects hold embeddings plus minimal images. Non-enrolled people are not tracked beyond transient detection.
    Is it open source, and can I see the code?
    Both repositories are private client work, so there is no public repo or live inference demo to link. What I can share in a call is a walkthrough of the pipeline structure, the behaviour scoring constants and their reasoning, and the deployment runbook. The static preview site that exists is a simulation for client demos, not live inference.
    How does it handle 23 cameras on one machine?
    Three ways. Cameras are sampled at a configured frames-per-second rather than read at full rate, detection runs on the NVR sub-stream with the main stream reserved for face quality, and a motion gate skips pose inference on static frames. A shared frame hub keeps one RTSP connection per camera regardless of how many workers read it.
    Is face recognition legal for my site?
    That is a question for your lawyer, and the project treats it as a blocker rather than a detail. Biometric data is regulated under BIPA, GDPR and various state laws, and some jurisdictions restrict retail facial recognition outright. The build ships retention expiry, an audit log and role-based access, but signage, a documented lawful basis and client sign-off come before anyone is enrolled.
    Can the behaviour side run without face recognition?
    Yes, and that is often the right starting point. The pose-based behaviour engine answers whether someone is moving like they are concealing something, entirely independent of identity. It needs no enrolment, stores no biometric records, and therefore carries a far lighter compliance load. Face matching is a separate phase you can decide on later.

    Interested in a Similar Project?

    Let's discuss how I can help bring your ideas to life.

    Get in Touch

    Let's Create a Revolution