YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Unified Multi-Class Threat Detection
A personal computer-vision project: training a single real-time object detector that covers 21 classes relevant to security monitoring β weapons (short-gun, long-gun, knife), fire and smoke, phones, and the surrounding everyday COCO objects (person, car, bag, umbrella, β¦) that a surveillance model has to reason about at the same time.
The interesting part of the project was never the training command β it was getting five mismatched public datasets into a state where a model could actually learn from them. Most of the effort, and most of this README, is about data engineering.
- Model: YOLO11s, fine-tuned from COCO-pretrained weights
- Classes: 20 target classes (indices 0β19) +
phone(20) - Released weights:
weights/best.pt(v3)
Results (held-out test set, 2,470 images / 6,780 instances, conf 0.7)
| Class | Recall | Precision |
|---|---|---|
| Short-gun | 0.81 | 0.96 |
| Long-gun | 0.71 | 0.97 |
| Phone | 0.80 | 0.98 |
| Knife | 0.58 | 0.90 |
| Fire | 0.58 | 0.96 |
| Smoke | 0.50 | 0.95 |
- Overall precision 0.91 at conf 0.7 β few false alarms, which is the property that keeps a human operator trusting the alerts.
- All 21 classes register non-zero recall at the reporting threshold (no class is completely missed).
- Misses fall to background rather than being misclassified β the confusable pairs (short-gun/phone, long-gun/umbrella, knife/scissors) stay cleanly separated.
See evaluation/ for the training curves and the normalized confusion
matrix.
What was involved in the data work
Five datasets, 184,593 images, 974,388 label lines β parsed once into a single per-bounding-box table so the audit could be done with queries instead of re-reading files. That inspection surfaced five issues, each of which would have quietly degraded the model:
- Class remapping. The COCO export was ordered alphabetically, not in canonical
COCO order β
personis class 48, not 0. A hand-written remap would have silently mislabelled ~872k annotations. The mapping is instead generated from each dataset's owndata.yamlby name, and fails loudly if any class doesn't resolve. - Label completeness. The single-purpose datasets only labelled their own target, so real persons/cars in weapon and fire scenes were unlabelled β teaching the model to suppress the person class. Fixed by pseudo-labelling the non-COCO data with a strong pretrained detector (YOLO11x), with a name-based remap and IoU dedup as guards. ~36k boxes added.
- Train/val leakage. Roboflow augmentation had scattered near-identical copies of the same source photo across both splits β inflating validation metrics by measuring memorization. Content hashing misses this (augmented variants are byte-distinct). Fixed by recovering each image's source ID from its filename and enforcing source-level split assignment; 5,738 images reassigned, zero split-spanning after.
- Class imbalance. Measured at the box level (which is what the model learns from), the person-to-short-gun ratio was 41:1, not the 8:1 the image counts suggest. Addressed with tiered subsampling of COCO β keep every image with a scarce class, a confuser, or a knife/phone; random-fill the rest β bringing the ratio to ~20:1 while preserving the weapon data intact.
- Format inconsistencies. Segmentation polygons and degenerate boxes mixed into the detection labels, plus unlabelled stray images.
Training notes
- YOLO11s at 640px β a deliberate choice, not a default. Input resolution was set from the box statistics (safety-critical objects are staged close-ups, 12β25% of frame), and a small fast model is closer to what a multi-stream deployment would actually run than a leaderboard-optimized one.
- Final run on an A100: 45 epochs, batch 80, AdamW (lr0 1e-3, cosine), AMP.
- Augmentation: mosaic (closed for the last 10 epochs), mixup 0.15, copy-paste 0.3, mild realistic geometry (rotation 5Β°, no vertical flip / perspective β surveillance objects are upright), plus degradation augmentation (blur, downscale, JPEG artefacts, contrast) to narrow the gap to compressed CCTV footage.
- Iteration v1 β v3: v1 left backpack/handbag at 0.00 recall at conf 0.7 (hidden by aggregate mAP); v2 was a clean negative result on aggressive augmentation; v3 boosted weak-class sampling and added degradation aug, bringing every class off zero and raising precision 0.85 β 0.90. v3 is the released model.
Known weaknesses
- Confuser coverage is partial β broom, wallet, pen, fog, steam don't appear in any source dataset, so those false-positive pairs can't be quantified yet.
- Small-object recall at 0.7 β backpack/handbag/boat clear "not completely missed" but stay low; recall rises substantially at lower thresholds. Object-scale, not a training defect.
- Domain gap β training images are clean staged close-ups. On low-res, motion-blurred
footage, weapon detection at 0.7 is intermittent frame-to-frame. Raising inference to
imgsz=1280recovers it, which confirms the issue is scale, not capability. - Pseudo-label ceiling β distant / low-contrast persons are still sometimes missed.
Recommended for real deployment: per-class thresholds, imgsz=1280 in high-sensitivity
zones, and temporal aggregation (alert on detections across several frames).
The model was also spot-checked on unseen real-world-style clips (motion blur,
compression, wide-angle CCTV). On clear footage weapons and people are detected
reliably at 0.7; on degraded footage confidence fluctuates across the threshold until
inference resolution is raised to imgsz=1280 β confirming the issue is object scale,
not capability.
Repository layout
weights/best.pt released YOLO11s weights (v3) [Git LFS]
evaluation/ training curves + confusion matrix
weights/best.pt is stored via Git LFS β run git lfs install before cloning.
Datasets are not tracked.