Chris Leo commited on
Commit
e56263b
·
verified ·
1 Parent(s): 190efc0

scorevision: push artifact

Browse files
Files changed (1) hide show
  1. training/verify_dataset.py +170 -0
training/verify_dataset.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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())