Spaces:
Running
Running
Fixed mismatch of the helmet
Browse files- app.py +1 -1
- ppe_compliance.py +40 -17
app.py
CHANGED
|
@@ -185,7 +185,7 @@ with gr.Blocks(title="Small Object Detection") as app:
|
|
| 185 |
ppe_threshold_slider = gr.Slider(
|
| 186 |
minimum=0.05,
|
| 187 |
maximum=0.95,
|
| 188 |
-
value=0.
|
| 189 |
step=0.05,
|
| 190 |
label="PPE detection threshold",
|
| 191 |
)
|
|
|
|
| 185 |
ppe_threshold_slider = gr.Slider(
|
| 186 |
minimum=0.05,
|
| 187 |
maximum=0.95,
|
| 188 |
+
value=0.25,
|
| 189 |
step=0.05,
|
| 190 |
label="PPE detection threshold",
|
| 191 |
)
|
ppe_compliance.py
CHANGED
|
@@ -34,6 +34,18 @@ DEVICE = "cpu"
|
|
| 34 |
|
| 35 |
ALL_PPE = ["goggles", "helmet", "mask", "shoes", "vest", "glove"]
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
# Person detector defaults to the same model the "Detect & Classify" tab uses.
|
| 38 |
DEFAULT_PERSON_MODEL = "medium-obj2coco"
|
| 39 |
|
|
@@ -119,7 +131,7 @@ def _color_of(name):
|
|
| 119 |
def run_ppe_compliance(
|
| 120 |
image,
|
| 121 |
person_threshold=0.75,
|
| 122 |
-
ppe_threshold=0.
|
| 123 |
assoc=0.5,
|
| 124 |
person_model=DEFAULT_PERSON_MODEL,
|
| 125 |
min_side=960,
|
|
@@ -151,17 +163,12 @@ def run_ppe_compliance(
|
|
| 151 |
persons.sort(key=lambda ps: -ps[0])
|
| 152 |
persons = persons[:MAX_PERSON_CROPS]
|
| 153 |
|
| 154 |
-
# 2
|
| 155 |
-
# (vests, distant helmets) isn't lost to the 640x640 full-frame resize
|
| 156 |
-
#
|
| 157 |
-
# whose crop produced it (gated by containment in their own box),
|
| 158 |
-
# rather than re-assigning globally to whichever overlapping neighbour contains
|
| 159 |
-
# it most — that global step mislabelled a worker's own vest to a denser
|
| 160 |
-
# neighbour in crowded frames, so the vest was drawn but the wearer showed
|
| 161 |
-
# "no vest". Boxes are mapped back to full coords; the draw list is de-duped.
|
| 162 |
people = [{"score": conf, "box": pb, "present": {}} for conf, pb in persons]
|
| 163 |
-
ppe = [] # (name, score, full_box)
|
| 164 |
-
for
|
| 165 |
x1, y1, x2, y2 = pb
|
| 166 |
pw, ph = x2 - x1, y2 - y1
|
| 167 |
cx1 = max(0, int(x1 - PERSON_CROP_PAD * pw))
|
|
@@ -172,15 +179,31 @@ def run_ppe_compliance(
|
|
| 172 |
continue
|
| 173 |
crop = im.crop((cx1, cy1, cx2, cy2))
|
| 174 |
for name, s, b in detect_ppe_boxes(crop, threshold=ppe_threshold):
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
if _contain_frac(box, pb) < assoc:
|
| 179 |
continue
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
if cur is None or s > cur[0]:
|
| 182 |
-
people[
|
| 183 |
-
ppe = _dedup_ppe(ppe)
|
| 184 |
|
| 185 |
# 4) Verdicts + report.
|
| 186 |
verdicts = []
|
|
|
|
| 34 |
|
| 35 |
ALL_PPE = ["goggles", "helmet", "mask", "shoes", "vest", "glove"]
|
| 36 |
|
| 37 |
+
# Expected vertical position of each item within its wearer's box (0 = top of the
|
| 38 |
+
# person box, 1 = bottom). Used ONLY to disambiguate which of several overlapping
|
| 39 |
+
# persons an item belongs to — never to reject an item outright.
|
| 40 |
+
PPE_VFRAC = {
|
| 41 |
+
"helmet": 0.10,
|
| 42 |
+
"goggles": 0.13,
|
| 43 |
+
"mask": 0.20,
|
| 44 |
+
"vest": 0.45,
|
| 45 |
+
"glove": 0.62,
|
| 46 |
+
"shoes": 0.92,
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
# Person detector defaults to the same model the "Detect & Classify" tab uses.
|
| 50 |
DEFAULT_PERSON_MODEL = "medium-obj2coco"
|
| 51 |
|
|
|
|
| 131 |
def run_ppe_compliance(
|
| 132 |
image,
|
| 133 |
person_threshold=0.75,
|
| 134 |
+
ppe_threshold=0.25,
|
| 135 |
assoc=0.5,
|
| 136 |
person_model=DEFAULT_PERSON_MODEL,
|
| 137 |
min_side=960,
|
|
|
|
| 163 |
persons.sort(key=lambda ps: -ps[0])
|
| 164 |
persons = persons[:MAX_PERSON_CROPS]
|
| 165 |
|
| 166 |
+
# 2) PPE detector (fine-tuned D-FINE-M), run on EACH person crop so small PPE
|
| 167 |
+
# (vests, distant helmets) isn't lost to the 640x640 full-frame resize. Boxes
|
| 168 |
+
# are mapped back to full-image coords; the union is de-duplicated across crops.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
people = [{"score": conf, "box": pb, "present": {}} for conf, pb in persons]
|
| 170 |
+
ppe = [] # (name, score, full_box)
|
| 171 |
+
for conf, pb in persons:
|
| 172 |
x1, y1, x2, y2 = pb
|
| 173 |
pw, ph = x2 - x1, y2 - y1
|
| 174 |
cx1 = max(0, int(x1 - PERSON_CROP_PAD * pw))
|
|
|
|
| 179 |
continue
|
| 180 |
crop = im.crop((cx1, cy1, cx2, cy2))
|
| 181 |
for name, s, b in detect_ppe_boxes(crop, threshold=ppe_threshold):
|
| 182 |
+
ppe.append((name, s, [b[0] + cx1, b[1] + cy1, b[2] + cx1, b[3] + cy1]))
|
| 183 |
+
ppe = _dedup_ppe(ppe)
|
| 184 |
+
|
| 185 |
+
# 3) Attribute each PPE item to EXACTLY ONE person. Among the persons whose box
|
| 186 |
+
# contains the item (>= assoc), pick the one where it sits in the anatomically
|
| 187 |
+
# expected place (helmet near the top, shoes near the bottom, ...). This stops a
|
| 188 |
+
# helmet that lies in the MIDDLE of a tall overlapping neighbour from being
|
| 189 |
+
# credited to them when it's really at the TOP (head) of the person beside them.
|
| 190 |
+
# It's a tie-break among containers, not a hard reject, so a uniquely-contained
|
| 191 |
+
# item is still credited regardless of pose (no false "missing" on bent workers).
|
| 192 |
+
for name, s, box in ppe:
|
| 193 |
+
tgt = PPE_VFRAC.get(name, 0.5)
|
| 194 |
+
cy = 0.5 * (box[1] + box[3])
|
| 195 |
+
best_i, best_d = -1, None
|
| 196 |
+
for i, p in enumerate(people):
|
| 197 |
+
pb = p["box"]
|
| 198 |
if _contain_frac(box, pb) < assoc:
|
| 199 |
continue
|
| 200 |
+
d = abs((cy - pb[1]) / max(1e-6, pb[3] - pb[1]) - tgt)
|
| 201 |
+
if best_d is None or d < best_d:
|
| 202 |
+
best_i, best_d = i, d
|
| 203 |
+
if best_i >= 0:
|
| 204 |
+
cur = people[best_i]["present"].get(name)
|
| 205 |
if cur is None or s > cur[0]:
|
| 206 |
+
people[best_i]["present"][name] = (s, box)
|
|
|
|
| 207 |
|
| 208 |
# 4) Verdicts + report.
|
| 209 |
verdicts = []
|