Spaces:
Sleeping
Sleeping
Adjusted thresholds and view
Browse files- app.py +11 -4
- ppe_compliance.py +92 -64
app.py
CHANGED
|
@@ -177,7 +177,7 @@ with gr.Blocks(title="Small Object Detection") as app:
|
|
| 177 |
person_threshold_slider = gr.Slider(
|
| 178 |
minimum=0.05,
|
| 179 |
maximum=0.95,
|
| 180 |
-
value=0.
|
| 181 |
step=0.05,
|
| 182 |
label="Person detection threshold",
|
| 183 |
)
|
|
@@ -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 |
)
|
|
@@ -198,7 +198,7 @@ with gr.Blocks(title="Small Object Detection") as app:
|
|
| 198 |
with gr.Column(scale=1):
|
| 199 |
|
| 200 |
out_ppe_image = gr.Image(
|
| 201 |
-
label="
|
| 202 |
height=IMG_HEIGHT,
|
| 203 |
)
|
| 204 |
|
|
@@ -208,6 +208,13 @@ with gr.Blocks(title="Small Object Detection") as app:
|
|
| 208 |
interactive=False,
|
| 209 |
)
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
btn_ppe.click(
|
| 212 |
fn=lambda image, p_thr, ppe_thr: run_ppe_compliance(
|
| 213 |
image,
|
|
@@ -215,7 +222,7 @@ with gr.Blocks(title="Small Object Detection") as app:
|
|
| 215 |
ppe_threshold=float(ppe_thr),
|
| 216 |
),
|
| 217 |
inputs=[inp_ppe, person_threshold_slider, ppe_threshold_slider],
|
| 218 |
-
outputs=[out_ppe_image, out_ppe_status],
|
| 219 |
concurrency_limit=1,
|
| 220 |
)
|
| 221 |
|
|
|
|
| 177 |
person_threshold_slider = gr.Slider(
|
| 178 |
minimum=0.05,
|
| 179 |
maximum=0.95,
|
| 180 |
+
value=0.75,
|
| 181 |
step=0.05,
|
| 182 |
label="Person detection threshold",
|
| 183 |
)
|
|
|
|
| 185 |
ppe_threshold_slider = gr.Slider(
|
| 186 |
minimum=0.05,
|
| 187 |
maximum=0.95,
|
| 188 |
+
value=0.3,
|
| 189 |
step=0.05,
|
| 190 |
label="PPE detection threshold",
|
| 191 |
)
|
|
|
|
| 198 |
with gr.Column(scale=1):
|
| 199 |
|
| 200 |
out_ppe_image = gr.Image(
|
| 201 |
+
label="Overview (green = OK, red = violation; #N per person)",
|
| 202 |
height=IMG_HEIGHT,
|
| 203 |
)
|
| 204 |
|
|
|
|
| 208 |
interactive=False,
|
| 209 |
)
|
| 210 |
|
| 211 |
+
out_ppe_gallery = gr.Gallery(
|
| 212 |
+
label="Per-person breakdown (click a person — PPE worn + checklist: 1 = present, 0 = missing)",
|
| 213 |
+
columns=3,
|
| 214 |
+
height=IMG_HEIGHT,
|
| 215 |
+
object_fit="contain",
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
btn_ppe.click(
|
| 219 |
fn=lambda image, p_thr, ppe_thr: run_ppe_compliance(
|
| 220 |
image,
|
|
|
|
| 222 |
ppe_threshold=float(ppe_thr),
|
| 223 |
),
|
| 224 |
inputs=[inp_ppe, person_threshold_slider, ppe_threshold_slider],
|
| 225 |
+
outputs=[out_ppe_image, out_ppe_gallery, out_ppe_status],
|
| 226 |
concurrency_limit=1,
|
| 227 |
)
|
| 228 |
|
ppe_compliance.py
CHANGED
|
@@ -118,18 +118,21 @@ def _color_of(name):
|
|
| 118 |
@torch.no_grad()
|
| 119 |
def run_ppe_compliance(
|
| 120 |
image,
|
| 121 |
-
person_threshold=0.
|
| 122 |
-
ppe_threshold=0.
|
| 123 |
assoc=0.5,
|
| 124 |
person_model=DEFAULT_PERSON_MODEL,
|
| 125 |
min_side=960,
|
| 126 |
):
|
| 127 |
"""Detect persons + PPE and flag per-person missing PPE (all 6 required).
|
| 128 |
|
| 129 |
-
Returns ``(
|
|
|
|
|
|
|
|
|
|
| 130 |
"""
|
| 131 |
if image is None:
|
| 132 |
-
return None, "Upload an image."
|
| 133 |
|
| 134 |
required = list(ALL_PPE) # all 6 items are required
|
| 135 |
|
|
@@ -193,68 +196,93 @@ def run_ppe_compliance(
|
|
| 193 |
if not people:
|
| 194 |
lines.append("No persons detected — lower the person threshold or try another image.")
|
| 195 |
|
| 196 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
scale = max(1.0, min_side / max(im.size))
|
|
|
|
|
|
|
| 198 |
if scale > 1.0:
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
#
|
| 207 |
-
|
| 208 |
-
W, H = im.size
|
| 209 |
-
base = im.convert("RGBA")
|
| 210 |
-
ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
| 211 |
-
od = ImageDraw.Draw(ov)
|
| 212 |
-
rf = _load_font(max(12, int(0.016 * max(W, H)))) # checklist rows
|
| 213 |
-
GREEN, RED = (40, 200, 90), (235, 45, 55)
|
| 214 |
-
placed = [] # occupied panel rects, to avoid overlaps
|
| 215 |
-
|
| 216 |
-
def overlaps(r):
|
| 217 |
-
return any(not (r[2] <= q[0] or r[0] >= q[2] or r[3] <= q[1] or r[1] >= q[3]) for q in placed)
|
| 218 |
-
|
| 219 |
-
# thin PPE boxes (no inline text — presence is shown in the checklist)
|
| 220 |
for name, s, box in ppe:
|
| 221 |
-
|
|
|
|
| 222 |
|
| 223 |
-
|
| 224 |
-
pad = 5
|
| 225 |
-
line_h = rf.getbbox("Hg1")[3] + 4
|
| 226 |
-
for i, p, present, missing, ok in verdicts:
|
| 227 |
col = GREEN if ok else RED
|
| 228 |
-
x1, y1, x2, y2 = [int(v) for v in
|
| 229 |
-
od.rectangle([x1, y1, x2, y2], outline=col
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
@torch.no_grad()
|
| 119 |
def run_ppe_compliance(
|
| 120 |
image,
|
| 121 |
+
person_threshold=0.75,
|
| 122 |
+
ppe_threshold=0.3,
|
| 123 |
assoc=0.5,
|
| 124 |
person_model=DEFAULT_PERSON_MODEL,
|
| 125 |
min_side=960,
|
| 126 |
):
|
| 127 |
"""Detect persons + PPE and flag per-person missing PPE (all 6 required).
|
| 128 |
|
| 129 |
+
Returns ``(overview_PIL, person_cards, status_text)`` where ``overview_PIL``
|
| 130 |
+
shows only the numbered person boxes (green=OK / red=violation), and
|
| 131 |
+
``person_cards`` is a list of ``(crop_with_checklist_PIL, caption)`` for a
|
| 132 |
+
per-person gallery.
|
| 133 |
"""
|
| 134 |
if image is None:
|
| 135 |
+
return None, [], "Upload an image."
|
| 136 |
|
| 137 |
required = list(ALL_PPE) # all 6 items are required
|
| 138 |
|
|
|
|
| 196 |
if not people:
|
| 197 |
lines.append("No persons detected — lower the person threshold or try another image.")
|
| 198 |
|
| 199 |
+
# 5) Build per-person CARDS first (crop + PPE boxes + checklist beside it),
|
| 200 |
+
# off the original image, before the overview is upscaled.
|
| 201 |
+
cards = [_person_card(im, i, p, ok) for i, p, present, missing, ok in verdicts]
|
| 202 |
+
|
| 203 |
+
# 6) Overview image: bounding boxes for detected objects — thin coloured boxes
|
| 204 |
+
# for the PPE items, plus the numbered person boxes (green = OK, red =
|
| 205 |
+
# violation). No checklist text on top; that 1/0 detail lives in the cards.
|
| 206 |
scale = max(1.0, min_side / max(im.size))
|
| 207 |
+
overview = im
|
| 208 |
+
boxes = [(i, [v * scale for v in p["box"]], ok) for i, p, present, missing, ok in verdicts]
|
| 209 |
if scale > 1.0:
|
| 210 |
+
overview = im.resize((round(im.size[0] * scale), round(im.size[1] * scale)), Image.LANCZOS)
|
| 211 |
+
overview = overview.copy()
|
| 212 |
+
od = ImageDraw.Draw(overview)
|
| 213 |
+
OW, OH = overview.size
|
| 214 |
+
nf = _load_font(max(16, int(0.030 * max(OW, OH))))
|
| 215 |
+
pw = max(3, int(0.006 * max(OW, OH)))
|
| 216 |
+
|
| 217 |
+
# PPE item boxes first (thin), so the thicker person boxes sit on top.
|
| 218 |
+
ppe_w = max(2, int(0.004 * max(OW, OH)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
for name, s, box in ppe:
|
| 220 |
+
b = [int(v * scale) for v in box]
|
| 221 |
+
od.rectangle(b, outline=_color_of(name), width=ppe_w)
|
| 222 |
|
| 223 |
+
for i, box, ok in boxes:
|
|
|
|
|
|
|
|
|
|
| 224 |
col = GREEN if ok else RED
|
| 225 |
+
x1, y1, x2, y2 = [int(v) for v in box]
|
| 226 |
+
od.rectangle([x1, y1, x2, y2], outline=col, width=pw)
|
| 227 |
+
tag = f"#{i}"
|
| 228 |
+
tb = od.textbbox((0, 0), tag, font=nf)
|
| 229 |
+
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
| 230 |
+
ty = max(0, y1 - th - 6)
|
| 231 |
+
od.rectangle([x1, ty, x1 + tw + 8, ty + th + 6], fill=col)
|
| 232 |
+
od.text((x1 + 4, ty + 2), tag, font=nf, fill=(255, 255, 255))
|
| 233 |
+
|
| 234 |
+
return overview, cards, "\n".join(lines)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
GREEN, RED = (40, 200, 90), (235, 45, 55)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _person_card(im, idx, person, ok, pad=0.12):
|
| 241 |
+
"""One person's crop with PPE boxes drawn, plus a checklist column beside it
|
| 242 |
+
(item 1 = present / 0 = missing). Returns ``(PIL_card, caption)`` for a gallery.
|
| 243 |
+
"""
|
| 244 |
+
W, H = im.size
|
| 245 |
+
x1, y1, x2, y2 = person["box"]
|
| 246 |
+
bw, bh = x2 - x1, y2 - y1
|
| 247 |
+
cx1 = max(0, int(x1 - pad * bw)); cy1 = max(0, int(y1 - pad * bh))
|
| 248 |
+
cx2 = min(W, int(x2 + pad * bw)); cy2 = min(H, int(y2 + pad * bh))
|
| 249 |
+
crop = im.crop((cx1, cy1, cx2, cy2)).convert("RGB")
|
| 250 |
+
cw, ch = crop.size
|
| 251 |
+
|
| 252 |
+
# Draw the present items' boxes on the crop (with class colour + score).
|
| 253 |
+
cd = ImageDraw.Draw(crop)
|
| 254 |
+
cf = _load_font(max(12, int(0.045 * max(cw, ch))))
|
| 255 |
+
for name, (s, box) in person["present"].items():
|
| 256 |
+
b = [box[0] - cx1, box[1] - cy1, box[2] - cx1, box[3] - cy1]
|
| 257 |
+
col = _color_of(name)
|
| 258 |
+
cd.rectangle(b, outline=col, width=max(2, int(0.006 * max(cw, ch))))
|
| 259 |
+
t = f"{name} {s:.2f}"
|
| 260 |
+
tb = cd.textbbox((0, 0), t, font=cf)
|
| 261 |
+
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
| 262 |
+
ty = max(0, b[1] - th - 3)
|
| 263 |
+
cd.rectangle([b[0], ty, b[0] + tw + 5, ty + th + 3], fill=col)
|
| 264 |
+
cd.text((b[0] + 2, ty + 1), t, font=cf, fill=(255, 255, 255))
|
| 265 |
+
|
| 266 |
+
# Build the checklist panel to the RIGHT of the crop.
|
| 267 |
+
present = set(person["present"])
|
| 268 |
+
pf = _load_font(max(14, int(ch / 14)))
|
| 269 |
+
line_h = pf.getbbox("Hg1")[3] + 6
|
| 270 |
+
rows = [(f"#{idx} {'OK' if ok else 'VIOLATION'}", None)] + [(it, it in present) for it in ALL_PPE]
|
| 271 |
+
txts = [lab if pres is None else f"{lab} {1 if pres else 0}" for lab, pres in rows]
|
| 272 |
+
tmp = ImageDraw.Draw(crop)
|
| 273 |
+
panel_pad = 12
|
| 274 |
+
panel_w = max(tmp.textbbox((0, 0), t, font=pf)[2] for t in txts) + 2 * panel_pad
|
| 275 |
+
panel_h = line_h * len(rows) + 2 * panel_pad
|
| 276 |
+
|
| 277 |
+
card_h = max(ch, panel_h)
|
| 278 |
+
card = Image.new("RGB", (cw + panel_w, card_h), (245, 245, 245))
|
| 279 |
+
card.paste(crop, (0, (card_h - ch) // 2))
|
| 280 |
+
pd = ImageDraw.Draw(card)
|
| 281 |
+
x0 = cw + panel_pad
|
| 282 |
+
y0 = (card_h - panel_h) // 2 + panel_pad
|
| 283 |
+
for k, (lab, pres) in enumerate(rows):
|
| 284 |
+
col = (GREEN if ok else RED) if pres is None else (GREEN if pres else RED)
|
| 285 |
+
pd.text((x0, y0 + k * line_h), txts[k], font=pf, fill=col)
|
| 286 |
+
|
| 287 |
+
caption = f"#{idx} — " + ("OK" if ok else "VIOLATION: no " + ", ".join(c for c in ALL_PPE if c not in present))
|
| 288 |
+
return (card, caption)
|