Skip to content

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

SentinelVision: per-worker PPE compliance from the cameras you already own

SentinelVision binds every PPE observation to a tracked worker, applies the policy you configure,
and writes a tamper-evident audit trail. Commodity cameras. A CPU. No cloud dependency.


Live site Source Model weights

status tests coverage license python YOLOv8 ByteTrack

Live site · Quick start · How it works · Demo · Roadmap · Links


Note

Status: Phase 2 (accuracy core) complete. Detector, class taxonomy, visualization, CLI and the full pipeline: track → associate → N-of-M vote → track-bound violation events. Phases 3–8 are roadmap, not built yet.

Benchmark (Phase 8): SH17 val mAP@50 = TBD · mAP@50:95 = TBD · person-attributed violation F1 = TBD


Highlights

Per-worker attribution Every NO-Hardhat is bound to a tracked person, never a floating box.
Temporal voting A violation must appear in 6 of the last 10 frames. One bad frame can never raise an event.
Tamper-evident audit trail SHA-256 hash-chained event log. Edit one character and the chain breaks.
Class-aware thresholds Recall-tolerant for people, precision-strict for violations.
Runs anywhere Commodity cameras, CPU only, zero cloud dependency.
Weight-free tests 289 tests, 96% coverage, no torch, no weights, no camera required.

Try it in two minutes

pip install -e ".[dev,vision]"
pip install gradio
python app.py            # opens http://127.0.0.1:7860

The demo app has four tabs:

Tab What it shows
Image Detect, bind PPE to each worker, judge against your policy, annotate.
Video · temporal vote The same pipeline over footage, with a 6-of-10 quorum before a violation is committed.
Audit trail A SHA-256 hash-chained events.jsonl. Edit any character, press Verify chain, and watch it break.
Policy & classes The ten-class taxonomy with per-class confidence bars.

Set SV_MODEL=path/to/weights.pt to use weights other than models/ppe.pt.


Demo

Landing page: sentinelvision-sonubiswal.netlify.app

