orik-ss commited on
Commit
9bddbe9
·
1 Parent(s): f560a14

Adjusted thresholds and view

Browse files
Files changed (2) hide show
  1. app.py +11 -4
  2. 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.5,
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.4,
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="Compliance (green = OK, red = missing PPE)",
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.5,
122
- ppe_threshold=0.4,
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 ``(annotated_PIL, status_text)``.
 
 
 
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
- # Upscale small images so labels/boxes render crisply.
 
 
 
 
 
 
197
  scale = max(1.0, min_side / max(im.size))
 
 
198
  if scale > 1.0:
199
- im = im.resize((round(im.size[0] * scale), round(im.size[1] * scale)), Image.LANCZOS)
200
- for p in people:
201
- p["box"] = [v * scale for v in p["box"]]
202
- ppe = [(n, s, [v * scale for v in b]) for (n, s, b) in ppe]
203
-
204
- # 5) Render: thin PPE boxes + a compact per-person CHECKLIST panel placed
205
- # OUTSIDE the person box. One short row per required item ("vest 1" present /
206
- # "vest 0" missing, green/red), stacked in a column instead of one long
207
- # "no goggles, mask, shoes, glove" sentence that collides in crowded frames.
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
- od.rectangle(box, outline=_color_of(name) + (255,), width=2)
 
222
 
223
- pw = max(3, int(0.006 * max(W, H)))
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 p["box"]]
229
- od.rectangle([x1, y1, x2, y2], outline=col + (255,), width=pw)
230
-
231
- rows = [(f"#{i}", None)] + [(it, it in present) for it in ALL_PPE]
232
- txts = [lab if pres is None else f"{lab} {1 if pres else 0}" for lab, pres in rows]
233
- p_w = max(od.textbbox((0, 0), t, font=rf)[2] for t in txts) + 2 * pad
234
- p_h = line_h * len(rows) + 2 * pad
235
-
236
- # Prefer just right of the box; fall back to the left; keep inside frame.
237
- rx = x2 + 6
238
- if rx + p_w > W:
239
- rx = x1 - p_w - 6
240
- if rx < 0:
241
- rx = min(x1 + 6, max(0, W - p_w))
242
- ry = max(0, min(y1, H - p_h))
243
- for _ in range(300): # nudge down, then to a fresh column, until it fits
244
- if not overlaps((rx, ry, rx + p_w, ry + p_h)):
245
- break
246
- ry += line_h
247
- if ry + p_h > H:
248
- ry = max(0, min(y1, H - p_h))
249
- rx = rx + p_w + 6
250
- if rx + p_w > W:
251
- rx = max(0, W - p_w)
252
- placed.append((rx, ry, rx + p_w, ry + p_h))
253
-
254
- od.rounded_rectangle((rx, ry, rx + p_w, ry + p_h), radius=5, fill=(0, 0, 0, 175))
255
- for k, (lab, pres) in enumerate(rows):
256
- tc = col if pres is None else (GREEN if pres else RED)
257
- od.text((rx + pad, ry + pad + k * line_h), txts[k], font=rf, fill=tc + (255,))
258
-
259
- out_im = Image.alpha_composite(base, ov).convert("RGB")
260
- return out_im, "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)