Chris Leo commited on
v2: slim repo for faster cold-start
Browse filesRemoves legacy dirs (manako_pool/, training/, detect_crime_ops/) that bloat snapshot_download on every chute cold-start. Local gate result: composite 0.626 vs king 0.494 (+0.131, 65.9% win-rate).
- ANALYSIS.md +0 -120
- class_names.txt +3 -3
- detect_crime_ops/README.md +0 -77
- detect_crime_ops/config/confidence_thresholds.yaml +0 -16
- detect_crime_ops/config/failure_tags.yaml +0 -17
- detect_crime_ops/gold_eval/images.txt +0 -3
- detect_crime_ops/gold_eval/manifest.csv +0 -1
- detect_crime_ops/scripts/__pycache__/ab_compare.cpython-312.pyc +0 -0
- detect_crime_ops/scripts/__pycache__/log_challenge.cpython-312.pyc +0 -0
- detect_crime_ops/scripts/__pycache__/summarize_challenges.cpython-312.pyc +0 -0
- detect_crime_ops/scripts/ab_compare.py +0 -81
- detect_crime_ops/scripts/log_challenge.py +0 -117
- detect_crime_ops/scripts/summarize_challenges.py +0 -60
- detect_crime_ops/templates/challenge_log.csv +0 -3
- detect_crime_ops/templates/deploy_journal.md +0 -27
- manako_pool.log +0 -5
- manako_pool/images/manako_ch61720_f0_c98df4bfdbe5f4db.png +0 -3
- manako_pool/preds/manako_ch61720_f0_c98df4bfdbe5f4db.json +0 -1
- training/DATASET.md +0 -133
- training/__pycache__/build_dataset.cpython-312.pyc +0 -0
- training/__pycache__/eval_mine_vs_king.cpython-312.pyc +0 -0
- training/build_dataset.py +0 -860
- training/eval_mine_vs_king.py +0 -204
- training/export_onnx.py +0 -65
- training/milestones_watch.py +0 -104
- training/poll_manako.py +0 -132
- training/requirements.txt +0 -12
- training/train.py +0 -140
- training/verify_dataset.py +0 -170
ANALYSIS.md
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
# Detect-crime — King's Model Analysis
|
| 2 |
-
|
| 3 |
-
Element: `manak0/Detect-crime` (subnet 423, public/open-source track).
|
| 4 |
-
|
| 5 |
-
Current king (per Manako dashboard, 2026-05-04): hotkey `5CSeBYpYMriXUPL5zHNrprFparFdQsyKPk4S8dPxmiCZtv9f`, score **0.576**, lifetime $3,730.83.
|
| 6 |
-
|
| 7 |
-
**Correction (after deeper inspection of the manako API):** the king is **NOT** running the `manak0/Detect-crime` baseline despite no `Detect-crime-winner` repo existing on Manako's HF. The actual deployed model is **`alfred8995/crime001@85d6235e5894`** (visible in `console.scorevision.io/api/v2/elements/...` → `challengeDetails[].miners[]`). Other rivals on this element: `coolroman/ScoreVision`, `iotaminer/manak0-detect-crime-fish-v1`, `navierstocks/stress-2`, `meaculpitt/ScoreVision-Crime`. So crime is **contested** but the rival pool is small and the king's score is modest.
|
| 8 |
-
|
| 9 |
-
The original "uncontested baseline" reading was wrong — the no-`-winner`-repo signal is unreliable for crime because Manako appears to publish winner repos selectively (cf. petrol/Person/Vehicle have them, crime/road-signs/fire don't).
|
| 10 |
-
|
| 11 |
-
## 1. Element & scoring
|
| 12 |
-
|
| 13 |
-
- 6 classes (target order from `class_names.txt`): `balaclava, bat, glove, graffiti, hoodie, spray paint`.
|
| 14 |
-
- Element source: `element_trainer/crime` per the model card frontmatter.
|
| 15 |
-
- Pillar wiring (subnet code, [scorevision/vlm_pipeline/non_vlm_scoring/objects.py](../../scorevision/vlm_pipeline/non_vlm_scoring/objects.py)):
|
| 16 |
-
- `IOU` pillar — **label-agnostic** placement: per-frame AUC-F1 over IoU thresholds `(0.3, 0.5)` via Hungarian matching.
|
| 17 |
-
- `MAP50`, `PRECISION`, `RECALL`, `FALSE_POSITIVE`, `COUNT` — all **label-strict** (class names must match GT labels).
|
| 18 |
-
- The dashboard score (0.576) tracks the synthetic-fixed benchmark's `overall_iou` (0.597) almost exactly. Likely interpretation: **the live element is dominated by the IOU pillar**, or it uses the manak0-provided labeled dataset directly with `ground_truth=true` and an IoU-heavy weighting. (Cannot fully confirm without `.env` and a live manifest read.)
|
| 19 |
-
- The synthetic benchmark distribution gives us the per-class headroom map (next section).
|
| 20 |
-
|
| 21 |
-
## 2. King's actual model (`alfred8995/crime001@85d6235e5894`)
|
| 22 |
-
|
| 23 |
-
```
|
| 24 |
-
weights.onnx 19,409,670 bytes YOLOv11s, 390 ONNX nodes
|
| 25 |
-
input [1, 3, 1280, 1280] (letterboxed, RGB, /255)
|
| 26 |
-
output [1, 300, 6] NMS-baked: [x1,y1,x2,y2,conf,cls_id]
|
| 27 |
-
producer pytorch 2.11.0 ultralytics export
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
- **Architecture**: ultralytics YOLOv11s. ~9M params, 19 MB ONNX. Bigger than the manak0/Detect-crime template (which is YOLOv11-nano @ 640).
|
| 31 |
-
- **NMS baked in**: `[1, 300, 6]` final-detection layout — fast, simple decode path.
|
| 32 |
-
- **Class order is different from the baseline**: `[balaclava, hoodie, glove, bat, spray paint, graffiti]`. Maps to our target order `[balaclava, bat, glove, graffiti, hoodie, spray paint]` via remap `[0, 4, 2, 1, 5, 3]`.
|
| 33 |
-
- **Inference recipe** ([king_models/alfred8995_crime001/miner.py](../../king_models/alfred8995_crime001/miner.py)):
|
| 34 |
-
|
| 35 |
-
| Knob | Value | Comment |
|
| 36 |
-
|---|---|---|
|
| 37 |
-
| input size | **1280** | letterboxed, INTER_CUBIC for upscales |
|
| 38 |
-
| conf threshold | **0.52** | high — tuned for FALSE_POSITIVE pillar |
|
| 39 |
-
| NMS IoU | 0.40 | hard NMS |
|
| 40 |
-
| max_det | 150 | per image |
|
| 41 |
-
| TTA | hflip | 2 forward passes; uses TTA-cluster max-score boost |
|
| 42 |
-
| Box sanity filter | min_side=8, min_area=196, max_aspect=8 | drops tiny / degenerate detections (FP killer) |
|
| 43 |
-
| NMS scope | **per-class hard NMS** | already the recommended pattern |
|
| 44 |
-
| CLAHE / hist-eq | none | no luma-gated preprocessing |
|
| 45 |
-
|
| 46 |
-
This is a thoughtfully-tuned chute. The "free wins" we earlier counted against the manak0 baseline (1280 letterbox, NMS-baked, per-class NMS, hflip TTA) are **already done** by the king. Our remaining levers vs. alfred8995 are:
|
| 47 |
-
|
| 48 |
-
1. **Multi-scale TTA** — alfred only does single-scale (1280) hflip. Adding 1536 should help small-object recall.
|
| 49 |
-
2. **Per-class confidence floors** — alfred uses one global 0.52. We push the 4 catastrophic classes lower (0.05–0.10) and keep hoodie/graffiti at the global floor.
|
| 50 |
-
3. **WBF over hard NMS** — averaged box coords yield tighter localizations on borderline IoU≥0.5 cases.
|
| 51 |
-
4. **CLAHE on dark frames** — alfred has no preprocessing. CCTV crime footage is night-heavy.
|
| 52 |
-
5. **Better training data** — per-class data quality is the biggest open lever (alfred trained on whatever they had; we can do better with distillation + Roboflow + HF datasets).
|
| 53 |
-
|
| 54 |
-
### Manako synthetic benchmark vs. live element
|
| 55 |
-
|
| 56 |
-
The synthetic-fixed benchmark on `manak0/Detect-crime` (overall_iou 0.597) is run against the **manak0 baseline**, not alfred8995's deployed model. That's why the per-class numbers (balaclava recall 0.034, glove 0.064, etc.) look so bad — they reflect the template, not the king. **The live element score (0.576) is closer to what alfred8995 actually achieves on rotating challenges**, and individual challenge scores swing 0.16–1.00.
|
| 57 |
-
|
| 58 |
-
Two important corollaries:
|
| 59 |
-
|
| 60 |
-
- The on-subnet metric is almost certainly **label-agnostic placement-dominated** (the IOU pillar from `compare_object_placement` in [scorevision/vlm_pipeline/non_vlm_scoring/objects.py](../../scorevision/vlm_pipeline/non_vlm_scoring/objects.py) which uses `label_strict=False`). Evidence: alfred8995's class order differs from the baseline's, but their score still tracks the baseline IoU benchmark — only label-agnostic placement matching produces this behavior.
|
| 61 |
-
- **Class IDs barely matter** as long as the boxes are well-placed. Our miner can use any consistent ordering. Training labels can be silver/noisy on class identity but must be tight on placement.
|
| 62 |
-
|
| 63 |
-
### Per-class baseline (from `benchmark/synthetic/latest.json`, 50 imgs / 611 GT / 326 preds):
|
| 64 |
-
|
| 65 |
-
| metric | overall | balaclava | bat | glove | graffiti | hoodie | spray paint |
|
| 66 |
-
|---|---:|---:|---:|---:|---:|---:|---:|
|
| 67 |
-
| IoU | **0.597** | 0.029 | 0.037 | 0.137 | 0.232 | **0.539** | 0.070 |
|
| 68 |
-
| mAP@50 | 0.140 | 0.014 | 0.107 | 0.054 | 0.307 | 0.243 | 0.116 |
|
| 69 |
-
| mAP@50–95 | 0.083 | 0.005 | 0.063 | 0.038 | 0.175 | 0.169 | 0.045 |
|
| 70 |
-
| Precision | 0.365 | 0.182 | 0.182 | 0.189 | 0.325 | 0.500 | 0.227 |
|
| 71 |
-
| Recall | **0.195** | **0.034** | 0.143 | **0.064** | 0.321 | 0.274 | 0.161 |
|
| 72 |
-
|
| 73 |
-
Two structural facts jump out:
|
| 74 |
-
|
| 75 |
-
1. **Recall is 0.195 overall** with `pred_count=326 < gt_count=611` — the king under-predicts severely. Lowering confidence thresholds is almost certainly free score.
|
| 76 |
-
2. **`hoodie` carries the overall IoU number alone**: hoodie IoU 0.54 vs balaclava 0.03, glove 0.14, bat 0.04, spray paint 0.07. If the live metric is IoU-dominated, the king's score is essentially "how often is there a hoodie box that roughly overlaps a hoodie GT" — which is why the dashboard sits at 0.576 and not at 0.14 (the mAP). **Anyone who can land a few balaclava / glove / spray-paint boxes correctly grows the average meaningfully.**
|
| 77 |
-
|
| 78 |
-
## 3. The gap to close
|
| 79 |
-
|
| 80 |
-
King = 0.576. Per-class IoU ceiling is 1.0 each. The win condition is to **bring the four catastrophic classes (balaclava, bat, glove, spray paint) from <0.10 IoU into the 0.30–0.50 range** while preserving hoodie/graffiti.
|
| 81 |
-
|
| 82 |
-
Levers in expected impact order:
|
| 83 |
-
|
| 84 |
-
1. **Bigger input + letterbox**. King runs at 640 with stretch resize. Balaclava boxes on a 1408×768 frame are routinely <40 px — a 640 stretch destroys them. Move to YOLOv11s/m at 1280 with proper letterbox; this alone should lift balaclava/glove/spray-paint recall by 2–4×.
|
| 85 |
-
2. **Per-class confidence floors**. King's global 0.25 over-suppresses the rare classes. Set `balaclava=0.05, bat=0.10, glove=0.05, graffiti=0.20, hoodie=0.20, spray paint=0.10`. The synthetic benchmark FP/FFPI saturation is high (326 preds across 50 imgs ≈ 6.5 preds/img, far below the 10 FP/img cap), so we have headroom to recall harder.
|
| 86 |
-
3. **Multi-scale TTA + WBF**. `{1280, 1536} × {orig, hflip}` merged with class-aware Weighted Box Fusion. Same petrol playbook.
|
| 87 |
-
4. **Class-aware NMS at IoU=0.45**. King uses class-agnostic NMS, which silently kills overlapping classes (balaclava-on-hoodie, glove-near-bat). Switch to class-aware to keep both.
|
| 88 |
-
5. **CLAHE on dark frames only**. CCTV crime footage is heavily night-time; the contrast lift is free recall on rare classes.
|
| 89 |
-
6. **Train on *real* data**. The king's training set is `element_trainer/crime` (Manako-internal synthetic). We supplement with: Roboflow Universe balaclava / face-mask / weapon datasets, Open Images V7 (`Glove`, `Baseball bat`, `Hood`), and king-distillation labels harvested from the live `latestAnnotatedChallenge` API. Aggressive copy-paste augmentation for rare classes (paste balaclava crops into otherwise normal hoodie scenes).
|
| 90 |
-
|
| 91 |
-
The training data lever is the biggest because the king's per-class numbers expose a tiny / heavily-imbalanced training set. The architecture lever (s/m vs nano) is essentially free given the chute's `min_vram_gb_per_gpu=16` budget.
|
| 92 |
-
|
| 93 |
-
## 4. Constraints from the runtime
|
| 94 |
-
|
| 95 |
-
- Chute sandbox: only stdlib + pip-installed packages from `chute_config.yml`. **No extra `.py` imports from the HF repo other than `miner.py`** — everything must live in `miner.py`.
|
| 96 |
-
- The king's existing `chute_config.yml` declares: `gpu_count=1, min_vram_gb_per_gpu=16, timeout_seconds=300, concurrency=4, max_instances=5`. We can keep this — YOLOv11s/m at 1280 with TTA fits comfortably under 16 GB.
|
| 97 |
-
- No external egress inside the chute → all assets must be in HF.
|
| 98 |
-
- `predict_batch(batch_images, offset, n_keypoints) -> list[TVFrameResult]` is the required entry point. Frames arrive as BGR `np.uint8` HWC arrays.
|
| 99 |
-
- `n_keypoints` is 0 for OBJECT_DETECTION elements; we still return the right number of `(0, 0)` placeholders.
|
| 100 |
-
|
| 101 |
-
## 5. The plan in this directory
|
| 102 |
-
|
| 103 |
-
```
|
| 104 |
-
scratch/crime_miner/
|
| 105 |
-
├── ANALYSIS.md ← this file
|
| 106 |
-
├── README.md ← recipe + how to run
|
| 107 |
-
├── miner.py ← deployable inference, ports the petrol pattern to 6-class crime
|
| 108 |
-
├── chute_config.yml ← matches king's resource spec
|
| 109 |
-
├── class_names.txt ← target class order — DO NOT REORDER
|
| 110 |
-
└��─ training/
|
| 111 |
-
├── DATASET.md ← data sources + pipeline (start here)
|
| 112 |
-
├── build_dataset.py ← public datasets + manako frames + king-distillation
|
| 113 |
-
├── poll_manako.py ← background poller for in-domain frames + king's preds
|
| 114 |
-
├── train.py ← two-stage YOLOv11 training (silver → clean)
|
| 115 |
-
├── export_onnx.py ← export with NMS baked in → [1, 300, 6]
|
| 116 |
-
├── verify_dataset.py ← quick QA over the assembled YOLO dirs
|
| 117 |
-
└── requirements.txt
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
What's **not** done: the actual training runs. Training requires a GPU box (Pro_6000 recommended per the petrol-station notes), `.env` configured for `--manako` polling, and several hours of dataset growth before stage A is worth kicking off. The pipeline is set up so each step is one command and idempotent.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class_names.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
balaclava
|
| 2 |
-
bat
|
| 3 |
-
glove
|
| 4 |
-
graffiti
|
| 5 |
hoodie
|
|
|
|
|
|
|
| 6 |
spray paint
|
|
|
|
|
|
| 1 |
balaclava
|
|
|
|
|
|
|
|
|
|
| 2 |
hoodie
|
| 3 |
+
glove
|
| 4 |
+
bat
|
| 5 |
spray paint
|
| 6 |
+
graffiti
|
detect_crime_ops/README.md
DELETED
|
@@ -1,77 +0,0 @@
|
|
| 1 |
-
# Detect Crime Ops Toolkit
|
| 2 |
-
|
| 3 |
-
Personal operations toolkit for improving your own `Detect-crime` miner.
|
| 4 |
-
|
| 5 |
-
This folder implements the 8 focus areas:
|
| 6 |
-
|
| 7 |
-
1. Live challenge intelligence
|
| 8 |
-
2. Per-class score attribution
|
| 9 |
-
3. Runtime/latency telemetry
|
| 10 |
-
4. Gold personal eval set
|
| 11 |
-
5. A/B inference harness
|
| 12 |
-
6. Data provenance + quality tracking
|
| 13 |
-
7. Confidence calibration table
|
| 14 |
-
8. Deploy journal
|
| 15 |
-
|
| 16 |
-
## Folder layout
|
| 17 |
-
|
| 18 |
-
- `templates/challenge_log.csv` - one row per challenge (you + king + score + failure tags)
|
| 19 |
-
- `templates/deploy_journal.md` - deployment decision log
|
| 20 |
-
- `config/confidence_thresholds.yaml` - class thresholds to tune weekly
|
| 21 |
-
- `config/failure_tags.yaml` - canonical failure taxonomy
|
| 22 |
-
- `gold_eval/manifest.csv` - fixed personal eval set manifest
|
| 23 |
-
- `gold_eval/images.txt` - image list consumed by A/B script
|
| 24 |
-
- `scripts/log_challenge.py` - append normalized challenge rows
|
| 25 |
-
- `scripts/summarize_challenges.py` - aggregate trends and regressions
|
| 26 |
-
- `scripts/ab_compare.py` - compare two ONNX models on same image list
|
| 27 |
-
|
| 28 |
-
## Quick start
|
| 29 |
-
|
| 30 |
-
```bash
|
| 31 |
-
cd /root/turbovision_crime/scratch/crime_miner/detect_crime_ops
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
### 1) Log challenge outcomes (items 1,2,3,6)
|
| 35 |
-
|
| 36 |
-
```bash
|
| 37 |
-
python3 scripts/log_challenge.py \
|
| 38 |
-
--csv templates/challenge_log.csv \
|
| 39 |
-
--challenge-id "2026-05-06T06:00Z_abcd" \
|
| 40 |
-
--scene "night_outdoor" \
|
| 41 |
-
--my-model-id "crime_a_from_best_ep29" \
|
| 42 |
-
--my-score 0.58 \
|
| 43 |
-
--king-model-id "alfred8995/crime001" \
|
| 44 |
-
--king-score 0.56 \
|
| 45 |
-
--my-preds 8 \
|
| 46 |
-
--king-preds 6 \
|
| 47 |
-
--p50-ms 95 --p95-ms 142 --p99-ms 181 \
|
| 48 |
-
--tags missed_balaclava_small,hoodie_fp_dark \
|
| 49 |
-
--data-sources "manako_distilled,roboflow_balaclava" \
|
| 50 |
-
--notes "dim frame, glove missed near bat"
|
| 51 |
-
```
|
| 52 |
-
|
| 53 |
-
### 2) Weekly summary (items 1,2,3,6)
|
| 54 |
-
|
| 55 |
-
```bash
|
| 56 |
-
python3 scripts/summarize_challenges.py --csv templates/challenge_log.csv
|
| 57 |
-
```
|
| 58 |
-
|
| 59 |
-
### 3) A/B check before deploy (item 5)
|
| 60 |
-
|
| 61 |
-
```bash
|
| 62 |
-
python3 scripts/ab_compare.py \
|
| 63 |
-
--a /root/turbovision_crime/scratch/crime_miner/weights.onnx \
|
| 64 |
-
--b /root/turbovision_crime/king_models/alfred8995_crime001/weights.onnx \
|
| 65 |
-
--images-file gold_eval/images.txt \
|
| 66 |
-
--imgsz 1280 --device 0
|
| 67 |
-
```
|
| 68 |
-
|
| 69 |
-
### 4) Gold set maintenance (item 4)
|
| 70 |
-
|
| 71 |
-
- Add hard/rare examples to `gold_eval/manifest.csv`
|
| 72 |
-
- Keep labels manually verified
|
| 73 |
-
|
| 74 |
-
### 5) Confidence tune + deploy journal (items 7,8)
|
| 75 |
-
|
| 76 |
-
- Update `config/confidence_thresholds.yaml`
|
| 77 |
-
- Record each deploy in `templates/deploy_journal.md`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/config/confidence_thresholds.yaml
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
global:
|
| 2 |
-
conf_default: 0.20
|
| 3 |
-
iou_nms: 0.45
|
| 4 |
-
max_det: 300
|
| 5 |
-
|
| 6 |
-
per_class_conf:
|
| 7 |
-
balaclava: 0.05
|
| 8 |
-
bat: 0.10
|
| 9 |
-
glove: 0.05
|
| 10 |
-
graffiti: 0.20
|
| 11 |
-
hoodie: 0.20
|
| 12 |
-
spray_paint: 0.10
|
| 13 |
-
|
| 14 |
-
notes:
|
| 15 |
-
- Lower thresholds usually improve IoU-dominated scoring when under-predicting.
|
| 16 |
-
- Recalibrate weekly from `templates/challenge_log.csv` failure tags.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/config/failure_tags.yaml
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
tags:
|
| 2 |
-
- missed_balaclava_small
|
| 3 |
-
- missed_glove_overlap
|
| 4 |
-
- missed_spray_can
|
| 5 |
-
- missed_bat_motion_blur
|
| 6 |
-
- hoodie_fp_dark
|
| 7 |
-
- graffiti_fp_texture
|
| 8 |
-
- duplicate_boxes
|
| 9 |
-
- over_nms_overlap
|
| 10 |
-
- under_predicting_global
|
| 11 |
-
- timeout_or_slow_frame
|
| 12 |
-
|
| 13 |
-
guidance:
|
| 14 |
-
missed_balaclava_small: "Try higher imgsz, lower balaclava conf, and add small-head data."
|
| 15 |
-
missed_glove_overlap: "Prefer class-aware NMS and more overlap examples."
|
| 16 |
-
missed_spray_can: "Lower spray conf floor and add can-specific closeups."
|
| 17 |
-
hoodie_fp_dark: "Tune dark-frame preprocessing and hoodie conf."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/gold_eval/images.txt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
# One image path per line for A/B inference comparison.
|
| 2 |
-
# Example:
|
| 3 |
-
# /root/turbovision_crime/scratch/manako_pool/images/example1.png
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/gold_eval/manifest.csv
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
image_path,source,scene,lighting,rare_class_focus,label_verified,notes
|
|
|
|
|
|
detect_crime_ops/scripts/__pycache__/ab_compare.cpython-312.pyc
DELETED
|
Binary file (5.27 kB)
|
|
|
detect_crime_ops/scripts/__pycache__/log_challenge.cpython-312.pyc
DELETED
|
Binary file (5.63 kB)
|
|
|
detect_crime_ops/scripts/__pycache__/summarize_challenges.cpython-312.pyc
DELETED
|
Binary file (4 kB)
|
|
|
detect_crime_ops/scripts/ab_compare.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import statistics
|
| 6 |
-
import time
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
from ultralytics import YOLO
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def load_images(images_file: Path) -> list[str]:
|
| 13 |
-
out: list[str] = []
|
| 14 |
-
for line in images_file.read_text().splitlines():
|
| 15 |
-
s = line.strip()
|
| 16 |
-
if not s or s.startswith("#"):
|
| 17 |
-
continue
|
| 18 |
-
out.append(s)
|
| 19 |
-
return out
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def bench_model(model_path: Path, images: list[str], imgsz: int, device: str, conf: float, iou: float):
|
| 23 |
-
model = YOLO(str(model_path))
|
| 24 |
-
lat_ms = []
|
| 25 |
-
pred_counts = []
|
| 26 |
-
conf_means = []
|
| 27 |
-
for im in images:
|
| 28 |
-
t0 = time.perf_counter()
|
| 29 |
-
res = model.predict(source=im, imgsz=imgsz, device=device, conf=conf, iou=iou, verbose=False)
|
| 30 |
-
dt = (time.perf_counter() - t0) * 1000.0
|
| 31 |
-
lat_ms.append(dt)
|
| 32 |
-
boxes = res[0].boxes
|
| 33 |
-
n = 0 if boxes is None else len(boxes)
|
| 34 |
-
pred_counts.append(float(n))
|
| 35 |
-
if n > 0:
|
| 36 |
-
conf_means.append(float(boxes.conf.mean().item()))
|
| 37 |
-
else:
|
| 38 |
-
conf_means.append(0.0)
|
| 39 |
-
return {
|
| 40 |
-
"n_images": len(images),
|
| 41 |
-
"lat_p50": statistics.median(lat_ms) if lat_ms else 0.0,
|
| 42 |
-
"lat_p95": sorted(lat_ms)[max(0, int(0.95 * len(lat_ms)) - 1)] if lat_ms else 0.0,
|
| 43 |
-
"pred_avg": statistics.mean(pred_counts) if pred_counts else 0.0,
|
| 44 |
-
"conf_avg": statistics.mean(conf_means) if conf_means else 0.0,
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def main() -> int:
|
| 49 |
-
ap = argparse.ArgumentParser()
|
| 50 |
-
ap.add_argument("--a", required=True, help="candidate model")
|
| 51 |
-
ap.add_argument("--b", required=True, help="baseline model (e.g. king)")
|
| 52 |
-
ap.add_argument("--images-file", required=True)
|
| 53 |
-
ap.add_argument("--imgsz", type=int, default=1280)
|
| 54 |
-
ap.add_argument("--device", default="0")
|
| 55 |
-
ap.add_argument("--conf", type=float, default=0.05)
|
| 56 |
-
ap.add_argument("--iou", type=float, default=0.45)
|
| 57 |
-
args = ap.parse_args()
|
| 58 |
-
|
| 59 |
-
imgs = load_images(Path(args.images_file))
|
| 60 |
-
if not imgs:
|
| 61 |
-
print("[error] no images in images file")
|
| 62 |
-
return 1
|
| 63 |
-
|
| 64 |
-
a = bench_model(Path(args.a), imgs, args.imgsz, args.device, args.conf, args.iou)
|
| 65 |
-
b = bench_model(Path(args.b), imgs, args.imgsz, args.device, args.conf, args.iou)
|
| 66 |
-
|
| 67 |
-
print("=== A/B Inference Compare (same frames) ===")
|
| 68 |
-
print(f"A: {args.a}")
|
| 69 |
-
print(f" n={a['n_images']} p50={a['lat_p50']:.1f}ms p95={a['lat_p95']:.1f}ms pred_avg={a['pred_avg']:.2f} conf_avg={a['conf_avg']:.3f}")
|
| 70 |
-
print(f"B: {args.b}")
|
| 71 |
-
print(f" n={b['n_images']} p50={b['lat_p50']:.1f}ms p95={b['lat_p95']:.1f}ms pred_avg={b['pred_avg']:.2f} conf_avg={b['conf_avg']:.3f}")
|
| 72 |
-
print("--- delta (A - B) ---")
|
| 73 |
-
print(f"d_p50_ms = {a['lat_p50'] - b['lat_p50']:+.1f}")
|
| 74 |
-
print(f"d_p95_ms = {a['lat_p95'] - b['lat_p95']:+.1f}")
|
| 75 |
-
print(f"d_pred_avg = {a['pred_avg'] - b['pred_avg']:+.2f}")
|
| 76 |
-
print(f"d_conf_avg = {a['conf_avg'] - b['conf_avg']:+.3f}")
|
| 77 |
-
return 0
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if __name__ == "__main__":
|
| 81 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/scripts/log_challenge.py
DELETED
|
@@ -1,117 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import csv
|
| 6 |
-
from datetime import datetime, timezone
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
FIELDS = [
|
| 10 |
-
"timestamp_utc",
|
| 11 |
-
"challenge_id",
|
| 12 |
-
"scene",
|
| 13 |
-
"lighting",
|
| 14 |
-
"camera_angle",
|
| 15 |
-
"motion_blur",
|
| 16 |
-
"crowd_level",
|
| 17 |
-
"my_model_id",
|
| 18 |
-
"my_score",
|
| 19 |
-
"king_model_id",
|
| 20 |
-
"king_score",
|
| 21 |
-
"delta_score",
|
| 22 |
-
"my_pred_count",
|
| 23 |
-
"king_pred_count",
|
| 24 |
-
"my_latency_p50_ms",
|
| 25 |
-
"my_latency_p95_ms",
|
| 26 |
-
"my_latency_p99_ms",
|
| 27 |
-
"dropped_frames",
|
| 28 |
-
"timeout_count",
|
| 29 |
-
"balaclava_hits",
|
| 30 |
-
"bat_hits",
|
| 31 |
-
"glove_hits",
|
| 32 |
-
"graffiti_hits",
|
| 33 |
-
"hoodie_hits",
|
| 34 |
-
"spray_paint_hits",
|
| 35 |
-
"tag_list",
|
| 36 |
-
"data_source_mix",
|
| 37 |
-
"notes",
|
| 38 |
-
]
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def main() -> int:
|
| 42 |
-
ap = argparse.ArgumentParser()
|
| 43 |
-
ap.add_argument("--csv", required=True)
|
| 44 |
-
ap.add_argument("--challenge-id", required=True)
|
| 45 |
-
ap.add_argument("--scene", default="")
|
| 46 |
-
ap.add_argument("--lighting", default="")
|
| 47 |
-
ap.add_argument("--camera-angle", default="")
|
| 48 |
-
ap.add_argument("--motion-blur", default="")
|
| 49 |
-
ap.add_argument("--crowd-level", default="")
|
| 50 |
-
ap.add_argument("--my-model-id", required=True)
|
| 51 |
-
ap.add_argument("--my-score", type=float, required=True)
|
| 52 |
-
ap.add_argument("--king-model-id", default="alfred8995/crime001")
|
| 53 |
-
ap.add_argument("--king-score", type=float, required=True)
|
| 54 |
-
ap.add_argument("--my-preds", type=int, default=0)
|
| 55 |
-
ap.add_argument("--king-preds", type=int, default=0)
|
| 56 |
-
ap.add_argument("--p50-ms", type=float, default=0.0)
|
| 57 |
-
ap.add_argument("--p95-ms", type=float, default=0.0)
|
| 58 |
-
ap.add_argument("--p99-ms", type=float, default=0.0)
|
| 59 |
-
ap.add_argument("--dropped-frames", type=int, default=0)
|
| 60 |
-
ap.add_argument("--timeouts", type=int, default=0)
|
| 61 |
-
ap.add_argument("--balaclava-hits", type=int, default=0)
|
| 62 |
-
ap.add_argument("--bat-hits", type=int, default=0)
|
| 63 |
-
ap.add_argument("--glove-hits", type=int, default=0)
|
| 64 |
-
ap.add_argument("--graffiti-hits", type=int, default=0)
|
| 65 |
-
ap.add_argument("--hoodie-hits", type=int, default=0)
|
| 66 |
-
ap.add_argument("--spray-paint-hits", type=int, default=0)
|
| 67 |
-
ap.add_argument("--tags", default="")
|
| 68 |
-
ap.add_argument("--data-sources", default="")
|
| 69 |
-
ap.add_argument("--notes", default="")
|
| 70 |
-
args = ap.parse_args()
|
| 71 |
-
|
| 72 |
-
out = Path(args.csv)
|
| 73 |
-
out.parent.mkdir(parents=True, exist_ok=True)
|
| 74 |
-
new_file = not out.exists()
|
| 75 |
-
row = {
|
| 76 |
-
"timestamp_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 77 |
-
"challenge_id": args.challenge_id,
|
| 78 |
-
"scene": args.scene,
|
| 79 |
-
"lighting": args.lighting,
|
| 80 |
-
"camera_angle": args.camera_angle,
|
| 81 |
-
"motion_blur": args.motion_blur,
|
| 82 |
-
"crowd_level": args.crowd_level,
|
| 83 |
-
"my_model_id": args.my_model_id,
|
| 84 |
-
"my_score": args.my_score,
|
| 85 |
-
"king_model_id": args.king_model_id,
|
| 86 |
-
"king_score": args.king_score,
|
| 87 |
-
"delta_score": round(args.my_score - args.king_score, 6),
|
| 88 |
-
"my_pred_count": args.my_preds,
|
| 89 |
-
"king_pred_count": args.king_preds,
|
| 90 |
-
"my_latency_p50_ms": args.p50_ms,
|
| 91 |
-
"my_latency_p95_ms": args.p95_ms,
|
| 92 |
-
"my_latency_p99_ms": args.p99_ms,
|
| 93 |
-
"dropped_frames": args.dropped_frames,
|
| 94 |
-
"timeout_count": args.timeouts,
|
| 95 |
-
"balaclava_hits": args.balaclava_hits,
|
| 96 |
-
"bat_hits": args.bat_hits,
|
| 97 |
-
"glove_hits": args.glove_hits,
|
| 98 |
-
"graffiti_hits": args.graffiti_hits,
|
| 99 |
-
"hoodie_hits": args.hoodie_hits,
|
| 100 |
-
"spray_paint_hits": args.spray_paint_hits,
|
| 101 |
-
"tag_list": args.tags,
|
| 102 |
-
"data_source_mix": args.data_sources,
|
| 103 |
-
"notes": args.notes,
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
with out.open("a", newline="") as f:
|
| 107 |
-
w = csv.DictWriter(f, fieldnames=FIELDS)
|
| 108 |
-
if new_file:
|
| 109 |
-
w.writeheader()
|
| 110 |
-
w.writerow(row)
|
| 111 |
-
|
| 112 |
-
print(f"[ok] logged challenge {args.challenge_id} delta={row['delta_score']:+.4f}")
|
| 113 |
-
return 0
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
if __name__ == "__main__":
|
| 117 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/scripts/summarize_challenges.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
from __future__ import annotations
|
| 3 |
-
|
| 4 |
-
import argparse
|
| 5 |
-
import csv
|
| 6 |
-
from collections import Counter
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def _f(x: str) -> float:
|
| 11 |
-
try:
|
| 12 |
-
return float(x)
|
| 13 |
-
except Exception:
|
| 14 |
-
return 0.0
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def main() -> int:
|
| 18 |
-
ap = argparse.ArgumentParser()
|
| 19 |
-
ap.add_argument("--csv", required=True)
|
| 20 |
-
args = ap.parse_args()
|
| 21 |
-
|
| 22 |
-
p = Path(args.csv)
|
| 23 |
-
if not p.exists():
|
| 24 |
-
print(f"[error] missing {p}")
|
| 25 |
-
return 1
|
| 26 |
-
|
| 27 |
-
rows = list(csv.DictReader(p.open()))
|
| 28 |
-
if not rows:
|
| 29 |
-
print("[info] no rows yet")
|
| 30 |
-
return 0
|
| 31 |
-
|
| 32 |
-
n = len(rows)
|
| 33 |
-
my = [_f(r.get("my_score", "0")) for r in rows]
|
| 34 |
-
king = [_f(r.get("king_score", "0")) for r in rows]
|
| 35 |
-
delta = [m - k for m, k in zip(my, king)]
|
| 36 |
-
p95 = [_f(r.get("my_latency_p95_ms", "0")) for r in rows]
|
| 37 |
-
preds = [_f(r.get("my_pred_count", "0")) for r in rows]
|
| 38 |
-
|
| 39 |
-
tags = Counter()
|
| 40 |
-
for r in rows:
|
| 41 |
-
raw = r.get("tag_list", "")
|
| 42 |
-
for t in [x.strip() for x in raw.split(",") if x.strip()]:
|
| 43 |
-
tags[t] += 1
|
| 44 |
-
|
| 45 |
-
print("=== Detect-crime challenge summary ===")
|
| 46 |
-
print(f"rows={n}")
|
| 47 |
-
print(f"my_score: avg={sum(my)/n:.4f} min={min(my):.4f} max={max(my):.4f}")
|
| 48 |
-
print(f"king_score: avg={sum(king)/n:.4f} min={min(king):.4f} max={max(king):.4f}")
|
| 49 |
-
print(f"delta: avg={sum(delta)/n:+.4f} wins={sum(1 for d in delta if d > 0)}/{n}")
|
| 50 |
-
print(f"latency p95 avg={sum(p95)/n:.1f}ms")
|
| 51 |
-
print(f"pred_count avg={sum(preds)/n:.2f}")
|
| 52 |
-
if tags:
|
| 53 |
-
print("top failure tags:")
|
| 54 |
-
for k, v in tags.most_common(8):
|
| 55 |
-
print(f" - {k}: {v}")
|
| 56 |
-
return 0
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
if __name__ == "__main__":
|
| 60 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/templates/challenge_log.csv
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
timestamp_utc,challenge_id,scene,lighting,camera_angle,motion_blur,crowd_level,my_model_id,my_score,king_model_id,king_score,delta_score,my_pred_count,king_pred_count,my_latency_p50_ms,my_latency_p95_ms,my_latency_p99_ms,dropped_frames,timeout_count,balaclava_hits,bat_hits,glove_hits,graffiti_hits,hoodie_hits,spray_paint_hits,tag_list,data_source_mix,notes
|
| 2 |
-
2026-05-06T07:00:57Z,offline_val_smoke,offline_val_split,,,,,weights.onnx,0.317738,weights.onnx,0.038666,0.279072,,,4.976,,,,,,,,,,,offline_val_split,,auto-log from eval_mine_vs_king.py; scores are mAP50 on local val split
|
| 3 |
-
2026-05-06T07:04:31Z,offline_val_20260506T0702Z,offline_val_split,,,,,weights.onnx,0.317738,weights.onnx,0.038666,0.279072,,,4.984,,,,,,,,,,,offline_val_split,,auto-log from eval_mine_vs_king.py; scores are mAP50 on local val split
|
|
|
|
|
|
|
|
|
|
|
|
detect_crime_ops/templates/deploy_journal.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
# Deploy Journal
|
| 2 |
-
|
| 3 |
-
Use this log for every release candidate.
|
| 4 |
-
|
| 5 |
-
## Entry template
|
| 6 |
-
|
| 7 |
-
- Date (UTC):
|
| 8 |
-
- Model ID / hash:
|
| 9 |
-
- Base weights:
|
| 10 |
-
- Data version:
|
| 11 |
-
- Key threshold config:
|
| 12 |
-
- Offline A/B result vs previous champion:
|
| 13 |
-
- Live score window (N challenges):
|
| 14 |
-
- Decision: deploy / rollback / hold
|
| 15 |
-
- Notes:
|
| 16 |
-
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
-
- Date (UTC): 2026-05-06
|
| 20 |
-
- Model ID / hash: crime_a_from_best_ep29
|
| 21 |
-
- Base weights: `runs/detect/runs/detect/crime_a_from_best/weights/best.pt`
|
| 22 |
-
- Data version: scratch/data rebuild 2026-05-05
|
| 23 |
-
- Key threshold config: see `config/confidence_thresholds.yaml`
|
| 24 |
-
- Offline A/B result vs previous champion: +0.279 mAP50 on local val (not live guarantee)
|
| 25 |
-
- Live score window (N challenges): TODO
|
| 26 |
-
- Decision: TODO
|
| 27 |
-
- Notes: TODO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manako_pool.log
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
[poll] starting; existing frames in pool: 0
|
| 2 |
-
[poll 1] cid=61720 frames=1 new=1 total_unique=1 (cumulative new this run: 1)
|
| 3 |
-
[poll 2] cid=61720 frames=1 new=0 total_unique=1 (cumulative new this run: 1)
|
| 4 |
-
[poll 3] cid=61720 frames=1 new=0 total_unique=1 (cumulative new this run: 1)
|
| 5 |
-
[poll 4] cid=61720 frames=1 new=0 total_unique=1 (cumulative new this run: 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
manako_pool/images/manako_ch61720_f0_c98df4bfdbe5f4db.png
DELETED
Git LFS Details
|
manako_pool/preds/manako_ch61720_f0_c98df4bfdbe5f4db.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"frame_id": 0, "boxes": [{"x1": 727, "y1": 260, "x2": 827, "y2": 395, "cls_id": 1, "conf": 0.9638671875}, {"x1": 884, "y1": 328, "x2": 1014, "y2": 465, "cls_id": 1, "conf": 0.8896484375}, {"x1": 371, "y1": 251, "x2": 439, "y2": 348, "cls_id": 1, "conf": 0.7939453125}, {"x1": 923, "y1": 426, "x2": 945, "y2": 451, "cls_id": 2, "conf": 0.57763671875}, {"x1": 878, "y1": 460, "x2": 901, "y2": 483, "cls_id": 2, "conf": 0.548828125}], "keypoints": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]]}
|
|
|
|
|
|
training/DATASET.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
| 1 |
-
# Detect-crime — Dataset Sources & Pipeline
|
| 2 |
-
|
| 3 |
-
The king's training set is small and synthetic — that's why per-class recalls collapse on
|
| 4 |
-
balaclava (3.4%), glove (6.4%), and spray paint (16.1%). The single biggest score lever is
|
| 5 |
-
**more, real, in-domain data**, especially for the four catastrophic classes.
|
| 6 |
-
|
| 7 |
-
## Target classes (target order — must match `class_names.txt`)
|
| 8 |
-
|
| 9 |
-
| id | name | king recall | priority |
|
| 10 |
-
|---:|---|---:|---|
|
| 11 |
-
| 0 | balaclava | 0.034 | **critical** |
|
| 12 |
-
| 1 | bat | 0.143 | high |
|
| 13 |
-
| 2 | glove | 0.064 | **critical** |
|
| 14 |
-
| 3 | graffiti | 0.321 | medium |
|
| 15 |
-
| 4 | hoodie | 0.274 | medium (carries the IoU mean alone) |
|
| 16 |
-
| 5 | spray paint | 0.161 | high |
|
| 17 |
-
|
| 18 |
-
## Sources
|
| 19 |
-
|
| 20 |
-
### Tier 1 — in-domain (gold)
|
| 21 |
-
|
| 22 |
-
**`manak0/Detect-crime` `latestAnnotatedChallenge` API.** Same pattern as petrol-station: the
|
| 23 |
-
console at `console.scorevision.io/api/v2/elements/manak0%2FDetect-crime?lookback_days=30`
|
| 24 |
-
returns one `latestAnnotatedChallenge` record at a time. The record rotates as the runner
|
| 25 |
-
emits fresh challenges, so polling at ~120s for several hours / days accumulates distinct
|
| 26 |
-
in-domain frames *with the king's predictions attached*. Use those predictions as silver
|
| 27 |
-
labels (king is 0.576, so they're noisy but in-domain).
|
| 28 |
-
|
| 29 |
-
Use `poll_manako.py` to collect; use `build_dataset.py --manako` to integrate.
|
| 30 |
-
|
| 31 |
-
### Tier 2 — public detection datasets (silver / gold mix)
|
| 32 |
-
|
| 33 |
-
| class | dataset | notes |
|
| 34 |
-
|---|---|---|
|
| 35 |
-
| **balaclava** | Roboflow Universe `balaclava-detection` (multiple variants, ~1k–5k labeled imgs) | search "balaclava" on universe.roboflow.com; download YOLO format |
|
| 36 |
-
| balaclava | MAFA — masked-face dataset (35k labeled faces, occluded) | ~3k images contain ski-mask / balaclava-style coverage; relabel as "balaclava" only when the mouth+nose+ears are all occluded |
|
| 37 |
-
| **bat** | COCO 2017, class 35 "baseball bat" (~3.3k train + ~150 val instances) | already labeled YOLO-compatible after `pycocotools` re-export; map cls 35 -> our cls 1 |
|
| 38 |
-
| bat | Open Images V7, class `Baseball bat` | larger, but noisier labels |
|
| 39 |
-
| **glove** | Roboflow Universe `gloves-detection` / `ppe-detection` | ~5k–10k labeled; merges leather, latex, work gloves |
|
| 40 |
-
| glove | COCO 2017 has no "glove" class; ImageNet n02883344 "ski glove" / n03775546 "boxing glove" — classification only, but useful for crops |
|
| 41 |
-
| **graffiti** | Roboflow Universe `graffiti-detection` | several variants 500–3k imgs |
|
| 42 |
-
| graffiti | STORM-IRMA Graffiti Dataset (Bardienus et al., academic release) | annotated graffiti tags + region bboxes |
|
| 43 |
-
| **hoodie** | DeepFashion2 (item id `short_sleeve_top + long_sleeve_outwear`) | very large; relabel "hooded" subset only |
|
| 44 |
-
| hoodie | Roboflow Universe `clothing-detection` | hooded sweatshirt subset |
|
| 45 |
-
| **spray paint** | Roboflow Universe `spray-paint-can`/`graffiti-tools` | small, 200–800 imgs each |
|
| 46 |
-
| spray paint | ImageNet n04270147 "spray can" | classification only — useful as copy-paste source crops |
|
| 47 |
-
|
| 48 |
-
Roboflow Universe links rotate; the build script uses the public Roboflow REST API
|
| 49 |
-
(`https://universe.roboflow.com/...`) and falls back to anonymous downloads for permissive-
|
| 50 |
-
licensed datasets. **Always check each dataset's license before use** — if a dataset is
|
| 51 |
-
non-redistributable, paste it into `--extra-dir` from a local copy instead.
|
| 52 |
-
|
| 53 |
-
### Tier 3 — synthetic & augmentation
|
| 54 |
-
|
| 55 |
-
- **Copy-paste augmentation** (built into ultralytics: `copy_paste=0.30`). Disproportionately
|
| 56 |
-
helps rare classes by pasting their crops into other training images.
|
| 57 |
-
- **Mosaic + mixup** (also built-in): standard for YOLOv11.
|
| 58 |
-
- **Photometric augmentation**: HSV jitter, brightness scaling. CCTV crime footage is
|
| 59 |
-
high-variance lighting (flashbang, IR night vision, daylight) — push `hsv_v` to 0.5.
|
| 60 |
-
|
| 61 |
-
## Pipeline
|
| 62 |
-
|
| 63 |
-
```
|
| 64 |
-
sources → KingLabeler (silver) ──┐
|
| 65 |
-
├─ manako_pool/ (poll_manako.py) king's preds attached │
|
| 66 |
-
├─ roboflow/ (download.py per-class) │
|
| 67 |
-
├─ coco_bat/ (subset extractor) ├─ build_dataset.py
|
| 68 |
-
├─ openimages/ (`oidv6` CLI, optional) │ → YOLO data.yaml
|
| 69 |
-
└─ extra/ (manual / verified frames) │
|
| 70 |
-
┘
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
## Stage A vs Stage B
|
| 74 |
-
|
| 75 |
-
- **Stage A — silver pretrain**: train on the **assembled silver corpus** (Roboflow + COCO
|
| 76 |
-
+ king-labeled manako + ImageNet copy-paste source). Targets: `200 epochs` at `imgsz=1280`,
|
| 77 |
-
YOLOv11s. Use heavy augmentation. Save EMA. Expect val mAP@50 around 0.30–0.45 — well
|
| 78 |
-
above king's 0.14, but synthetic-eval-flattering.
|
| 79 |
-
- **Stage B — clean fine-tune**: hand-verify (or LLM-verify with Qwen2-VL) ~300 manako frames
|
| 80 |
-
drawn from the latest 7-day window. This is the small-but-clean set that aligns most
|
| 81 |
-
closely with the live evaluator. Train `50 epochs` at `imgsz=1280` on Stage A weights with
|
| 82 |
-
reduced augmentation. Expect val mAP@50 to recede slightly (cleaner GT, harder eval) but
|
| 83 |
-
live-element score to *rise*, because we've shifted the distribution toward what's actually
|
| 84 |
-
scored on subnet.
|
| 85 |
-
|
| 86 |
-
## Class balancing
|
| 87 |
-
|
| 88 |
-
The king's set is heavily skewed toward hoodie (~half of GT boxes). Without rebalancing, our
|
| 89 |
-
model would inherit that. Two interventions:
|
| 90 |
-
|
| 91 |
-
1. **Weighted sampling** in dataloader (`sampler=` weighted by 1/sqrt(class_count)).
|
| 92 |
-
2. **Copy-paste augmentation** sourced from a curated rare-class crop bank
|
| 93 |
-
(`crime_miner/training/_rare_crops/{balaclava, glove, spray_paint}/`). The build script
|
| 94 |
-
produces this bank as a byproduct.
|
| 95 |
-
|
| 96 |
-
## What the build script writes
|
| 97 |
-
|
| 98 |
-
```
|
| 99 |
-
data/
|
| 100 |
-
├── data.yaml # ultralytics-style; names + train/val paths
|
| 101 |
-
├── images/
|
| 102 |
-
│ ├── train/{src}_{sha}.jpg # one per source image
|
| 103 |
-
│ └── val/...
|
| 104 |
-
└── labels/
|
| 105 |
-
├── train/{src}_{sha}.txt # YOLO format: cls cx cy w h (normalized)
|
| 106 |
-
└── val/...
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
Manako frames are forced into `val` so the held-out set lives in the real evaluation domain.
|
| 110 |
-
|
| 111 |
-
## Hard-class strategy: balaclava
|
| 112 |
-
|
| 113 |
-
Balaclava is the lowest-recall class. Two failure modes from the king's perspective:
|
| 114 |
-
|
| 115 |
-
1. **Box too small at 640 input** (king's stretch resize destroys 30 px head crops).
|
| 116 |
-
*Solved* by 1280 letterboxed input + multi-scale TTA. No retraining required.
|
| 117 |
-
2. **Few training examples**. Mitigations:
|
| 118 |
-
- Pull MAFA (~3k masked-face images) and select the ~30% subset where the mask covers the
|
| 119 |
-
entire head (i.e., balaclava, not surgical mask).
|
| 120 |
-
- Roboflow `balaclava-detection-v3` has ~1.5k labeled.
|
| 121 |
-
- Synthesize via copy-paste of balaclava crops onto random `hoodie` GT boxes (we have many
|
| 122 |
-
hoodie images; pasting a balaclava crop on top is plausible-looking).
|
| 123 |
-
|
| 124 |
-
Target: **5,000 balaclava-positive training images** going into Stage A.
|
| 125 |
-
|
| 126 |
-
## Notes on label vocabulary
|
| 127 |
-
|
| 128 |
-
Both the manak0 baseline and our model emit class names matching `class_names.txt` exactly.
|
| 129 |
-
**Do not rename or reorder** — class id is what's compared on-subnet.
|
| 130 |
-
|
| 131 |
-
If the live element uses SAM3 pseudo-GT (not real GT), SAM3 will be prompted with these
|
| 132 |
-
exact strings as text labels (`scorevision/vlm_pipeline/sam3/detect_objects.py:36`). So the
|
| 133 |
-
class names in `class_names.txt` *are* the SAM3 prompts.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/__pycache__/build_dataset.cpython-312.pyc
DELETED
|
Binary file (56.4 kB)
|
|
|
training/__pycache__/eval_mine_vs_king.cpython-312.pyc
DELETED
|
Binary file (10.3 kB)
|
|
|
training/build_dataset.py
DELETED
|
@@ -1,860 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
End-to-end dataset builder for Detect-crime.
|
| 3 |
-
|
| 4 |
-
Sources (each toggleable via flags; pipeline tolerates missing sources):
|
| 5 |
-
1. manako latestAnnotatedChallenge (--manako): king-labeled in-domain frames.
|
| 6 |
-
2. Roboflow Universe (--roboflow PER_CLASS_SLUGS): per-class detection datasets.
|
| 7 |
-
3. COCO 2017 'baseball bat' subset (--coco-bat): high-quality bat boxes.
|
| 8 |
-
4. Extra dir (--extra-dir): pre-collected images you've curated locally.
|
| 9 |
-
|
| 10 |
-
Each Roboflow / COCO source is mapped to our 6-class target via an explicit class table.
|
| 11 |
-
Empty-label images are dropped unless --keep-empty (we keep them as negative-bg supervision
|
| 12 |
-
during silver pretrain).
|
| 13 |
-
|
| 14 |
-
Pipeline:
|
| 15 |
-
- Pull each enabled source into a working dir.
|
| 16 |
-
- Auto-relabel: re-emit YOLO label files with cls ids remapped to our target order.
|
| 17 |
-
For sources that come *unlabeled* (e.g. manako frames without preds), run the king's
|
| 18 |
-
ONNX over them to get silver labels.
|
| 19 |
-
- Manako frames go to val (real-domain eval); rest is 90/10 train/val.
|
| 20 |
-
|
| 21 |
-
Usage:
|
| 22 |
-
python build_dataset.py \
|
| 23 |
-
--out ../data \
|
| 24 |
-
--king-onnx /root/turbovision_crime/king_models/Detect-crime/weights.onnx \
|
| 25 |
-
--manako --manako-polls 30 \
|
| 26 |
-
--roboflow balaclava=brainster/balaclava-detection-v3 \
|
| 27 |
-
--roboflow glove=ppe-detection/gloves-v1 \
|
| 28 |
-
--coco-bat /path/to/coco/annotations/instances_train2017.json /path/to/coco/train2017 \
|
| 29 |
-
--extra-dir ../data_extra --keep-empty
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
from __future__ import annotations
|
| 33 |
-
|
| 34 |
-
import argparse
|
| 35 |
-
import hashlib
|
| 36 |
-
import io
|
| 37 |
-
import json
|
| 38 |
-
import os
|
| 39 |
-
import random
|
| 40 |
-
import shutil
|
| 41 |
-
import sys
|
| 42 |
-
import time
|
| 43 |
-
from pathlib import Path
|
| 44 |
-
from urllib.parse import quote
|
| 45 |
-
from urllib.request import Request, urlopen
|
| 46 |
-
|
| 47 |
-
import numpy as np
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
TARGET_CLASS_NAMES = ["balaclava", "bat", "glove", "graffiti", "hoodie", "spray paint"]
|
| 51 |
-
CLASS_TO_ID = {name: i for i, name in enumerate(TARGET_CLASS_NAMES)}
|
| 52 |
-
ELEMENT_ID = "manak0/Detect-crime"
|
| 53 |
-
CONSOLE_API = "https://console.scorevision.io/api/v2"
|
| 54 |
-
|
| 55 |
-
# The current king (`alfred8995/crime001`) emits class ids in their own order:
|
| 56 |
-
# 0 balaclava, 1 hoodie, 2 glove, 3 bat, 4 spray paint, 5 graffiti
|
| 57 |
-
# When we use their ONNX as a silver labeler OR ingest manako API predictions, we
|
| 58 |
-
# remap from alfred's order -> our target (manak0-baseline) order.
|
| 59 |
-
KING_CLASS_ORDER = ["balaclava", "hoodie", "glove", "bat", "spray paint", "graffiti"]
|
| 60 |
-
KING_TO_TARGET_REMAP = np.array(
|
| 61 |
-
[TARGET_CLASS_NAMES.index(n) for n in KING_CLASS_ORDER], dtype=np.int32
|
| 62 |
-
)
|
| 63 |
-
# Sanity: KING_TO_TARGET_REMAP == [0, 4, 2, 1, 5, 3]
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
# --------------------------------------------------------------------- utils
|
| 67 |
-
|
| 68 |
-
def http_get(url: str, timeout: int = 60, retries: int = 4, headers: dict | None = None) -> bytes:
|
| 69 |
-
last = None
|
| 70 |
-
base_headers = {"User-Agent": "build_dataset/1.0"}
|
| 71 |
-
if headers:
|
| 72 |
-
base_headers.update(headers)
|
| 73 |
-
for i in range(retries):
|
| 74 |
-
try:
|
| 75 |
-
req = Request(url, headers=base_headers)
|
| 76 |
-
with urlopen(req, timeout=timeout) as r:
|
| 77 |
-
return r.read()
|
| 78 |
-
except Exception as e:
|
| 79 |
-
last = e
|
| 80 |
-
if i < retries - 1:
|
| 81 |
-
time.sleep(1.5 * (i + 1))
|
| 82 |
-
raise last # type: ignore
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def sha16(b: bytes) -> str:
|
| 86 |
-
return hashlib.sha256(b).hexdigest()[:16]
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def image_size(buf: bytes) -> tuple[int, int]:
|
| 90 |
-
if buf[:8] == b"\x89PNG\r\n\x1a\n":
|
| 91 |
-
return int.from_bytes(buf[16:20], "big"), int.from_bytes(buf[20:24], "big")
|
| 92 |
-
if buf[:2] == b"\xff\xd8":
|
| 93 |
-
from PIL import Image
|
| 94 |
-
return Image.open(io.BytesIO(buf)).size
|
| 95 |
-
from PIL import Image
|
| 96 |
-
return Image.open(io.BytesIO(buf)).size
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
# --------------------------------------------------------------------- sources
|
| 100 |
-
|
| 101 |
-
def fetch_manako(work: Path, log, polls: int = 1, delay_s: int = 180) -> list[tuple[Path, str, dict | None]]:
|
| 102 |
-
"""Pull rotating latestAnnotatedChallenge frames + king predictions."""
|
| 103 |
-
out: list[tuple[Path, str, dict | None]] = []
|
| 104 |
-
img_dir = work / "manako_images"
|
| 105 |
-
img_dir.mkdir(parents=True, exist_ok=True)
|
| 106 |
-
seen_paths: set[str] = set()
|
| 107 |
-
|
| 108 |
-
for attempt in range(max(1, polls)):
|
| 109 |
-
try:
|
| 110 |
-
raw = http_get(f"{CONSOLE_API}/elements/{quote(ELEMENT_ID, safe='')}?lookback_days=30")
|
| 111 |
-
except Exception as e:
|
| 112 |
-
log(f"[manako] API call failed (attempt {attempt + 1}/{polls}): {e}")
|
| 113 |
-
if attempt < polls - 1:
|
| 114 |
-
time.sleep(delay_s)
|
| 115 |
-
continue
|
| 116 |
-
elt = json.loads(raw)
|
| 117 |
-
lac = elt.get("latestAnnotatedChallenge")
|
| 118 |
-
if not lac:
|
| 119 |
-
log(f"[manako] no latestAnnotatedChallenge (attempt {attempt + 1}/{polls})")
|
| 120 |
-
if attempt < polls - 1:
|
| 121 |
-
time.sleep(delay_s)
|
| 122 |
-
continue
|
| 123 |
-
|
| 124 |
-
frames = (lac.get("response") or {}).get("frames") or []
|
| 125 |
-
preds = ((lac.get("response") or {}).get("predictions") or {}).get("frames") or []
|
| 126 |
-
preds_by_id = {p.get("frame_id"): p for p in preds}
|
| 127 |
-
|
| 128 |
-
new_count = 0
|
| 129 |
-
for f in frames:
|
| 130 |
-
url = f.get("url")
|
| 131 |
-
fid = f.get("frame_id")
|
| 132 |
-
if not url:
|
| 133 |
-
continue
|
| 134 |
-
try:
|
| 135 |
-
data = http_get(url)
|
| 136 |
-
except Exception as e:
|
| 137 |
-
log(f"[manako] image fetch failed {url}: {e}")
|
| 138 |
-
continue
|
| 139 |
-
digest = sha16(data)
|
| 140 |
-
path = img_dir / f"manako_ch{lac.get('challenge_id')}_f{fid}_{digest}.png"
|
| 141 |
-
if str(path) in seen_paths:
|
| 142 |
-
continue
|
| 143 |
-
seen_paths.add(str(path))
|
| 144 |
-
path.write_bytes(data)
|
| 145 |
-
out.append((path, "manako", preds_by_id.get(fid)))
|
| 146 |
-
new_count += 1
|
| 147 |
-
|
| 148 |
-
log(f"[manako] poll {attempt + 1}/{polls}: +{new_count} (total {len(out)}, "
|
| 149 |
-
f"challenge_id={lac.get('challenge_id')})")
|
| 150 |
-
if attempt < polls - 1:
|
| 151 |
-
time.sleep(delay_s)
|
| 152 |
-
|
| 153 |
-
log(f"[manako] pulled {len(out)} unique image(s) across {polls} poll(s)")
|
| 154 |
-
return out
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
def fetch_roboflow(slug: str, target_class: str, work: Path, log,
|
| 158 |
-
api_key: str | None = None) -> list[tuple[Path, str, list[str]]]:
|
| 159 |
-
"""Download a Roboflow Universe dataset in YOLO format and remap to target class id.
|
| 160 |
-
|
| 161 |
-
`slug` is the workspace/project[/version] path on universe.roboflow.com.
|
| 162 |
-
Without API key, only public/free datasets work; many crime-relevant ones are public.
|
| 163 |
-
"""
|
| 164 |
-
target_id = CLASS_TO_ID.get(target_class)
|
| 165 |
-
if target_id is None:
|
| 166 |
-
log(f"[roboflow] unknown target class: {target_class}")
|
| 167 |
-
return []
|
| 168 |
-
|
| 169 |
-
out_dir = work / "roboflow" / slug.replace("/", "_")
|
| 170 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 171 |
-
out: list[tuple[Path, str, list[str]]] = []
|
| 172 |
-
|
| 173 |
-
try:
|
| 174 |
-
from roboflow import Roboflow # type: ignore
|
| 175 |
-
except ImportError:
|
| 176 |
-
log("[roboflow] roboflow SDK not installed; pip install roboflow")
|
| 177 |
-
return []
|
| 178 |
-
|
| 179 |
-
if api_key is None:
|
| 180 |
-
api_key = os.environ.get("ROBOFLOW_API_KEY", "")
|
| 181 |
-
if not api_key:
|
| 182 |
-
log(f"[roboflow] no ROBOFLOW_API_KEY set; skipping {slug}")
|
| 183 |
-
return []
|
| 184 |
-
|
| 185 |
-
parts = slug.split("/")
|
| 186 |
-
if len(parts) < 2:
|
| 187 |
-
log(f"[roboflow] malformed slug: {slug}")
|
| 188 |
-
return []
|
| 189 |
-
ws, project = parts[0], parts[1]
|
| 190 |
-
version = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 1
|
| 191 |
-
|
| 192 |
-
rf = Roboflow(api_key=api_key)
|
| 193 |
-
ds = rf.workspace(ws).project(project).version(version).download("yolov8", location=str(out_dir))
|
| 194 |
-
log(f"[roboflow] downloaded {slug} -> {ds.location}")
|
| 195 |
-
|
| 196 |
-
# Roboflow YOLO export: {ds.location}/{train,valid,test}/images/*, labels/*.txt with their own data.yaml
|
| 197 |
-
rf_yaml = Path(ds.location) / "data.yaml"
|
| 198 |
-
if not rf_yaml.exists():
|
| 199 |
-
log(f"[roboflow] no data.yaml in {ds.location}; skipping")
|
| 200 |
-
return []
|
| 201 |
-
|
| 202 |
-
# Parse names from data.yaml (we accept either yaml or simple key:value).
|
| 203 |
-
try:
|
| 204 |
-
import yaml
|
| 205 |
-
rf_cfg = yaml.safe_load(rf_yaml.read_text())
|
| 206 |
-
except Exception as e:
|
| 207 |
-
log(f"[roboflow] yaml parse failed for {rf_yaml}: {e}")
|
| 208 |
-
return []
|
| 209 |
-
rf_names = rf_cfg.get("names") or []
|
| 210 |
-
if isinstance(rf_names, dict):
|
| 211 |
-
rf_names = [rf_names[k] for k in sorted(rf_names.keys())]
|
| 212 |
-
|
| 213 |
-
for split in ("train", "valid", "test"):
|
| 214 |
-
img_dir = Path(ds.location) / split / "images"
|
| 215 |
-
lbl_dir = Path(ds.location) / split / "labels"
|
| 216 |
-
if not img_dir.exists() or not lbl_dir.exists():
|
| 217 |
-
continue
|
| 218 |
-
for img_path in img_dir.iterdir():
|
| 219 |
-
if not img_path.is_file():
|
| 220 |
-
continue
|
| 221 |
-
lbl_path = lbl_dir / (img_path.stem + ".txt")
|
| 222 |
-
if not lbl_path.exists():
|
| 223 |
-
continue
|
| 224 |
-
try:
|
| 225 |
-
rows = lbl_path.read_text().strip().splitlines()
|
| 226 |
-
except Exception:
|
| 227 |
-
continue
|
| 228 |
-
mapped: list[str] = []
|
| 229 |
-
for r in rows:
|
| 230 |
-
parts = r.split()
|
| 231 |
-
if len(parts) != 5:
|
| 232 |
-
continue
|
| 233 |
-
src_cls = int(parts[0])
|
| 234 |
-
# Single-class roboflow datasets: any positive box -> target class
|
| 235 |
-
if len(rf_names) == 1 or src_cls >= len(rf_names):
|
| 236 |
-
mapped.append(f"{target_id} {parts[1]} {parts[2]} {parts[3]} {parts[4]}")
|
| 237 |
-
continue
|
| 238 |
-
# Multi-class: keep only boxes whose name matches the target (loose match).
|
| 239 |
-
src_name = str(rf_names[src_cls]).lower().replace("_", " ")
|
| 240 |
-
if target_class.lower() in src_name or src_name in target_class.lower():
|
| 241 |
-
mapped.append(f"{target_id} {parts[1]} {parts[2]} {parts[3]} {parts[4]}")
|
| 242 |
-
if not mapped:
|
| 243 |
-
continue
|
| 244 |
-
out.append((img_path, f"roboflow_{slug.replace('/', '_')}", mapped))
|
| 245 |
-
|
| 246 |
-
log(f"[roboflow] {slug}: kept {len(out)} labeled images")
|
| 247 |
-
return out
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
def fetch_coco_bat(coco_ann_json: Path, coco_img_dir: Path, log) -> list[tuple[Path, str, list[str]]]:
|
| 251 |
-
"""Extract baseball-bat images from a COCO 2017 annotations file."""
|
| 252 |
-
target_id = CLASS_TO_ID["bat"]
|
| 253 |
-
out: list[tuple[Path, str, list[str]]] = []
|
| 254 |
-
if not coco_ann_json.exists() or not coco_img_dir.exists():
|
| 255 |
-
log(f"[coco-bat] paths missing: {coco_ann_json}, {coco_img_dir}")
|
| 256 |
-
return out
|
| 257 |
-
|
| 258 |
-
log(f"[coco-bat] reading {coco_ann_json} (this is large; ~30s)...")
|
| 259 |
-
with coco_ann_json.open() as f:
|
| 260 |
-
coco = json.load(f)
|
| 261 |
-
|
| 262 |
-
# COCO category id 39 = "baseball bat" in the 80-class set.
|
| 263 |
-
bat_ids = {c["id"] for c in coco.get("categories", []) if c.get("name", "").lower() == "baseball bat"}
|
| 264 |
-
if not bat_ids:
|
| 265 |
-
log("[coco-bat] no 'baseball bat' category found; aborting")
|
| 266 |
-
return out
|
| 267 |
-
|
| 268 |
-
# Group annotations by image_id.
|
| 269 |
-
images = {im["id"]: im for im in coco.get("images", [])}
|
| 270 |
-
by_img: dict[int, list[dict]] = {}
|
| 271 |
-
for ann in coco.get("annotations", []):
|
| 272 |
-
if ann.get("category_id") in bat_ids:
|
| 273 |
-
by_img.setdefault(ann["image_id"], []).append(ann)
|
| 274 |
-
|
| 275 |
-
for img_id, anns in by_img.items():
|
| 276 |
-
info = images.get(img_id)
|
| 277 |
-
if not info:
|
| 278 |
-
continue
|
| 279 |
-
fname = info["file_name"]
|
| 280 |
-
w, h = info["width"], info["height"]
|
| 281 |
-
img_path = coco_img_dir / fname
|
| 282 |
-
if not img_path.exists():
|
| 283 |
-
continue
|
| 284 |
-
rows = []
|
| 285 |
-
for a in anns:
|
| 286 |
-
x, y, bw, bh = a["bbox"] # COCO xywh top-left
|
| 287 |
-
cx = (x + bw / 2.0) / w
|
| 288 |
-
cy = (y + bh / 2.0) / h
|
| 289 |
-
rows.append(f"{target_id} {cx:.6f} {cy:.6f} {bw / w:.6f} {bh / h:.6f}")
|
| 290 |
-
if rows:
|
| 291 |
-
out.append((img_path, "coco_bat", rows))
|
| 292 |
-
log(f"[coco-bat] kept {len(out)} bat-positive images")
|
| 293 |
-
return out
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
def fetch_hf_parquet_coco(repo_id: str, filename: str, target_class: str,
|
| 297 |
-
work: Path, log,
|
| 298 |
-
limit: int = 0) -> list[tuple[bytes, str, list[str]]]:
|
| 299 |
-
"""Pull a HuggingFace `datasets`-style parquet whose rows have
|
| 300 |
-
`image: {bytes}`, `width`, `height`, `objects: [{bbox: [x,y,w,h], ...}]`
|
| 301 |
-
(graffiti dataset format). Maps every box to TARGET_CLASS_NAMES[target_class].
|
| 302 |
-
|
| 303 |
-
Returns list of (image_bytes, source_tag, yolo_label_rows).
|
| 304 |
-
"""
|
| 305 |
-
target_id = CLASS_TO_ID.get(target_class)
|
| 306 |
-
if target_id is None:
|
| 307 |
-
log(f"[hf-parquet-coco] unknown target class: {target_class}")
|
| 308 |
-
return []
|
| 309 |
-
try:
|
| 310 |
-
from huggingface_hub import hf_hub_download
|
| 311 |
-
import pyarrow.parquet as pq
|
| 312 |
-
except ImportError:
|
| 313 |
-
log("[hf-parquet-coco] missing deps; pip install huggingface_hub pyarrow")
|
| 314 |
-
return []
|
| 315 |
-
try:
|
| 316 |
-
local = hf_hub_download(
|
| 317 |
-
repo_id=repo_id, filename=filename,
|
| 318 |
-
repo_type="dataset", cache_dir=str(work / "_hf_cache"),
|
| 319 |
-
)
|
| 320 |
-
except Exception as e:
|
| 321 |
-
log(f"[hf-parquet-coco] download failed {repo_id}/{filename}: {e}")
|
| 322 |
-
return []
|
| 323 |
-
log(f"[hf-parquet-coco] downloaded {repo_id}/{filename}")
|
| 324 |
-
out: list[tuple[bytes, str, list[str]]] = []
|
| 325 |
-
table = pq.read_table(local, columns=["image", "width", "height", "objects"])
|
| 326 |
-
rows = table.to_pylist()
|
| 327 |
-
for row in rows:
|
| 328 |
-
img_dict = row.get("image") or {}
|
| 329 |
-
data = img_dict.get("bytes") if isinstance(img_dict, dict) else None
|
| 330 |
-
if not data:
|
| 331 |
-
continue
|
| 332 |
-
w = int(row.get("width") or 0)
|
| 333 |
-
h = int(row.get("height") or 0)
|
| 334 |
-
if w <= 0 or h <= 0:
|
| 335 |
-
try:
|
| 336 |
-
w, h = image_size(data)
|
| 337 |
-
except Exception:
|
| 338 |
-
continue
|
| 339 |
-
labels: list[str] = []
|
| 340 |
-
for obj in row.get("objects") or []:
|
| 341 |
-
bbox = obj.get("bbox") or []
|
| 342 |
-
if len(bbox) != 4:
|
| 343 |
-
continue
|
| 344 |
-
bx, by, bw, bh = (float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3]))
|
| 345 |
-
if bw <= 1 or bh <= 1:
|
| 346 |
-
continue
|
| 347 |
-
cx = (bx + bw / 2.0) / w
|
| 348 |
-
cy = (by + bh / 2.0) / h
|
| 349 |
-
labels.append(f"{target_id} {cx:.6f} {cy:.6f} {bw / w:.6f} {bh / h:.6f}")
|
| 350 |
-
if labels:
|
| 351 |
-
out.append((data, f"hf_{repo_id.replace('/', '_')}", labels))
|
| 352 |
-
if limit and len(out) >= limit:
|
| 353 |
-
break
|
| 354 |
-
log(f"[hf-parquet-coco] {repo_id}: kept {len(out)} labeled images")
|
| 355 |
-
return out
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
def fetch_hf_parquet_cls(repo_id: str, filename: str, target_class: str,
|
| 359 |
-
work: Path, log,
|
| 360 |
-
limit: int = 0) -> list[tuple[bytes, str, None]]:
|
| 361 |
-
"""Pull a HuggingFace classification-synset parquet (rows have just
|
| 362 |
-
`image: {bytes}` with optional `label`). Boxes will be filled in later by
|
| 363 |
-
the king's ONNX silver labeler — this fn just extracts the image bytes.
|
| 364 |
-
|
| 365 |
-
`target_class` is purely a tag for downstream filtering; the king labels
|
| 366 |
-
every detection regardless.
|
| 367 |
-
"""
|
| 368 |
-
try:
|
| 369 |
-
from huggingface_hub import hf_hub_download
|
| 370 |
-
import pyarrow.parquet as pq
|
| 371 |
-
except ImportError:
|
| 372 |
-
log("[hf-parquet-cls] missing deps; pip install huggingface_hub pyarrow")
|
| 373 |
-
return []
|
| 374 |
-
try:
|
| 375 |
-
local = hf_hub_download(
|
| 376 |
-
repo_id=repo_id, filename=filename,
|
| 377 |
-
repo_type="dataset", cache_dir=str(work / "_hf_cache"),
|
| 378 |
-
)
|
| 379 |
-
except Exception as e:
|
| 380 |
-
log(f"[hf-parquet-cls] download failed {repo_id}/{filename}: {e}")
|
| 381 |
-
return []
|
| 382 |
-
log(f"[hf-parquet-cls] downloaded {repo_id}/{filename}")
|
| 383 |
-
out: list[tuple[bytes, str, None]] = []
|
| 384 |
-
table = pq.read_table(local, columns=["image"])
|
| 385 |
-
for row in table.to_pylist():
|
| 386 |
-
img_dict = row.get("image") or {}
|
| 387 |
-
data = img_dict.get("bytes") if isinstance(img_dict, dict) else None
|
| 388 |
-
if not data:
|
| 389 |
-
continue
|
| 390 |
-
out.append((data, f"hf_{repo_id.replace('/', '_')}_{target_class}", None))
|
| 391 |
-
if limit and len(out) >= limit:
|
| 392 |
-
break
|
| 393 |
-
log(f"[hf-parquet-cls] {repo_id}: extracted {len(out)} images (king will label)")
|
| 394 |
-
return out
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
def fetch_extra(extra_dir: Path | None, log) -> list[tuple[Path, str, None]]:
|
| 398 |
-
if extra_dir is None or not extra_dir.exists():
|
| 399 |
-
return []
|
| 400 |
-
out: list[tuple[Path, str, None]] = []
|
| 401 |
-
for ext in ("*.jpg", "*.jpeg", "*.png", "*.bmp", "*.webp"):
|
| 402 |
-
for p in extra_dir.rglob(ext):
|
| 403 |
-
out.append((p, "extra", None))
|
| 404 |
-
log(f"[extra] picked up {len(out)} images from {extra_dir}")
|
| 405 |
-
return out
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
# --------------------------------------------------------------------- king labeler
|
| 409 |
-
|
| 410 |
-
class KingLabeler:
|
| 411 |
-
"""Run the king's ONNX over an unlabeled image to produce YOLO-format silver labels.
|
| 412 |
-
|
| 413 |
-
Mirrors alfred8995/crime001's preprocessing: letterbox to native input H/W, RGB / 255,
|
| 414 |
-
NCHW float32. Decodes both [N,6] / [1,N,6] (NMS-baked) and raw [1, C, N] formats with
|
| 415 |
-
proper letterbox-reverse back to original image coords.
|
| 416 |
-
"""
|
| 417 |
-
|
| 418 |
-
def __init__(self, onnx_path: Path, conf_floor: float = 0.15, intra_threads: int = 0) -> None:
|
| 419 |
-
import onnxruntime as ort
|
| 420 |
-
sess_options = ort.SessionOptions()
|
| 421 |
-
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 422 |
-
if intra_threads > 0:
|
| 423 |
-
sess_options.intra_op_num_threads = int(intra_threads)
|
| 424 |
-
try:
|
| 425 |
-
self.session = ort.InferenceSession(
|
| 426 |
-
str(onnx_path), sess_options=sess_options,
|
| 427 |
-
providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
|
| 428 |
-
)
|
| 429 |
-
except Exception:
|
| 430 |
-
self.session = ort.InferenceSession(
|
| 431 |
-
str(onnx_path), sess_options=sess_options,
|
| 432 |
-
providers=["CPUExecutionProvider"],
|
| 433 |
-
)
|
| 434 |
-
self.input_name = self.session.get_inputs()[0].name
|
| 435 |
-
self.output_names = [o.name for o in self.session.get_outputs()]
|
| 436 |
-
in_shape = self.session.get_inputs()[0].shape
|
| 437 |
-
self.in_h = int(in_shape[2]) if isinstance(in_shape[2], int) and in_shape[2] > 0 else 640
|
| 438 |
-
self.in_w = int(in_shape[3]) if isinstance(in_shape[3], int) and in_shape[3] > 0 else 640
|
| 439 |
-
self.conf_floor = float(conf_floor)
|
| 440 |
-
self.iou_thresh = 0.45
|
| 441 |
-
|
| 442 |
-
@staticmethod
|
| 443 |
-
def _letterbox(image: np.ndarray, new_size: tuple[int, int],
|
| 444 |
-
color: tuple[int, int, int] = (114, 114, 114)
|
| 445 |
-
) -> tuple[np.ndarray, float, tuple[float, float]]:
|
| 446 |
-
import cv2
|
| 447 |
-
h, w = image.shape[:2]
|
| 448 |
-
new_w, new_h = new_size
|
| 449 |
-
ratio = min(new_w / w, new_h / h)
|
| 450 |
-
rw, rh = int(round(w * ratio)), int(round(h * ratio))
|
| 451 |
-
if (rw, rh) != (w, h):
|
| 452 |
-
interp = cv2.INTER_CUBIC if ratio > 1.0 else cv2.INTER_LINEAR
|
| 453 |
-
image = cv2.resize(image, (rw, rh), interpolation=interp)
|
| 454 |
-
dw = (new_w - rw) / 2.0
|
| 455 |
-
dh = (new_h - rh) / 2.0
|
| 456 |
-
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
|
| 457 |
-
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
|
| 458 |
-
padded = cv2.copyMakeBorder(image, top, bottom, left, right,
|
| 459 |
-
borderType=cv2.BORDER_CONSTANT, value=color)
|
| 460 |
-
return padded, ratio, (dw, dh)
|
| 461 |
-
|
| 462 |
-
def label(self, image_bytes: bytes) -> list[str]:
|
| 463 |
-
import cv2
|
| 464 |
-
arr = np.frombuffer(image_bytes, dtype=np.uint8)
|
| 465 |
-
bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
| 466 |
-
if bgr is None:
|
| 467 |
-
return []
|
| 468 |
-
orig_h, orig_w = bgr.shape[:2]
|
| 469 |
-
|
| 470 |
-
padded, ratio, (dw, dh) = self._letterbox(bgr, (self.in_w, self.in_h))
|
| 471 |
-
rgb = cv2.cvtColor(padded, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
|
| 472 |
-
x = np.transpose(rgb, (2, 0, 1))[None, ...]
|
| 473 |
-
x = np.ascontiguousarray(x, dtype=np.float32)
|
| 474 |
-
|
| 475 |
-
out = self.session.run(self.output_names, {self.input_name: x})[0]
|
| 476 |
-
return self._decode(out, ratio, dw, dh, orig_w, orig_h)
|
| 477 |
-
|
| 478 |
-
def _decode(self, output: np.ndarray, ratio: float, dw: float, dh: float,
|
| 479 |
-
orig_w: int, orig_h: int) -> list[str]:
|
| 480 |
-
"""Decode + reverse-letterbox to original image coords."""
|
| 481 |
-
# NMS-baked: [N, 6] or [1, N, 6]
|
| 482 |
-
if output.ndim == 3 and output.shape[0] == 1 and output.shape[2] == 6:
|
| 483 |
-
preds = output[0]
|
| 484 |
-
elif output.ndim == 2 and output.shape[1] == 6:
|
| 485 |
-
preds = output
|
| 486 |
-
else:
|
| 487 |
-
return self._decode_raw(output, ratio, dw, dh, orig_w, orig_h)
|
| 488 |
-
|
| 489 |
-
boxes = preds[:, :4].astype(np.float32, copy=True)
|
| 490 |
-
scores = preds[:, 4].astype(np.float32)
|
| 491 |
-
cls = preds[:, 5].astype(np.int32)
|
| 492 |
-
|
| 493 |
-
keep = (scores >= self.conf_floor) & (boxes[:, 2] > boxes[:, 0]) & (boxes[:, 3] > boxes[:, 1])
|
| 494 |
-
boxes, scores, cls = boxes[keep], scores[keep], cls[keep]
|
| 495 |
-
if len(boxes) == 0:
|
| 496 |
-
return []
|
| 497 |
-
|
| 498 |
-
# reverse letterbox (input-letterboxed coords -> orig image coords)
|
| 499 |
-
boxes[:, [0, 2]] -= dw
|
| 500 |
-
boxes[:, [1, 3]] -= dh
|
| 501 |
-
boxes /= ratio
|
| 502 |
-
boxes[:, 0] = np.clip(boxes[:, 0], 0, orig_w - 1)
|
| 503 |
-
boxes[:, 1] = np.clip(boxes[:, 1], 0, orig_h - 1)
|
| 504 |
-
boxes[:, 2] = np.clip(boxes[:, 2], 0, orig_w - 1)
|
| 505 |
-
boxes[:, 3] = np.clip(boxes[:, 3], 0, orig_h - 1)
|
| 506 |
-
|
| 507 |
-
return self._to_yolo_rows(boxes, cls, orig_w, orig_h)
|
| 508 |
-
|
| 509 |
-
def _decode_raw(self, output: np.ndarray, ratio: float, dw: float, dh: float,
|
| 510 |
-
orig_w: int, orig_h: int) -> list[str]:
|
| 511 |
-
"""Fallback for raw [1, C, N] / [1, N, C] YOLO output (e.g. manak0 baseline)."""
|
| 512 |
-
if output.ndim != 3 or output.shape[0] != 1:
|
| 513 |
-
return []
|
| 514 |
-
p = output[0]
|
| 515 |
-
if p.shape[0] <= 16 and p.shape[1] > p.shape[0]:
|
| 516 |
-
p = p.T
|
| 517 |
-
if p.shape[1] < 5:
|
| 518 |
-
return []
|
| 519 |
-
boxes_xywh = p[:, :4].astype(np.float32)
|
| 520 |
-
cls_part = p[:, 4:].astype(np.float32)
|
| 521 |
-
cls = np.argmax(cls_part, axis=1).astype(np.int32)
|
| 522 |
-
scores = cls_part[np.arange(len(cls_part)), cls]
|
| 523 |
-
keep = scores >= self.conf_floor
|
| 524 |
-
if not np.any(keep):
|
| 525 |
-
return []
|
| 526 |
-
boxes_xywh = boxes_xywh[keep]
|
| 527 |
-
scores = scores[keep]
|
| 528 |
-
cls = cls[keep]
|
| 529 |
-
# xywh (input-letterboxed coords) -> xyxy (input coords)
|
| 530 |
-
xyxy = np.empty((len(boxes_xywh), 4), dtype=np.float32)
|
| 531 |
-
xyxy[:, 0] = boxes_xywh[:, 0] - boxes_xywh[:, 2] / 2.0
|
| 532 |
-
xyxy[:, 1] = boxes_xywh[:, 1] - boxes_xywh[:, 3] / 2.0
|
| 533 |
-
xyxy[:, 2] = boxes_xywh[:, 0] + boxes_xywh[:, 2] / 2.0
|
| 534 |
-
xyxy[:, 3] = boxes_xywh[:, 1] + boxes_xywh[:, 3] / 2.0
|
| 535 |
-
xyxy, scores, cls = self._nms(xyxy, scores, cls, self.iou_thresh)
|
| 536 |
-
if len(xyxy) == 0:
|
| 537 |
-
return []
|
| 538 |
-
# reverse letterbox
|
| 539 |
-
xyxy[:, [0, 2]] -= dw
|
| 540 |
-
xyxy[:, [1, 3]] -= dh
|
| 541 |
-
xyxy /= ratio
|
| 542 |
-
xyxy[:, 0] = np.clip(xyxy[:, 0], 0, orig_w - 1)
|
| 543 |
-
xyxy[:, 1] = np.clip(xyxy[:, 1], 0, orig_h - 1)
|
| 544 |
-
xyxy[:, 2] = np.clip(xyxy[:, 2], 0, orig_w - 1)
|
| 545 |
-
xyxy[:, 3] = np.clip(xyxy[:, 3], 0, orig_h - 1)
|
| 546 |
-
return self._to_yolo_rows(xyxy, cls, orig_w, orig_h)
|
| 547 |
-
|
| 548 |
-
@staticmethod
|
| 549 |
-
def _nms(boxes: np.ndarray, scores: np.ndarray, cls: np.ndarray,
|
| 550 |
-
iou_thresh: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 551 |
-
if len(boxes) == 0:
|
| 552 |
-
return boxes, scores, cls
|
| 553 |
-
order = np.argsort(-scores)
|
| 554 |
-
boxes = boxes[order]; scores = scores[order]; cls = cls[order]
|
| 555 |
-
keep = []
|
| 556 |
-
suppressed = np.zeros(len(boxes), dtype=bool)
|
| 557 |
-
for i in range(len(boxes)):
|
| 558 |
-
if suppressed[i]:
|
| 559 |
-
continue
|
| 560 |
-
keep.append(i)
|
| 561 |
-
xa = np.maximum(boxes[i, 0], boxes[i + 1:, 0])
|
| 562 |
-
ya = np.maximum(boxes[i, 1], boxes[i + 1:, 1])
|
| 563 |
-
xb = np.minimum(boxes[i, 2], boxes[i + 1:, 2])
|
| 564 |
-
yb = np.minimum(boxes[i, 3], boxes[i + 1:, 3])
|
| 565 |
-
inter = np.maximum(0.0, xb - xa) * np.maximum(0.0, yb - ya)
|
| 566 |
-
area_i = (boxes[i, 2] - boxes[i, 0]) * (boxes[i, 3] - boxes[i, 1])
|
| 567 |
-
area_r = (boxes[i + 1:, 2] - boxes[i + 1:, 0]) * (boxes[i + 1:, 3] - boxes[i + 1:, 1])
|
| 568 |
-
ious = inter / (area_i + area_r - inter + 1e-7)
|
| 569 |
-
for k, ok in enumerate(ious >= iou_thresh):
|
| 570 |
-
if ok:
|
| 571 |
-
suppressed[i + 1 + k] = True
|
| 572 |
-
return boxes[keep], scores[keep], cls[keep]
|
| 573 |
-
|
| 574 |
-
@staticmethod
|
| 575 |
-
def _to_yolo_rows(boxes: np.ndarray, cls: np.ndarray, w: int, h: int) -> list[str]:
|
| 576 |
-
# Remap king's class order -> our target order before emitting.
|
| 577 |
-
rows = []
|
| 578 |
-
for (x1, y1, x2, y2), c in zip(boxes, cls):
|
| 579 |
-
if c < 0 or c >= len(KING_TO_TARGET_REMAP):
|
| 580 |
-
continue
|
| 581 |
-
tgt = int(KING_TO_TARGET_REMAP[int(c)])
|
| 582 |
-
bw = float(x2 - x1)
|
| 583 |
-
bh = float(y2 - y1)
|
| 584 |
-
if bw <= 1 or bh <= 1:
|
| 585 |
-
continue
|
| 586 |
-
cx = (float(x1) + float(x2)) / 2.0 / w
|
| 587 |
-
cy = (float(y1) + float(y2)) / 2.0 / h
|
| 588 |
-
rows.append(f"{tgt} {cx:.6f} {cy:.6f} {bw / w:.6f} {bh / h:.6f}")
|
| 589 |
-
return rows
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
def king_preds_to_yolo(preds: dict, img_w: int, img_h: int) -> list[str]:
|
| 593 |
-
"""Manako preds use absolute pixel xyxy + cls_id (in alfred8995's class order).
|
| 594 |
-
Remap to our target order before emitting YOLO rows.
|
| 595 |
-
"""
|
| 596 |
-
rows: list[str] = []
|
| 597 |
-
for box in preds.get("boxes") or []:
|
| 598 |
-
try:
|
| 599 |
-
x1, y1, x2, y2 = float(box["x1"]), float(box["y1"]), float(box["x2"]), float(box["y2"])
|
| 600 |
-
c = int(box["cls_id"])
|
| 601 |
-
except Exception:
|
| 602 |
-
continue
|
| 603 |
-
if c < 0 or c >= len(KING_TO_TARGET_REMAP):
|
| 604 |
-
continue
|
| 605 |
-
tgt = int(KING_TO_TARGET_REMAP[c])
|
| 606 |
-
bw = x2 - x1
|
| 607 |
-
bh = y2 - y1
|
| 608 |
-
if bw <= 1 or bh <= 1:
|
| 609 |
-
continue
|
| 610 |
-
cx = (x1 + x2) / 2.0 / img_w
|
| 611 |
-
cy = (y1 + y2) / 2.0 / img_h
|
| 612 |
-
rows.append(f"{tgt} {cx:.6f} {cy:.6f} {bw / img_w:.6f} {bh / img_h:.6f}")
|
| 613 |
-
return rows
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
# --------------------------------------------------------------------- writer
|
| 617 |
-
|
| 618 |
-
def write_yolo_pair(out_root: Path, split: str, name: str, image_bytes: bytes,
|
| 619 |
-
label_lines: list[str], ext: str) -> None:
|
| 620 |
-
img_dir = out_root / "images" / split
|
| 621 |
-
lbl_dir = out_root / "labels" / split
|
| 622 |
-
img_dir.mkdir(parents=True, exist_ok=True)
|
| 623 |
-
lbl_dir.mkdir(parents=True, exist_ok=True)
|
| 624 |
-
(img_dir / f"{name}.{ext}").write_bytes(image_bytes)
|
| 625 |
-
(lbl_dir / f"{name}.txt").write_text("\n".join(label_lines), encoding="utf-8")
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
def write_data_yaml(out_root: Path) -> Path:
|
| 629 |
-
data_yaml = out_root / "data.yaml"
|
| 630 |
-
data_yaml.write_text(
|
| 631 |
-
"# YOLO dataset for manak0/Detect-crime\n"
|
| 632 |
-
f"path: {out_root.resolve()}\n"
|
| 633 |
-
"train: images/train\n"
|
| 634 |
-
"val: images/val\n"
|
| 635 |
-
"names:\n"
|
| 636 |
-
+ "".join(f" {i}: {n}\n" for i, n in enumerate(TARGET_CLASS_NAMES)),
|
| 637 |
-
encoding="utf-8",
|
| 638 |
-
)
|
| 639 |
-
return data_yaml
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
# --------------------------------------------------------------------- main
|
| 643 |
-
|
| 644 |
-
def main() -> int:
|
| 645 |
-
ap = argparse.ArgumentParser()
|
| 646 |
-
ap.add_argument("--out", required=True, help="dataset output root (YOLO format)")
|
| 647 |
-
ap.add_argument("--king-onnx", required=True,
|
| 648 |
-
help="path to king's ONNX (used as silver labeler for unlabeled frames)")
|
| 649 |
-
ap.add_argument("--manako", action="store_true", help="poll manako latestAnnotatedChallenge")
|
| 650 |
-
ap.add_argument("--manako-polls", type=int, default=1)
|
| 651 |
-
ap.add_argument("--manako-poll-delay", type=int, default=180)
|
| 652 |
-
ap.add_argument("--roboflow", action="append", default=[],
|
| 653 |
-
help="repeatable: TARGETCLASS=workspace/project[/version]; "
|
| 654 |
-
"e.g. balaclava=brainster/balaclava-detection-v3")
|
| 655 |
-
ap.add_argument("--coco-bat", nargs=2, metavar=("ANN_JSON", "IMG_DIR"),
|
| 656 |
-
help="paths to COCO instances_train2017.json and train2017/")
|
| 657 |
-
ap.add_argument("--extra-dir", type=Path, default=None,
|
| 658 |
-
help="optional dir of pre-collected images (auto-labeled by king)")
|
| 659 |
-
ap.add_argument("--hf-parquet-coco", action="append", default=[],
|
| 660 |
-
help="repeatable: TARGETCLASS=repo_id::filename (COCO-bbox parquet); "
|
| 661 |
-
"e.g. graffiti=junaidfayazlone/GraffitiDetection::Train-00000-of-00001-graffiti.parquet")
|
| 662 |
-
ap.add_argument("--hf-parquet-cls", action="append", default=[],
|
| 663 |
-
help="repeatable: TARGETCLASS=repo_id::filename (classification parquet, "
|
| 664 |
-
"king-labeled); e.g. balaclava=mlnomad/imnet1k_ski_mask::data/train-00000-of-00001.parquet")
|
| 665 |
-
ap.add_argument("--hf-limit", type=int, default=0,
|
| 666 |
-
help="cap rows pulled from each HF parquet (0 = all)")
|
| 667 |
-
ap.add_argument("--min-conf", type=float, default=0.15,
|
| 668 |
-
help="confidence floor for the silver labeler (lower = more labels, more noise)")
|
| 669 |
-
ap.add_argument("--workdir", type=Path, default=Path("/tmp/crime_dataset_work"))
|
| 670 |
-
ap.add_argument("--val-frac", type=float, default=0.10)
|
| 671 |
-
ap.add_argument("--seed", type=int, default=42)
|
| 672 |
-
ap.add_argument("--limit", type=int, default=0,
|
| 673 |
-
help="cap images per source (0 = no cap)")
|
| 674 |
-
ap.add_argument("--intra-threads", type=int, default=0)
|
| 675 |
-
ap.add_argument("--keep-empty", action="store_true",
|
| 676 |
-
help="keep images with no detected boxes (negative-bg supervision)")
|
| 677 |
-
args = ap.parse_args()
|
| 678 |
-
|
| 679 |
-
out_root = Path(args.out).resolve()
|
| 680 |
-
work = args.workdir.resolve()
|
| 681 |
-
work.mkdir(parents=True, exist_ok=True)
|
| 682 |
-
|
| 683 |
-
def log(msg: str) -> None:
|
| 684 |
-
print(msg, file=sys.stderr, flush=True)
|
| 685 |
-
|
| 686 |
-
# Sources collected as (path_or_bytes, src_tag, labels_or_preds_or_None).
|
| 687 |
-
# labels can be: list[str] YOLO rows (already mapped), dict (king preds), or None (needs labeling).
|
| 688 |
-
sources: list[tuple[Path, str, list[str] | dict | None]] = []
|
| 689 |
-
|
| 690 |
-
if args.manako:
|
| 691 |
-
for img_path, src, preds in fetch_manako(work, log,
|
| 692 |
-
polls=args.manako_polls,
|
| 693 |
-
delay_s=args.manako_poll_delay):
|
| 694 |
-
sources.append((img_path, src, preds))
|
| 695 |
-
|
| 696 |
-
for spec in args.roboflow:
|
| 697 |
-
if "=" not in spec:
|
| 698 |
-
log(f"[roboflow] malformed --roboflow {spec}; expected TARGETCLASS=slug")
|
| 699 |
-
continue
|
| 700 |
-
target, slug = spec.split("=", 1)
|
| 701 |
-
for img_path, src, labels in fetch_roboflow(slug.strip(), target.strip(), work, log):
|
| 702 |
-
sources.append((img_path, src, labels))
|
| 703 |
-
|
| 704 |
-
if args.coco_bat:
|
| 705 |
-
coco_ann, coco_img = Path(args.coco_bat[0]), Path(args.coco_bat[1])
|
| 706 |
-
for img_path, src, labels in fetch_coco_bat(coco_ann, coco_img, log):
|
| 707 |
-
sources.append((img_path, src, labels))
|
| 708 |
-
|
| 709 |
-
# HF parquet sources land as (bytes, src, labels). Persist to disk under work
|
| 710 |
-
# so the rest of the pipeline (which expects file paths) can pick them up.
|
| 711 |
-
hf_dir = work / "hf_images"
|
| 712 |
-
hf_dir.mkdir(parents=True, exist_ok=True)
|
| 713 |
-
|
| 714 |
-
for spec in args.hf_parquet_coco:
|
| 715 |
-
if "=" not in spec or "::" not in spec:
|
| 716 |
-
log(f"[hf-parquet-coco] malformed {spec}; expected TARGETCLASS=repo::filename")
|
| 717 |
-
continue
|
| 718 |
-
target, rest = spec.split("=", 1)
|
| 719 |
-
repo_id, filename = rest.split("::", 1)
|
| 720 |
-
for data, src, labels in fetch_hf_parquet_coco(repo_id.strip(), filename.strip(),
|
| 721 |
-
target.strip(), work, log,
|
| 722 |
-
limit=args.hf_limit):
|
| 723 |
-
digest = sha16(data)
|
| 724 |
-
ext = "jpg" if data[:2] == b"\xff\xd8" else "png"
|
| 725 |
-
p = hf_dir / f"{src}_{digest}.{ext}"
|
| 726 |
-
if not p.exists():
|
| 727 |
-
p.write_bytes(data)
|
| 728 |
-
sources.append((p, src, labels))
|
| 729 |
-
|
| 730 |
-
for spec in args.hf_parquet_cls:
|
| 731 |
-
if "=" not in spec or "::" not in spec:
|
| 732 |
-
log(f"[hf-parquet-cls] malformed {spec}; expected TARGETCLASS=repo::filename")
|
| 733 |
-
continue
|
| 734 |
-
target, rest = spec.split("=", 1)
|
| 735 |
-
repo_id, filename = rest.split("::", 1)
|
| 736 |
-
for data, src, _ in fetch_hf_parquet_cls(repo_id.strip(), filename.strip(),
|
| 737 |
-
target.strip(), work, log,
|
| 738 |
-
limit=args.hf_limit):
|
| 739 |
-
digest = sha16(data)
|
| 740 |
-
ext = "jpg" if data[:2] == b"\xff\xd8" else "png"
|
| 741 |
-
p = hf_dir / f"{src}_{digest}.{ext}"
|
| 742 |
-
if not p.exists():
|
| 743 |
-
p.write_bytes(data)
|
| 744 |
-
sources.append((p, src, None))
|
| 745 |
-
|
| 746 |
-
if args.extra_dir:
|
| 747 |
-
for img_path, src, _ in fetch_extra(args.extra_dir, log):
|
| 748 |
-
sources.append((img_path, src, None))
|
| 749 |
-
|
| 750 |
-
if not sources:
|
| 751 |
-
log("[!] no sources; pass at least --manako / --roboflow / --coco-bat / --extra-dir")
|
| 752 |
-
return 1
|
| 753 |
-
|
| 754 |
-
if args.limit:
|
| 755 |
-
sources = sources[: args.limit * 8] # rough cap; we'll dedupe below
|
| 756 |
-
log(f"[build] total candidate sources: {len(sources)}")
|
| 757 |
-
|
| 758 |
-
# Auto-labeler for sources that come unlabeled.
|
| 759 |
-
labeler = KingLabeler(Path(args.king_onnx), conf_floor=args.min_conf,
|
| 760 |
-
intra_threads=args.intra_threads)
|
| 761 |
-
log(f"[label] king onnx input={labeler.in_w}x{labeler.in_h} "
|
| 762 |
-
f"providers={labeler.session.get_providers()}")
|
| 763 |
-
|
| 764 |
-
samples: list[tuple[str, bytes, list[str], str]] = []
|
| 765 |
-
seen_hashes: set[str] = set()
|
| 766 |
-
n_labeled = 0
|
| 767 |
-
for i, (path, src, payload) in enumerate(sources):
|
| 768 |
-
try:
|
| 769 |
-
data = path.read_bytes()
|
| 770 |
-
except Exception as e:
|
| 771 |
-
log(f"[skip] read failed {path}: {e}")
|
| 772 |
-
continue
|
| 773 |
-
digest = sha16(data)
|
| 774 |
-
if digest in seen_hashes:
|
| 775 |
-
continue
|
| 776 |
-
seen_hashes.add(digest)
|
| 777 |
-
ext = "jpg" if data[:2] == b"\xff\xd8" else "png"
|
| 778 |
-
|
| 779 |
-
labels: list[str]
|
| 780 |
-
if isinstance(payload, list):
|
| 781 |
-
labels = payload # already remapped YOLO rows
|
| 782 |
-
elif isinstance(payload, dict):
|
| 783 |
-
try:
|
| 784 |
-
w, h = image_size(data)
|
| 785 |
-
labels = king_preds_to_yolo(payload, w, h)
|
| 786 |
-
except Exception as e:
|
| 787 |
-
log(f"[label] manako preds parse failed {path}: {e}; falling back to ONNX")
|
| 788 |
-
labels = labeler.label(data)
|
| 789 |
-
else:
|
| 790 |
-
try:
|
| 791 |
-
labels = labeler.label(data)
|
| 792 |
-
except Exception as e:
|
| 793 |
-
log(f"[label] ONNX labeling failed {path}: {e}")
|
| 794 |
-
continue
|
| 795 |
-
|
| 796 |
-
if not labels and not args.keep_empty:
|
| 797 |
-
continue
|
| 798 |
-
if labels:
|
| 799 |
-
n_labeled += 1
|
| 800 |
-
name = f"{src}_{digest}"
|
| 801 |
-
samples.append((name, data, labels, src))
|
| 802 |
-
if (i + 1) % 200 == 0:
|
| 803 |
-
log(f" labeled {i + 1}/{len(sources)} (kept {len(samples)})")
|
| 804 |
-
|
| 805 |
-
log(f"[build] labeled {n_labeled}/{len(sources)} unique images (others were empty)")
|
| 806 |
-
|
| 807 |
-
if not samples:
|
| 808 |
-
log("[!] zero labeled samples; lower --min-conf and retry")
|
| 809 |
-
return 1
|
| 810 |
-
|
| 811 |
-
rng = random.Random(args.seed)
|
| 812 |
-
manako_samples = [s for s in samples if s[3] == "manako"]
|
| 813 |
-
other_samples = [s for s in samples if s[3] != "manako"]
|
| 814 |
-
rng.shuffle(other_samples)
|
| 815 |
-
|
| 816 |
-
n_val_target = max(1, int(round(len(samples) * args.val_frac)))
|
| 817 |
-
val: list = list(manako_samples)
|
| 818 |
-
if len(val) < n_val_target:
|
| 819 |
-
val.extend(other_samples[: n_val_target - len(val)])
|
| 820 |
-
train = other_samples[n_val_target - len(manako_samples):]
|
| 821 |
-
else:
|
| 822 |
-
train = other_samples
|
| 823 |
-
log(f"[split] train={len(train)} val={len(val)} (manako-in-val={len(manako_samples)})")
|
| 824 |
-
|
| 825 |
-
if out_root.exists():
|
| 826 |
-
for sub in ("images/train", "images/val", "labels/train", "labels/val"):
|
| 827 |
-
d = out_root / sub
|
| 828 |
-
if d.exists():
|
| 829 |
-
shutil.rmtree(d)
|
| 830 |
-
out_root.mkdir(parents=True, exist_ok=True)
|
| 831 |
-
|
| 832 |
-
for name, data, labels, _src in train:
|
| 833 |
-
ext = "jpg" if data[:2] == b"\xff\xd8" else "png"
|
| 834 |
-
write_yolo_pair(out_root, "train", name, data, labels, ext)
|
| 835 |
-
for name, data, labels, _src in val:
|
| 836 |
-
ext = "jpg" if data[:2] == b"\xff\xd8" else "png"
|
| 837 |
-
write_yolo_pair(out_root, "val", name, data, labels, ext)
|
| 838 |
-
|
| 839 |
-
yaml_path = write_data_yaml(out_root)
|
| 840 |
-
log(f"[done] wrote dataset to {out_root}")
|
| 841 |
-
log(f" data.yaml: {yaml_path}")
|
| 842 |
-
log(f" train images: {len(train)}, val images: {len(val)}")
|
| 843 |
-
|
| 844 |
-
cls_train = [0] * len(TARGET_CLASS_NAMES)
|
| 845 |
-
cls_val = [0] * len(TARGET_CLASS_NAMES)
|
| 846 |
-
for _n, _b, labs, _s in train:
|
| 847 |
-
for line in labs:
|
| 848 |
-
cls_train[int(line.split()[0])] += 1
|
| 849 |
-
for _n, _b, labs, _s in val:
|
| 850 |
-
for line in labs:
|
| 851 |
-
cls_val[int(line.split()[0])] += 1
|
| 852 |
-
log("[stats] per-class instance counts")
|
| 853 |
-
for i, n in enumerate(TARGET_CLASS_NAMES):
|
| 854 |
-
log(f" {i} {n:13s} train={cls_train[i]:6d} val={cls_val[i]:6d}")
|
| 855 |
-
|
| 856 |
-
return 0
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
if __name__ == "__main__":
|
| 860 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/eval_mine_vs_king.py
DELETED
|
@@ -1,204 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Val-split mAP: your exported ONNX vs current king ONNX (alfred8995/crime001).
|
| 4 |
-
|
| 5 |
-
King ONNX uses semantic class ids in a different order than `scratch/data/data.yaml`.
|
| 6 |
-
Remap king outputs to dataset indices before TP/FP matching:
|
| 7 |
-
|
| 8 |
-
king id king name -> yaml id / name
|
| 9 |
-
0 balaclava -> 0 balaclava
|
| 10 |
-
1 hoodie -> 4 hoodie
|
| 11 |
-
2 glove -> 2 glove
|
| 12 |
-
3 wooden bat -> 1 bat
|
| 13 |
-
4 spray can -> 5 spray paint
|
| 14 |
-
5 graffiti -> 3 graffiti
|
| 15 |
-
|
| 16 |
-
Usage:
|
| 17 |
-
python3 eval_mine_vs_king.py --data /root/turbovision_crime/scratch/data/data.yaml \
|
| 18 |
-
--mine /root/turbovision_crime/scratch/crime_miner/weights.onnx \
|
| 19 |
-
--king /root/turbovision_crime/king_models/alfred8995_crime001/weights.onnx
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
from __future__ import annotations
|
| 23 |
-
|
| 24 |
-
import argparse
|
| 25 |
-
import csv
|
| 26 |
-
from datetime import datetime, timezone
|
| 27 |
-
from pathlib import Path
|
| 28 |
-
|
| 29 |
-
import torch
|
| 30 |
-
from ultralytics import YOLO
|
| 31 |
-
from ultralytics.models.yolo.detect import DetectionValidator
|
| 32 |
-
|
| 33 |
-
# king cls -> dataset cls (see module docstring)
|
| 34 |
-
KING_TO_DATASET = torch.tensor([0, 4, 2, 1, 5, 3])
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class KingRemappedValidator(DetectionValidator):
|
| 38 |
-
def postprocess(self, preds): # noqa: ANN001
|
| 39 |
-
outs = super().postprocess(preds)
|
| 40 |
-
for d in outs:
|
| 41 |
-
if d["cls"].numel() == 0:
|
| 42 |
-
continue
|
| 43 |
-
ci = d["cls"].long().clamp(0, 5).view(-1)
|
| 44 |
-
mapped = KING_TO_DATASET[ci.cpu()].to(device=d["cls"].device, dtype=d["cls"].dtype)
|
| 45 |
-
d["cls"] = mapped
|
| 46 |
-
return outs
|
| 47 |
-
|
| 48 |
-
def init_metrics(self, model): # noqa: ANN001
|
| 49 |
-
super().init_metrics(model)
|
| 50 |
-
# Confusion-matrix / prints use canonical names from dataset
|
| 51 |
-
names = {
|
| 52 |
-
0: "balaclava",
|
| 53 |
-
1: "bat",
|
| 54 |
-
2: "glove",
|
| 55 |
-
3: "graffiti",
|
| 56 |
-
4: "hoodie",
|
| 57 |
-
5: "spray paint",
|
| 58 |
-
}
|
| 59 |
-
self.names = names
|
| 60 |
-
self.nc = len(names)
|
| 61 |
-
self.metrics.names = names
|
| 62 |
-
try:
|
| 63 |
-
from ultralytics.utils.metrics import ConfusionMatrix
|
| 64 |
-
|
| 65 |
-
self.confusion_matrix = ConfusionMatrix(names=names, save_matches=self.args.plots and self.args.visualize)
|
| 66 |
-
except Exception:
|
| 67 |
-
pass
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def run_val(weights: Path, data: Path, imgsz: int, batch: int, device: str, validator_cls: type):
|
| 71 |
-
model = YOLO(str(weights))
|
| 72 |
-
return model.val(
|
| 73 |
-
data=str(data),
|
| 74 |
-
imgsz=imgsz,
|
| 75 |
-
batch=batch,
|
| 76 |
-
device=device,
|
| 77 |
-
plots=False,
|
| 78 |
-
verbose=False,
|
| 79 |
-
validator=validator_cls,
|
| 80 |
-
)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def maybe_log_ops_row(
|
| 84 |
-
csv_path: Path,
|
| 85 |
-
challenge_id: str,
|
| 86 |
-
scene: str,
|
| 87 |
-
mine: Path,
|
| 88 |
-
king: Path,
|
| 89 |
-
m_mine,
|
| 90 |
-
m_king,
|
| 91 |
-
) -> None:
|
| 92 |
-
fields = [
|
| 93 |
-
"timestamp_utc",
|
| 94 |
-
"challenge_id",
|
| 95 |
-
"scene",
|
| 96 |
-
"lighting",
|
| 97 |
-
"camera_angle",
|
| 98 |
-
"motion_blur",
|
| 99 |
-
"crowd_level",
|
| 100 |
-
"my_model_id",
|
| 101 |
-
"my_score",
|
| 102 |
-
"king_model_id",
|
| 103 |
-
"king_score",
|
| 104 |
-
"delta_score",
|
| 105 |
-
"my_pred_count",
|
| 106 |
-
"king_pred_count",
|
| 107 |
-
"my_latency_p50_ms",
|
| 108 |
-
"my_latency_p95_ms",
|
| 109 |
-
"my_latency_p99_ms",
|
| 110 |
-
"dropped_frames",
|
| 111 |
-
"timeout_count",
|
| 112 |
-
"balaclava_hits",
|
| 113 |
-
"bat_hits",
|
| 114 |
-
"glove_hits",
|
| 115 |
-
"graffiti_hits",
|
| 116 |
-
"hoodie_hits",
|
| 117 |
-
"spray_paint_hits",
|
| 118 |
-
"tag_list",
|
| 119 |
-
"data_source_mix",
|
| 120 |
-
"notes",
|
| 121 |
-
]
|
| 122 |
-
row = {
|
| 123 |
-
"timestamp_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
| 124 |
-
"challenge_id": challenge_id,
|
| 125 |
-
"scene": scene,
|
| 126 |
-
"lighting": "",
|
| 127 |
-
"camera_angle": "",
|
| 128 |
-
"motion_blur": "",
|
| 129 |
-
"crowd_level": "",
|
| 130 |
-
"my_model_id": mine.name,
|
| 131 |
-
"my_score": f"{m_mine.box.map50:.6f}",
|
| 132 |
-
"king_model_id": king.name,
|
| 133 |
-
"king_score": f"{m_king.box.map50:.6f}",
|
| 134 |
-
"delta_score": f"{(m_mine.box.map50 - m_king.box.map50):.6f}",
|
| 135 |
-
"my_pred_count": "",
|
| 136 |
-
"king_pred_count": "",
|
| 137 |
-
"my_latency_p50_ms": f"{m_mine.speed.get('inference', 0.0):.3f}",
|
| 138 |
-
"my_latency_p95_ms": "",
|
| 139 |
-
"my_latency_p99_ms": "",
|
| 140 |
-
"dropped_frames": "",
|
| 141 |
-
"timeout_count": "",
|
| 142 |
-
"balaclava_hits": "",
|
| 143 |
-
"bat_hits": "",
|
| 144 |
-
"glove_hits": "",
|
| 145 |
-
"graffiti_hits": "",
|
| 146 |
-
"hoodie_hits": "",
|
| 147 |
-
"spray_paint_hits": "",
|
| 148 |
-
"tag_list": "offline_val_split",
|
| 149 |
-
"data_source_mix": "",
|
| 150 |
-
"notes": "auto-log from eval_mine_vs_king.py; scores are mAP50 on local val split",
|
| 151 |
-
}
|
| 152 |
-
csv_path.parent.mkdir(parents=True, exist_ok=True)
|
| 153 |
-
new_file = not csv_path.exists()
|
| 154 |
-
with csv_path.open("a", newline="") as f:
|
| 155 |
-
writer = csv.DictWriter(f, fieldnames=fields)
|
| 156 |
-
if new_file:
|
| 157 |
-
writer.writeheader()
|
| 158 |
-
writer.writerow(row)
|
| 159 |
-
print(f"[ops-log] appended row -> {csv_path}", flush=True)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def main() -> int:
|
| 163 |
-
ap = argparse.ArgumentParser()
|
| 164 |
-
ap.add_argument("--data", required=True)
|
| 165 |
-
ap.add_argument("--mine", required=True)
|
| 166 |
-
ap.add_argument("--king", required=True)
|
| 167 |
-
ap.add_argument("--imgsz", type=int, default=1280)
|
| 168 |
-
ap.add_argument("--batch", type=int, default=8)
|
| 169 |
-
ap.add_argument("--device", default="0")
|
| 170 |
-
ap.add_argument("--log-csv", default="", help="optional path to detect_crime_ops challenge_log.csv")
|
| 171 |
-
ap.add_argument("--challenge-id", default="", help="optional challenge/eval id for ops logging")
|
| 172 |
-
ap.add_argument("--scene", default="offline_val_split", help="scene tag used when --log-csv is set")
|
| 173 |
-
args = ap.parse_args()
|
| 174 |
-
|
| 175 |
-
data = Path(args.data)
|
| 176 |
-
mine = Path(args.mine)
|
| 177 |
-
king = Path(args.king)
|
| 178 |
-
|
| 179 |
-
print("=== Yours (exported miner ONNX) ===", flush=True)
|
| 180 |
-
m_mine = run_val(mine, data, args.imgsz, args.batch, args.device, DetectionValidator)
|
| 181 |
-
print(f"images={m_mine.speed.get('images', '?')} "
|
| 182 |
-
f"mAP50={m_mine.box.map50:.5f} mAP50-95={m_mine.box.map:.5f} "
|
| 183 |
-
f"P={m_mine.box.mp:.5f} R={m_mine.box.mr:.5f}", flush=True)
|
| 184 |
-
|
| 185 |
-
print("\n=== King (alfred8995/crime001, cls remapped to data.yaml order) ===", flush=True)
|
| 186 |
-
m_king = run_val(king, data, args.imgsz, args.batch, args.device, KingRemappedValidator)
|
| 187 |
-
print(f"images={m_king.speed.get('images', '?')} "
|
| 188 |
-
f"mAP50={m_king.box.map50:.5f} mAP50-95={m_king.box.map:.5f} "
|
| 189 |
-
f"P={m_king.box.mp:.5f} R={m_king.box.mr:.5f}", flush=True)
|
| 190 |
-
|
| 191 |
-
print("\n=== Delta (yours − king) on same val split ===", flush=True)
|
| 192 |
-
print(f"d_mAP50 = {m_mine.box.map50 - m_king.box.map50:+.5f}", flush=True)
|
| 193 |
-
print(f"d_mAP50-95 = {m_mine.box.map - m_king.box.map:+.5f}", flush=True)
|
| 194 |
-
print("\nInterpretation:", flush=True)
|
| 195 |
-
print("- Same boxes (IoU/geometric inference) regardless of remap; remap only aligns class IDs for AP.", flush=True)
|
| 196 |
-
print("- This is YOUR silver-label val set, not the live Manako leaderboard.", flush=True)
|
| 197 |
-
if args.log_csv:
|
| 198 |
-
cid = args.challenge_id or f"offline_val_{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
|
| 199 |
-
maybe_log_ops_row(Path(args.log_csv), cid, args.scene, mine, king, m_mine, m_king)
|
| 200 |
-
return 0
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
if __name__ == "__main__":
|
| 204 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/export_onnx.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Export a trained YOLOv11 .pt to ONNX with NMS baked in -> output shape [1, max_det, 6].
|
| 3 |
-
|
| 4 |
-
The default chute miner.py decode path expects [1, N, 6] (NMS-baked). The king's own
|
| 5 |
-
weights.onnx is raw-YOLO [1, 10, 8400] without NMS — our exported model does NOT match
|
| 6 |
-
that, but our miner.py handles both formats. NMS-baked is preferred because:
|
| 7 |
-
- chute startup is faster (less graph work),
|
| 8 |
-
- the decode path is simpler (no fallback to raw YOLO numpy NMS),
|
| 9 |
-
- p95 latency is more stable (no Python-NMS spike on busy frames).
|
| 10 |
-
|
| 11 |
-
Usage:
|
| 12 |
-
python export_onnx.py --weights runs/detect/crime_b/weights/best.pt --imgsz 1280 \
|
| 13 |
-
--out ../weights.onnx
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
import argparse
|
| 19 |
-
import shutil
|
| 20 |
-
from pathlib import Path
|
| 21 |
-
|
| 22 |
-
from ultralytics import YOLO
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def main() -> int:
|
| 26 |
-
ap = argparse.ArgumentParser()
|
| 27 |
-
ap.add_argument("--weights", required=True)
|
| 28 |
-
ap.add_argument("--imgsz", type=int, default=1280)
|
| 29 |
-
ap.add_argument("--out", default=None,
|
| 30 |
-
help="destination path for weights.onnx (defaults next to source .pt)")
|
| 31 |
-
ap.add_argument("--max-det", type=int, default=300)
|
| 32 |
-
ap.add_argument("--opset", type=int, default=17)
|
| 33 |
-
ap.add_argument("--half", action="store_true",
|
| 34 |
-
help="export FP16 (smaller, faster on CUDA, marginal accuracy delta)")
|
| 35 |
-
args = ap.parse_args()
|
| 36 |
-
|
| 37 |
-
weights = Path(args.weights).resolve()
|
| 38 |
-
model = YOLO(str(weights))
|
| 39 |
-
|
| 40 |
-
onnx_path = model.export(
|
| 41 |
-
format="onnx",
|
| 42 |
-
imgsz=args.imgsz,
|
| 43 |
-
nms=True,
|
| 44 |
-
max_det=args.max_det,
|
| 45 |
-
opset=args.opset,
|
| 46 |
-
dynamic=False,
|
| 47 |
-
half=args.half,
|
| 48 |
-
simplify=True,
|
| 49 |
-
agnostic_nms=False, # class-aware NMS (must match miner.py expectations)
|
| 50 |
-
device="cpu",
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
src = Path(onnx_path)
|
| 54 |
-
if args.out:
|
| 55 |
-
dst = Path(args.out).resolve()
|
| 56 |
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 57 |
-
shutil.move(str(src), str(dst))
|
| 58 |
-
print(f"[export] {src} -> {dst}")
|
| 59 |
-
else:
|
| 60 |
-
print(f"[export] {src}")
|
| 61 |
-
return 0
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
if __name__ == "__main__":
|
| 65 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/milestones_watch.py
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Poll Ultralytics results.csv and append one summary line per N-epoch milestone."""
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import argparse
|
| 6 |
-
import csv
|
| 7 |
-
import time
|
| 8 |
-
from datetime import datetime, timezone
|
| 9 |
-
from pathlib import Path
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def fmt_row(row: dict) -> str:
|
| 13 |
-
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 14 |
-
ep = int(float(row["epoch"]))
|
| 15 |
-
return (
|
| 16 |
-
f"{ts} | epoch={ep} | "
|
| 17 |
-
f"mAP50={float(row['metrics/mAP50(B)']):.4f} mAP={float(row['metrics/mAP50-95(B)']):.4f} | "
|
| 18 |
-
f"P={float(row['metrics/precision(B)']):.4f} R={float(row['metrics/recall(B)']):.4f} | "
|
| 19 |
-
f"train_box={float(row['train/box_loss']):.4f} train_cls={float(row['train/cls_loss']):.4f}"
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def recover_done(out_path: Path) -> set[int]:
|
| 24 |
-
done: set[int] = set()
|
| 25 |
-
if not out_path.exists():
|
| 26 |
-
return set()
|
| 27 |
-
for line in out_path.read_text().splitlines():
|
| 28 |
-
if "| epoch=" not in line:
|
| 29 |
-
continue
|
| 30 |
-
try:
|
| 31 |
-
part = line.split("| epoch=")[1].split()[0]
|
| 32 |
-
done.add(int(part))
|
| 33 |
-
except (IndexError, ValueError):
|
| 34 |
-
continue
|
| 35 |
-
return done
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def main() -> int:
|
| 39 |
-
ap = argparse.ArgumentParser()
|
| 40 |
-
ap.add_argument("--csv", required=True, help="path to results.csv (created by Ultralytics)")
|
| 41 |
-
ap.add_argument("--every", type=int, default=10)
|
| 42 |
-
ap.add_argument("--max-epoch", type=int, default=200)
|
| 43 |
-
ap.add_argument("--out", default=None, help="append logs here (default: <run-dir>/milestones_every_N.log)")
|
| 44 |
-
ap.add_argument("--poll", type=float, default=15.0, help="seconds between reads")
|
| 45 |
-
ap.add_argument("--idle-exit-sec", type=float, default=180.0,
|
| 46 |
-
help="exit if results.csv is untouched this long and milestones caught up")
|
| 47 |
-
args = ap.parse_args()
|
| 48 |
-
|
| 49 |
-
csv_path = Path(args.csv)
|
| 50 |
-
run_dir = csv_path.parent
|
| 51 |
-
out_path = Path(args.out) if args.out else run_dir / f"milestones_every_{args.every}.log"
|
| 52 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 53 |
-
if not out_path.exists():
|
| 54 |
-
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
| 55 |
-
out_path.write_text(
|
| 56 |
-
f"{ts} | milestones_watch | status=started next_report=epoch_{args.every} csv={csv_path}\n"
|
| 57 |
-
)
|
| 58 |
-
print(f"[milestones_watch] created {out_path} (metric lines appear every {args.every} epochs)", flush=True)
|
| 59 |
-
done = recover_done(out_path)
|
| 60 |
-
|
| 61 |
-
while True:
|
| 62 |
-
if not csv_path.exists():
|
| 63 |
-
time.sleep(args.poll)
|
| 64 |
-
continue
|
| 65 |
-
try:
|
| 66 |
-
with csv_path.open(newline="") as f:
|
| 67 |
-
rows = list(csv.DictReader(f))
|
| 68 |
-
except Exception:
|
| 69 |
-
time.sleep(args.poll)
|
| 70 |
-
continue
|
| 71 |
-
if not rows:
|
| 72 |
-
time.sleep(args.poll)
|
| 73 |
-
continue
|
| 74 |
-
|
| 75 |
-
last_ep = int(float(rows[-1]["epoch"]))
|
| 76 |
-
for m in range(args.every, min(last_ep, args.max_epoch) + 1, args.every):
|
| 77 |
-
if m in done:
|
| 78 |
-
continue
|
| 79 |
-
match = [r for r in rows if int(float(r["epoch"])) == m]
|
| 80 |
-
if not match:
|
| 81 |
-
continue
|
| 82 |
-
line = fmt_row(match[0])
|
| 83 |
-
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 84 |
-
with out_path.open("a") as o:
|
| 85 |
-
o.write(line + "\n")
|
| 86 |
-
print(line, flush=True)
|
| 87 |
-
done.add(m)
|
| 88 |
-
|
| 89 |
-
mtime = csv_path.stat().st_mtime
|
| 90 |
-
idle = time.time() - mtime
|
| 91 |
-
max_reportable = (last_ep // args.every) * args.every
|
| 92 |
-
need = list(range(args.every, max_reportable + 1, args.every))
|
| 93 |
-
caught_up = all(m in done for m in need)
|
| 94 |
-
if idle >= args.idle_exit_sec and caught_up and last_ep >= 1:
|
| 95 |
-
print(f"milestones_watch: idle {idle:.0f}s and milestones caught up → exit", flush=True)
|
| 96 |
-
break
|
| 97 |
-
|
| 98 |
-
time.sleep(args.poll)
|
| 99 |
-
|
| 100 |
-
return 0
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
if __name__ == "__main__":
|
| 104 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/poll_manako.py
DELETED
|
@@ -1,132 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Continuous poller for in-domain frames from the manako/scoredata API for Detect-crime.
|
| 3 |
-
|
| 4 |
-
The console.scorevision.io endpoint surfaces ONE `latestAnnotatedChallenge` per element.
|
| 5 |
-
As the runner produces new challenges this single record rotates. By polling every couple
|
| 6 |
-
of minutes we accumulate distinct in-domain frames + the king's predictions for each.
|
| 7 |
-
|
| 8 |
-
Use this as a long-running collector alongside `build_dataset.py`. Run it for a few hours
|
| 9 |
-
or days, then re-run `build_dataset.py --extra-dir <out>/images` (or `--manako`) to fold
|
| 10 |
-
the new frames into the YOLO dataset.
|
| 11 |
-
|
| 12 |
-
Usage:
|
| 13 |
-
python poll_manako.py --out ./manako_pool --interval 120 --hours 8
|
| 14 |
-
python poll_manako.py --out ./manako_pool --interval 120 --forever # until killed
|
| 15 |
-
|
| 16 |
-
Output structure (each unique frame = one image + one preds JSON):
|
| 17 |
-
manako_pool/
|
| 18 |
-
images/manako_ch{cid}_f{fid}_{sha16}.png
|
| 19 |
-
preds/ manako_ch{cid}_f{fid}_{sha16}.json
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
from __future__ import annotations
|
| 23 |
-
|
| 24 |
-
import argparse
|
| 25 |
-
import hashlib
|
| 26 |
-
import json
|
| 27 |
-
import sys
|
| 28 |
-
import time
|
| 29 |
-
from pathlib import Path
|
| 30 |
-
from urllib.parse import quote
|
| 31 |
-
from urllib.request import Request, urlopen
|
| 32 |
-
|
| 33 |
-
API = "https://console.scorevision.io/api/v2"
|
| 34 |
-
ELEMENT_ID = "manak0/Detect-crime"
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def http_get(url: str, timeout: int = 30, retries: int = 3) -> bytes:
|
| 38 |
-
last = None
|
| 39 |
-
for i in range(retries):
|
| 40 |
-
try:
|
| 41 |
-
req = Request(url, headers={"User-Agent": "poll_manako/1.0"})
|
| 42 |
-
with urlopen(req, timeout=timeout) as r:
|
| 43 |
-
return r.read()
|
| 44 |
-
except Exception as e:
|
| 45 |
-
last = e
|
| 46 |
-
time.sleep(1.0 * (i + 1))
|
| 47 |
-
raise last # type: ignore
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def sha16(b: bytes) -> str:
|
| 51 |
-
return hashlib.sha256(b).hexdigest()[:16]
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def main() -> int:
|
| 55 |
-
ap = argparse.ArgumentParser()
|
| 56 |
-
ap.add_argument("--out", required=True, help="output dir for accumulated frames")
|
| 57 |
-
ap.add_argument("--interval", type=int, default=120, help="seconds between polls")
|
| 58 |
-
ap.add_argument("--hours", type=float, default=4.0, help="duration to run (ignored with --forever)")
|
| 59 |
-
ap.add_argument("--forever", action="store_true", help="run until killed")
|
| 60 |
-
args = ap.parse_args()
|
| 61 |
-
|
| 62 |
-
out = Path(args.out).resolve()
|
| 63 |
-
img_dir = out / "images"
|
| 64 |
-
pred_dir = out / "preds"
|
| 65 |
-
img_dir.mkdir(parents=True, exist_ok=True)
|
| 66 |
-
pred_dir.mkdir(parents=True, exist_ok=True)
|
| 67 |
-
|
| 68 |
-
deadline = time.time() + args.hours * 3600 if not args.forever else float("inf")
|
| 69 |
-
seen: set[str] = {p.stem.rsplit("_", 1)[-1] for p in img_dir.glob("manako_*.png")}
|
| 70 |
-
n_polls = 0
|
| 71 |
-
n_new = 0
|
| 72 |
-
print(f"[poll] starting; existing frames in pool: {len(seen)}", file=sys.stderr)
|
| 73 |
-
|
| 74 |
-
while time.time() < deadline:
|
| 75 |
-
n_polls += 1
|
| 76 |
-
try:
|
| 77 |
-
raw = http_get(f"{API}/elements/{quote(ELEMENT_ID, safe='')}?lookback_days=7")
|
| 78 |
-
except Exception as e:
|
| 79 |
-
print(f"[poll {n_polls}] API error: {e}", file=sys.stderr)
|
| 80 |
-
time.sleep(args.interval)
|
| 81 |
-
continue
|
| 82 |
-
|
| 83 |
-
elt = json.loads(raw)
|
| 84 |
-
lac = elt.get("latestAnnotatedChallenge")
|
| 85 |
-
if not lac:
|
| 86 |
-
print(f"[poll {n_polls}] no latestAnnotatedChallenge", file=sys.stderr)
|
| 87 |
-
time.sleep(args.interval)
|
| 88 |
-
continue
|
| 89 |
-
|
| 90 |
-
challenge_id = lac.get("challenge_id")
|
| 91 |
-
frames = (lac.get("response") or {}).get("frames") or []
|
| 92 |
-
preds = ((lac.get("response") or {}).get("predictions") or {}).get("frames") or []
|
| 93 |
-
preds_by_id = {p.get("frame_id"): p for p in preds}
|
| 94 |
-
|
| 95 |
-
added = 0
|
| 96 |
-
for f in frames:
|
| 97 |
-
url = f.get("url")
|
| 98 |
-
fid = f.get("frame_id")
|
| 99 |
-
if not url:
|
| 100 |
-
continue
|
| 101 |
-
try:
|
| 102 |
-
data = http_get(url)
|
| 103 |
-
except Exception as e:
|
| 104 |
-
print(f"[poll] image fetch failed {url}: {e}", file=sys.stderr)
|
| 105 |
-
continue
|
| 106 |
-
digest = sha16(data)
|
| 107 |
-
if digest in seen:
|
| 108 |
-
continue
|
| 109 |
-
seen.add(digest)
|
| 110 |
-
(img_dir / f"manako_ch{challenge_id}_f{fid}_{digest}.png").write_bytes(data)
|
| 111 |
-
preds_obj = preds_by_id.get(fid)
|
| 112 |
-
if preds_obj is not None:
|
| 113 |
-
(pred_dir / f"manako_ch{challenge_id}_f{fid}_{digest}.json").write_text(
|
| 114 |
-
json.dumps(preds_obj), encoding="utf-8"
|
| 115 |
-
)
|
| 116 |
-
added += 1
|
| 117 |
-
n_new += 1
|
| 118 |
-
|
| 119 |
-
print(f"[poll {n_polls}] cid={challenge_id} frames={len(frames)} new={added} "
|
| 120 |
-
f"total_unique={len(seen)} (cumulative new this run: {n_new})", file=sys.stderr)
|
| 121 |
-
|
| 122 |
-
if time.time() >= deadline:
|
| 123 |
-
break
|
| 124 |
-
time.sleep(args.interval)
|
| 125 |
-
|
| 126 |
-
print(f"[poll] done. polls={n_polls}, new frames added this run: {n_new}, "
|
| 127 |
-
f"pool size: {len(seen)}", file=sys.stderr)
|
| 128 |
-
return 0
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
if __name__ == "__main__":
|
| 132 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/requirements.txt
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
ultralytics>=8.3
|
| 2 |
-
torch>=2.1
|
| 3 |
-
torchvision>=0.16
|
| 4 |
-
onnx>=1.16
|
| 5 |
-
onnxruntime-gpu>=1.18
|
| 6 |
-
opencv-python>=4.7
|
| 7 |
-
numpy>=1.23
|
| 8 |
-
pillow>=9.5
|
| 9 |
-
pyyaml>=6.0
|
| 10 |
-
huggingface_hub>=0.24
|
| 11 |
-
roboflow>=1.1
|
| 12 |
-
pycocotools>=2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/train.py
DELETED
|
@@ -1,140 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Two-stage training recipe for Detect-crime.
|
| 3 |
-
|
| 4 |
-
Stage A (silver pretrain): warm a YOLOv11s/m on the full silver corpus
|
| 5 |
-
(Roboflow + COCO bat + king-distilled manako frames).
|
| 6 |
-
Stage B (clean fine-tune): short fine-tune on a manually-verified set of manako frames.
|
| 7 |
-
|
| 8 |
-
Defaults are tuned for a single GPU with >=24 GB VRAM. Pro_6000 should run YOLOv11s at
|
| 9 |
-
imgsz=1536 with batch 12 comfortably; YOLOv11m at imgsz=1280 needs batch ~8.
|
| 10 |
-
|
| 11 |
-
Usage:
|
| 12 |
-
# Stage A
|
| 13 |
-
python train.py --data ../data/data.yaml --weights yolo11s.pt --imgsz 1280 --stage A --epochs 200
|
| 14 |
-
|
| 15 |
-
# Stage B (clean fine-tune)
|
| 16 |
-
python train.py --data ../data_clean/data.yaml \
|
| 17 |
-
--weights runs/detect/crime_a/weights/best.pt \
|
| 18 |
-
--imgsz 1280 --stage B --epochs 50
|
| 19 |
-
|
| 20 |
-
Notes:
|
| 21 |
-
- Heavy mosaic+copy_paste is essential for crime — class imbalance is brutal (hoodie
|
| 22 |
-
dominates GT count; balaclava/glove/spray-paint are rare). Copy-paste pastes rare-class
|
| 23 |
-
crops into otherwise-normal frames; this is the most reliable fix for low recall on the
|
| 24 |
-
rare classes.
|
| 25 |
-
- HSV jitter cranked on V channel: CCTV crime footage spans daylight, IR night, and
|
| 26 |
-
mixed indoor lighting. Augment hard.
|
| 27 |
-
- `cls=1.0` (raised from default 0.5) because mAP@50 is per-class-averaged; pushing the
|
| 28 |
-
classification head matters more than localization here.
|
| 29 |
-
- Always save with ema=True so export uses smoothed weights.
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
from __future__ import annotations
|
| 33 |
-
|
| 34 |
-
import argparse
|
| 35 |
-
|
| 36 |
-
from ultralytics import YOLO
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
COMMON_AUG = dict(
|
| 40 |
-
hsv_h=0.02,
|
| 41 |
-
hsv_s=0.7,
|
| 42 |
-
hsv_v=0.5, # CCTV brightness varies massively (IR night, daylight, mixed indoor)
|
| 43 |
-
degrees=0.0, # CCTV cameras are mounted level
|
| 44 |
-
translate=0.10,
|
| 45 |
-
scale=0.5,
|
| 46 |
-
shear=0.0,
|
| 47 |
-
perspective=0.0,
|
| 48 |
-
flipud=0.0,
|
| 49 |
-
fliplr=0.5,
|
| 50 |
-
mosaic=1.0,
|
| 51 |
-
mixup=0.20,
|
| 52 |
-
copy_paste=0.50, # high: rebalance toward rare classes (balaclava, glove, spray paint)
|
| 53 |
-
erasing=0.0,
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
STAGE_A = dict(
|
| 57 |
-
epochs=200,
|
| 58 |
-
optimizer="AdamW",
|
| 59 |
-
lr0=2e-3,
|
| 60 |
-
lrf=0.01,
|
| 61 |
-
momentum=0.937,
|
| 62 |
-
weight_decay=5e-4,
|
| 63 |
-
warmup_epochs=3,
|
| 64 |
-
cos_lr=True,
|
| 65 |
-
label_smoothing=0.05,
|
| 66 |
-
cls=1.0, # higher cls weight: per-class AP matters
|
| 67 |
-
box=7.5,
|
| 68 |
-
dfl=1.5,
|
| 69 |
-
close_mosaic=15,
|
| 70 |
-
patience=40,
|
| 71 |
-
amp=True,
|
| 72 |
-
**COMMON_AUG,
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
STAGE_B = dict(
|
| 76 |
-
epochs=50,
|
| 77 |
-
optimizer="AdamW",
|
| 78 |
-
lr0=5e-4,
|
| 79 |
-
lrf=0.01,
|
| 80 |
-
momentum=0.9,
|
| 81 |
-
weight_decay=5e-4,
|
| 82 |
-
warmup_epochs=2,
|
| 83 |
-
cos_lr=True,
|
| 84 |
-
label_smoothing=0.05,
|
| 85 |
-
cls=0.8,
|
| 86 |
-
box=7.5,
|
| 87 |
-
dfl=1.5,
|
| 88 |
-
close_mosaic=10,
|
| 89 |
-
patience=15,
|
| 90 |
-
amp=True,
|
| 91 |
-
**{**COMMON_AUG, "mosaic": 0.5, "mixup": 0.0, "copy_paste": 0.10},
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def main() -> int:
|
| 96 |
-
ap = argparse.ArgumentParser()
|
| 97 |
-
ap.add_argument("--data", required=True, help="path to data.yaml (YOLO format)")
|
| 98 |
-
ap.add_argument("--weights", default="yolo11s.pt",
|
| 99 |
-
help="starting weights (yolo11n.pt / yolo11s.pt / yolo11m.pt / your stage-A best)")
|
| 100 |
-
ap.add_argument("--imgsz", type=int, default=1280)
|
| 101 |
-
ap.add_argument("--batch", type=int, default=16)
|
| 102 |
-
ap.add_argument("--stage", choices=["A", "B"], default="A")
|
| 103 |
-
ap.add_argument("--epochs", type=int, default=None)
|
| 104 |
-
ap.add_argument("--device", default=0)
|
| 105 |
-
ap.add_argument("--project", default="runs/detect")
|
| 106 |
-
ap.add_argument("--name", default=None)
|
| 107 |
-
ap.add_argument("--resume", action="store_true")
|
| 108 |
-
args = ap.parse_args()
|
| 109 |
-
|
| 110 |
-
cfg = STAGE_A.copy() if args.stage == "A" else STAGE_B.copy()
|
| 111 |
-
if args.epochs is not None:
|
| 112 |
-
cfg["epochs"] = args.epochs
|
| 113 |
-
|
| 114 |
-
model = YOLO(args.weights)
|
| 115 |
-
name = args.name or f"crime_{args.stage.lower()}"
|
| 116 |
-
|
| 117 |
-
model.train(
|
| 118 |
-
data=args.data,
|
| 119 |
-
imgsz=args.imgsz,
|
| 120 |
-
batch=args.batch,
|
| 121 |
-
device=args.device,
|
| 122 |
-
project=args.project,
|
| 123 |
-
name=name,
|
| 124 |
-
resume=args.resume,
|
| 125 |
-
save=True,
|
| 126 |
-
plots=True,
|
| 127 |
-
seed=42,
|
| 128 |
-
rect=False,
|
| 129 |
-
single_cls=False,
|
| 130 |
-
**cfg,
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
metrics = model.val(data=args.data, imgsz=args.imgsz, batch=args.batch,
|
| 134 |
-
device=args.device, plots=True, save_json=True)
|
| 135 |
-
print(f"[train] {args.stage} done. map50={metrics.box.map50:.4f} map={metrics.box.map:.4f}")
|
| 136 |
-
return 0
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
if __name__ == "__main__":
|
| 140 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
training/verify_dataset.py
DELETED
|
@@ -1,170 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Quick QA over an assembled YOLO dataset for Detect-crime.
|
| 3 |
-
|
| 4 |
-
Checks:
|
| 5 |
-
- Every label file has a corresponding image and vice versa.
|
| 6 |
-
- All class ids are within range (0..5).
|
| 7 |
-
- All bbox coords are normalized (0 < x < 1, 0 < y < 1, 0 < w/h <= 1).
|
| 8 |
-
- Reports per-class instance counts per split + total positive images per class.
|
| 9 |
-
- Flags samples with degenerate (near-zero) boxes.
|
| 10 |
-
- Optional: writes overlay PNGs for the first --visualize K val images.
|
| 11 |
-
|
| 12 |
-
Usage:
|
| 13 |
-
python verify_dataset.py --data ../data/data.yaml
|
| 14 |
-
python verify_dataset.py --data ../data/data.yaml --visualize 20 --vis-out /tmp/crime_vis
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import argparse
|
| 20 |
-
import sys
|
| 21 |
-
from pathlib import Path
|
| 22 |
-
|
| 23 |
-
import numpy as np
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
TARGET_CLASS_NAMES = ["balaclava", "bat", "glove", "graffiti", "hoodie", "spray paint"]
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def parse_yaml(path: Path) -> dict:
|
| 30 |
-
try:
|
| 31 |
-
import yaml
|
| 32 |
-
return yaml.safe_load(path.read_text())
|
| 33 |
-
except Exception:
|
| 34 |
-
cfg: dict = {}
|
| 35 |
-
for line in path.read_text().splitlines():
|
| 36 |
-
line = line.strip()
|
| 37 |
-
if not line or line.startswith("#") or ":" not in line:
|
| 38 |
-
continue
|
| 39 |
-
k, v = line.split(":", 1)
|
| 40 |
-
cfg[k.strip()] = v.strip()
|
| 41 |
-
return cfg
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def check_split(root: Path, split: str, log) -> dict:
|
| 45 |
-
img_dir = root / "images" / split
|
| 46 |
-
lbl_dir = root / "labels" / split
|
| 47 |
-
if not img_dir.exists():
|
| 48 |
-
log(f"[{split}] images/{split} missing: {img_dir}")
|
| 49 |
-
return {"images": 0}
|
| 50 |
-
if not lbl_dir.exists():
|
| 51 |
-
log(f"[{split}] labels/{split} missing: {lbl_dir}")
|
| 52 |
-
return {"images": 0}
|
| 53 |
-
|
| 54 |
-
image_names = {p.stem for p in img_dir.iterdir() if p.is_file()}
|
| 55 |
-
label_names = {p.stem for p in lbl_dir.iterdir() if p.is_file() and p.suffix == ".txt"}
|
| 56 |
-
only_img = image_names - label_names
|
| 57 |
-
only_lbl = label_names - image_names
|
| 58 |
-
|
| 59 |
-
n_total = len(image_names)
|
| 60 |
-
cls_counts = [0] * len(TARGET_CLASS_NAMES)
|
| 61 |
-
cls_image_counts = [set() for _ in range(len(TARGET_CLASS_NAMES))]
|
| 62 |
-
bad_rows = 0
|
| 63 |
-
degenerate = 0
|
| 64 |
-
for lbl in lbl_dir.glob("*.txt"):
|
| 65 |
-
rows = lbl.read_text().splitlines()
|
| 66 |
-
for r in rows:
|
| 67 |
-
parts = r.strip().split()
|
| 68 |
-
if len(parts) != 5:
|
| 69 |
-
bad_rows += 1
|
| 70 |
-
continue
|
| 71 |
-
try:
|
| 72 |
-
c = int(parts[0])
|
| 73 |
-
cx, cy, bw, bh = (float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]))
|
| 74 |
-
except Exception:
|
| 75 |
-
bad_rows += 1
|
| 76 |
-
continue
|
| 77 |
-
if not (0 <= c < len(TARGET_CLASS_NAMES)):
|
| 78 |
-
bad_rows += 1
|
| 79 |
-
continue
|
| 80 |
-
if not (0 < cx < 1 and 0 < cy < 1 and 0 < bw <= 1 and 0 < bh <= 1):
|
| 81 |
-
degenerate += 1
|
| 82 |
-
continue
|
| 83 |
-
cls_counts[c] += 1
|
| 84 |
-
cls_image_counts[c].add(lbl.stem)
|
| 85 |
-
|
| 86 |
-
return {
|
| 87 |
-
"images": n_total,
|
| 88 |
-
"only_img": only_img,
|
| 89 |
-
"only_lbl": only_lbl,
|
| 90 |
-
"cls_counts": cls_counts,
|
| 91 |
-
"cls_image_counts": [len(s) for s in cls_image_counts],
|
| 92 |
-
"bad_rows": bad_rows,
|
| 93 |
-
"degenerate": degenerate,
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def main() -> int:
|
| 98 |
-
ap = argparse.ArgumentParser()
|
| 99 |
-
ap.add_argument("--data", required=True)
|
| 100 |
-
ap.add_argument("--visualize", type=int, default=0,
|
| 101 |
-
help="render this many val images with boxes overlaid")
|
| 102 |
-
ap.add_argument("--vis-out", default="/tmp/crime_vis", help="dir for overlay images")
|
| 103 |
-
args = ap.parse_args()
|
| 104 |
-
|
| 105 |
-
data_yaml = Path(args.data).resolve()
|
| 106 |
-
cfg = parse_yaml(data_yaml)
|
| 107 |
-
root = Path(cfg.get("path") or data_yaml.parent).resolve()
|
| 108 |
-
|
| 109 |
-
def log(msg: str):
|
| 110 |
-
print(msg, file=sys.stderr, flush=True)
|
| 111 |
-
|
| 112 |
-
log(f"[verify] root={root}")
|
| 113 |
-
overall_ok = True
|
| 114 |
-
for split in ("train", "val"):
|
| 115 |
-
res = check_split(root, split, log)
|
| 116 |
-
if not res["images"]:
|
| 117 |
-
continue
|
| 118 |
-
log(f"[{split}] images: {res['images']}")
|
| 119 |
-
if res["only_img"]:
|
| 120 |
-
log(f"[{split}] {len(res['only_img'])} images without labels (sample: "
|
| 121 |
-
f"{list(res['only_img'])[:3]})")
|
| 122 |
-
if res["only_lbl"]:
|
| 123 |
-
log(f"[{split}] {len(res['only_lbl'])} labels without images (sample: "
|
| 124 |
-
f"{list(res['only_lbl'])[:3]})")
|
| 125 |
-
overall_ok = False
|
| 126 |
-
if res["bad_rows"]:
|
| 127 |
-
log(f"[{split}] BAD ROWS: {res['bad_rows']}"); overall_ok = False
|
| 128 |
-
if res["degenerate"]:
|
| 129 |
-
log(f"[{split}] degenerate boxes (out-of-range): {res['degenerate']}")
|
| 130 |
-
overall_ok = False
|
| 131 |
-
log(f"[{split}] per-class instance counts:")
|
| 132 |
-
for i, n in enumerate(TARGET_CLASS_NAMES):
|
| 133 |
-
log(f" {i} {n:13s} instances={res['cls_counts'][i]:6d} "
|
| 134 |
-
f"images={res['cls_image_counts'][i]:5d}")
|
| 135 |
-
|
| 136 |
-
if args.visualize > 0:
|
| 137 |
-
try:
|
| 138 |
-
import cv2
|
| 139 |
-
except ImportError:
|
| 140 |
-
log("[viz] cv2 not installed; pip install opencv-python")
|
| 141 |
-
return 0 if overall_ok else 1
|
| 142 |
-
out = Path(args.vis_out).resolve()
|
| 143 |
-
out.mkdir(parents=True, exist_ok=True)
|
| 144 |
-
val_imgs = sorted((root / "images" / "val").iterdir())[: args.visualize]
|
| 145 |
-
for img_path in val_imgs:
|
| 146 |
-
lbl_path = root / "labels" / "val" / (img_path.stem + ".txt")
|
| 147 |
-
img = cv2.imread(str(img_path))
|
| 148 |
-
if img is None:
|
| 149 |
-
continue
|
| 150 |
-
h, w = img.shape[:2]
|
| 151 |
-
if lbl_path.exists():
|
| 152 |
-
for r in lbl_path.read_text().splitlines():
|
| 153 |
-
p = r.split()
|
| 154 |
-
if len(p) != 5:
|
| 155 |
-
continue
|
| 156 |
-
c = int(p[0])
|
| 157 |
-
cx, cy, bw, bh = (float(p[1]) * w, float(p[2]) * h,
|
| 158 |
-
float(p[3]) * w, float(p[4]) * h)
|
| 159 |
-
x1, y1, x2, y2 = int(cx - bw / 2), int(cy - bh / 2), int(cx + bw / 2), int(cy + bh / 2)
|
| 160 |
-
cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 161 |
-
cv2.putText(img, TARGET_CLASS_NAMES[c], (x1, max(0, y1 - 6)),
|
| 162 |
-
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
|
| 163 |
-
cv2.imwrite(str(out / img_path.name), img)
|
| 164 |
-
log(f"[viz] wrote {len(val_imgs)} overlays to {out}")
|
| 165 |
-
|
| 166 |
-
return 0 if overall_ok else 1
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
if __name__ == "__main__":
|
| 170 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|