60-second webcam demo: coming soon (will be attached to the repo's Releases).


Why this is not "another YOLO PPE detector"

frame → YOLO → draw box → done        # what everyone else ships

The model is about 20% of the work. The other 80% is association, rules and operations, and the failure modes that come with skipping them:

Failure mode Why it happens What SentinelVision does Phase
Single-frame false positives One blurry frame flags a violation N-of-M temporal voting per track 2 (done)
Violation with no owner NO-Hardhat detected, but which worker? Person attribution via IoU / containment + track ID 2 (done)
One-size-fits-all rules A welding bay is not a loading dock Zone policies with per-zone PPE requirements 3
Alert fatigue 200 alerts a shift, all ignored Per-worker cooldown and severity tiers 3–4
No audit trail "Prove it happened" Hash-chained event log in SQLite / Postgres 4
Demo-only Runs on a laptop, dies on a Jetson ONNX / TensorRT export and Docker 7

Tip

The demo app already includes a hash-chained JSONL log so you can see the idea working. The production audit store (SQLite, snapshot archive, retention) lands in Phase 4.


How the pipeline works

flowchart LR
    CAM["Capture<br/>webcam · RTSP · file"] --> DET["Detect<br/>YOLOv8, per-class bars"]
    DET --> TRK["Track<br/>ByteTrack IDs"]
    TRK --> ASC["Associate<br/>PPE → worker"]
    ASC --> VOTE["Vote<br/>6 of 10 frames"]
    VOTE --> EVT["Violation event<br/>track-bound"]
    EVT -.-> LATER["Zone engine · audit store<br/>alerts · dashboard"]

    classDef done fill:#12a56b,stroke:#0d1b2a,color:#fff
    classDef later fill:#1a1a2e,stroke:#666,color:#ddd,stroke-dasharray: 4 3
    class CAM,DET,TRK,ASC,VOTE,EVT done
    class LATER later
Loading

Detection is deliberately permissive. Commitment is deliberately conservative: nothing becomes an event until the pipeline is sure it is looking at a person and not a flicker.

Step Stage What happens
1 Detect YOLOv8 finds persons and PPE items. Each class is gated by its own confidence bar.
2 Track ByteTrack gives each person a stable ID, so findings belong to a worker rather than a pixel box.
3 Associate PPE binds to its worker by geometric containment, falling back to IoU. Overlaps are resolved deterministically (containment, then overlap share, then the smaller person box).
4 Vote A violation must appear in at least 6 of the last 10 frames. One bad frame can never raise an event.
5 Audit Committed events carry a SHA-256 digest over their contents and the previous digest.
Example event (click to expand)
{
  "ts": "2026-10-13T09:41:07.221Z",
  "camera": "cam-04",
  "zone": "gate-b",
  "track_id": 17,
  "findings": [
    { "class": "Hardhat", "conf": 0.90, "verdict": "present" },
    { "class": "NO-Safety Vest", "conf": 0.81, "verdict": "violation", "votes": 6, "window": 10 }
  ],
  "policy": "gate-b: hardhat+safety-vest required",
  "prev_sha256": "b71e…",
  "sha256": "9f3c…a41d"
}

The class taxonomy

CLASS_THRESHOLDS = {
    "person":          0.35,
    "Hardhat":         0.50,
    "NO-Hardhat":      0.65,   # violations need more evidence
    "Safety Vest":     0.50,
    "NO-Safety Vest":  0.65,   # note the space
    "Mask":            0.50,
    "NO-Mask":         0.65,
    "Safety Cone":     0.55,
    "machinery":       0.55,
    "vehicle":         0.55,
}

A missed worker is cheap. A false violation erodes trust. Hence the split: recall-tolerant bars for people, precision-strict bars for violation classes. Combined with the 6-of-10 quorum, raising an alarm always demands more evidence than recording compliance.

Two foundations, deliberately hardened

1. Name normalization. The dataset emits NO-Safety Vest, with an uppercase prefix and a space inside the name, while Safety Vest is a compliant class. Naive startswith("NO") or split logic breaks on that space and silently turns a violation into compliance.

normalize_class_name("NO-Safety Vest")        # -> "no-safety-vest"
normalize_class_name("no_safety_vest")        # -> "no-safety-vest"
normalize_class_name("  NO  Safety Vest  ")   # -> "no-safety-vest"

The violation set is derived from the taxonomy, never hard-coded, so a future NO-Gloves is caught automatically:

VIOLATION_CLASSES = tuple(sorted(s for s in NORMALIZED_CLASSES if s.startswith("no-")))

2. BGR correctness. OpenCV is BGR, not RGB. The palette is defined once as explicit BGR tuples and asserted channel by channel in the tests.

GREEN_BGR = (0, 255, 0)   # green-dominant: index 1 is 255
RED_BGR   = (0, 0, 255)   # red-dominant:   index 2 is 255

Labels for boxes that hug the top edge flip below the box (y1 < 30), so a worker at the top of the frame still gets a readable label.


Quickstart (CLI)

# dev install (tests + lint only: no torch, no weights, no camera)
pip install -e ".[dev]"
pytest
ruff check .

# with the vision runtime
pip install -e ".[dev,vision]"
python -m sentinelvision --list-classes
python -m sentinelvision --model models/ppe.pt --source 0 --show            # webcam preview
python -m sentinelvision --model models/ppe.pt --source clip.mp4 \
    --events-jsonl events.jsonl --stats                                      # headless batch run
python -m sentinelvision --source clip.mp4 --save out.mp4 --max-frames 500

Windows: run ./setup.ps1 (add -Vision to install ultralytics).

Events stream to the JSONL log while the run is live, and the stats block prints on exit. The log is append-only: read the tail, never edit it.

Sample output of --list-classes
$ python -m sentinelvision --list-classes
person          0.35
Hardhat         0.50
NO-Hardhat      0.65
Safety Vest     0.50
NO-Safety Vest  0.65
Mask            0.50
NO-Mask         0.65
Safety Cone     0.55
machinery       0.55
vehicle         0.55

10 classes, 3 violations: no-hardhat, no-mask, no-safety-vest

Repository layout

Expand the project tree
sentinelvision/
├── app.py                      # Gradio demo (image · video vote · audit chain)
├── pyproject.toml              # uv / pip installable, ruff + pytest config
├── src/sentinelvision/
│   ├── __main__.py             # python -m sentinelvision
│   ├── cli.py                  # argparse entry point
│   ├── config.py               # 10-class taxonomy + class-aware thresholds
│   ├── classes.py              # normalize_class_name / is_violation / is_compliant
│   ├── detector.py             # YOLO wrapper, runtime names, per-class filtering
│   ├── visualize.py            # BGR-correct, top-edge-safe labels
│   ├── logging_setup.py        # structlog
│   ├── tracking.py             # [2] box geometry, IoU, track-ID plumbing
│   ├── association.py          # [2] person ↔ PPE binding, 3-state slots
│   ├── worker.py               # [2] WorkerState + WorkerRegistry
│   ├── voting.py               # [2] N-of-M temporal voting + hysteresis
│   ├── events.py               # [2] ViolationEvent, content-addressed IDs, EventBus
│   └── pipeline.py             # [2] FramePipeline + Policy seam
├── tests/
│   ├── fakes.py                # fake ultralytics surface (no torch, no weights)
│   └── test_*.py               # 289 tests, 96% coverage, all weight-free
├── benchmarks/evaluate_video.py  # [8] FP-rate measurement (numbers TBD)
├── scripts/detect_webcam.py    # thin wrapper
├── docs/architecture.md        # Mermaid sequence diagram
├── setup.ps1                   # Windows bootstrap
└── README.md

Everything is testable with zero heavy dependencies: ultralytics is imported lazily inside Detector.load(), and the tests inject a fake model exposing the same names / predict() surface.


Design decisions

Layer Choice Why not the obvious pick
Inference ONNX Runtime primary, TensorRT optional Not ultralytics.predict() in production
Tracker ByteTrack SORT degrades badly on occlusion
Backend FastAPI + Pydantic v2 + async (Phase 5) Typed, async, easy to test
Realtime WebSocket + Redis Streams (Phase 4–5) Event-driven, not polling
Database SQLite → Postgres + TimescaleDB (Phase 4) Single-site → multi-site migration path
Frontend Next.js 14 + shadcn/ui + Recharts (Phase 5) A real dashboard, not a notebook
Config Pydantic Settings Typed and env-overridable via SENTINEL_*
Testing pytest + pytest-cov + hypothesis Property-based tests for the rule engine
CI GitHub Actions: lint, test, ONNX export smoke test Every change is checked
Packaging uv, falling back to pip Fast installs, no lock-in

Roadmap

Phase Deliverable Time Status
1 Tested detector, taxonomy, CLI, BGR-correct visualization 1 day Done
2 ByteTrack IDs, person attribution, N-of-M voting, ViolationEvent 3 days Done
3 Zone engine: polygon geofences + per-zone PPE policy 2 days Planned
4 Audit store: hash-chained SQLite log, snapshot archive 2 days Planned
5 FastAPI + WebSocket dashboard 3 days Planned
6 Auto-generated shift reports (PDF) + analytics 2 days Planned
7 ONNX / TensorRT export, Docker Compose, Jetson profile 2 days Planned
8 Benchmark on SH17 / Pictor-v3, publish numbers, open weights 3 days Planned

Still to ship for a credible public release

  • Benchmark table
  • 60-second real-webcam demo GIF
  • Hosted demo
  • Reproducible train.py with dataset YAML and Dockerfile
  • Short method and ablation write-up in docs/
  • Open weights on the Hugging Face Hub
  • Honest ablation against a baseline YOLOv8n PPE detector

Links

Landing page sentinelvision-sonubiswal.netlify.app
Source code github.com/sonubiswal/sentinelvision
Demo video Coming soon: will be attached to the repo's Releases
Model weights wesleymqsss/yolov8m-construction-site-safety
License Apache-2.0

License

Released under the Apache-2.0 license.


If SentinelVision is useful to you, a star on the repo helps a lot.

Live site · GitHub · Weights

Releases

Packages

Contributors

Languages