orik-ss Claude Opus 4.8 (1M context) commited on
Commit
83fbdc4
·
1 Parent(s): 146b2bb

Add Object Detection tab (full-frame D-FINE, class summary)

Browse files

Third tab after PPE: run any of the 9 D-FINE models on the whole
image with a threshold slider, draw one colour-coded box per
detection (class + score), and list per-class counts below.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +78 -2
  2. dfine_jina_pipeline.py +108 -0
app.py CHANGED
@@ -1,10 +1,10 @@
1
- """ Gradio app: D-FINE + SigLIP Classify, plus PPE Detection. """
2
 
3
  import os
4
  import gradio as gr
5
  from pathlib import Path
6
 
7
- from dfine_jina_pipeline import run_single_image
8
  from ppe_compliance import run_ppe_compliance
9
 
10
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -43,6 +43,21 @@ def run_dfine_classify(image, dfine_threshold, dfine_model_choice, classifier_ch
43
  return [(g, None) for g in (group_crops or [])], [(k, None) for k in (known_crops or [])], status or ""
44
 
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  IMG_HEIGHT = 400
47
 
48
 
@@ -226,6 +241,67 @@ with gr.Blocks(title="Small Object Detection") as app:
226
  concurrency_limit=1,
227
  )
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  app.launch(
231
  server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
 
1
+ """ Gradio app: D-FINE + SigLIP Classify, PPE Detection, and D-FINE Object Detection. """
2
 
3
  import os
4
  import gradio as gr
5
  from pathlib import Path
6
 
7
+ from dfine_jina_pipeline import run_single_image, run_object_detection
8
  from ppe_compliance import run_ppe_compliance
9
 
10
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
43
  return [(g, None) for g in (group_crops or [])], [(k, None) for k in (known_crops or [])], status or ""
44
 
45
 
46
+ def run_objdet(image, dfine_model_choice, det_threshold):
47
+ """Run a chosen D-FINE model on the full image.
48
+ Returns (annotated_image, detected-classes summary).
49
+ """
50
+ if image is None:
51
+ return None, "Upload an image."
52
+
53
+ dfine_model = dfine_model_choice.strip().lower() if dfine_model_choice else "medium-obj2coco"
54
+ return run_object_detection(
55
+ image,
56
+ dfine_model=dfine_model,
57
+ det_threshold=float(det_threshold),
58
+ )
59
+
60
+
61
  IMG_HEIGHT = 400
62
 
63
 
 
241
  concurrency_limit=1,
242
  )
243
 
244
+ with gr.Tab("Object Detection"):
245
+
246
+ gr.Markdown(
247
+ "Run a **D-FINE** model on the whole image and see every detection. "
248
+ "Pick a D-FINE model and detection threshold; each object is drawn with a "
249
+ "colour-coded box (class + score) and the classes found are listed below."
250
+ )
251
+
252
+ with gr.Row():
253
+
254
+ with gr.Column(scale=1):
255
+
256
+ inp_objdet = gr.Image(
257
+ type="pil",
258
+ label="Input image",
259
+ height=IMG_HEIGHT,
260
+ )
261
+
262
+ objdet_model_dropdown = gr.Dropdown(
263
+ choices=[
264
+ "small-obj365", "medium-obj365", "large-obj365",
265
+ "small-coco", "medium-coco", "large-coco",
266
+ "small-obj2coco", "medium-obj2coco", "large-obj2coco",
267
+ ],
268
+ value="medium-obj2coco",
269
+ label="D-FINE model",
270
+ )
271
+
272
+ objdet_threshold_slider = gr.Slider(
273
+ minimum=0.05,
274
+ maximum=0.9,
275
+ value=0.3,
276
+ step=0.05,
277
+ label="Detection threshold",
278
+ )
279
+
280
+ btn_objdet = gr.Button(
281
+ "Run Object Detection",
282
+ variant="primary",
283
+ )
284
+
285
+ with gr.Column(scale=1):
286
+
287
+ out_objdet_image = gr.Image(
288
+ label="Detections (one colour-coded box per object, class + score)",
289
+ height=IMG_HEIGHT,
290
+ )
291
+
292
+ out_objdet_status = gr.Textbox(
293
+ label="Detected classes",
294
+ lines=12,
295
+ interactive=False,
296
+ )
297
+
298
+ btn_objdet.click(
299
+ fn=run_objdet,
300
+ inputs=[inp_objdet, objdet_model_dropdown, objdet_threshold_slider],
301
+ outputs=[out_objdet_image, out_objdet_status],
302
+ concurrency_limit=1,
303
+ )
304
+
305
 
306
  app.launch(
307
  server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
dfine_jina_pipeline.py CHANGED
@@ -5,6 +5,7 @@ Outputs jina_crops folder and results CSV.
5
 
6
  import argparse
7
  import csv
 
8
  import time
9
  from pathlib import Path
10
 
@@ -776,5 +777,112 @@ def run_single_image(
776
  return group_crop_images, known_crop_composites, log_text
777
 
778
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
  if __name__ == "__main__":
780
  main()
 
5
 
6
  import argparse
7
  import csv
8
+ import os
9
  import time
10
  from pathlib import Path
11
 
 
777
  return group_crop_images, known_crop_composites, log_text
778
 
779
 
780
+ # -----------------------------------------------------------------------------
781
+ # Full-frame object detection (Object Detection tab): run a chosen D-FINE model
782
+ # on the whole image, draw every detection, and summarise the classes found.
783
+ # -----------------------------------------------------------------------------
784
+
785
+ # Cycled by class id so different classes get distinct box colours.
786
+ OBJDET_PALETTE = [
787
+ (255, 56, 56), (255, 159, 0), (255, 221, 0), (0, 199, 89),
788
+ (0, 162, 255), (170, 0, 255), (0, 255, 200), (255, 0, 170),
789
+ (120, 200, 0), (0, 120, 255),
790
+ ]
791
+
792
+
793
+ def _objdet_font(size):
794
+ for p in ("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
795
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"):
796
+ if os.path.exists(p):
797
+ from PIL import ImageFont
798
+ return ImageFont.truetype(p, size)
799
+ from PIL import ImageFont
800
+ return ImageFont.load_default()
801
+
802
+
803
+ def run_object_detection(
804
+ pil_image,
805
+ dfine_model="medium-obj2coco",
806
+ det_threshold=0.3,
807
+ device=None,
808
+ ):
809
+ """Run a chosen D-FINE model on the full frame and draw every detection.
810
+
811
+ Used by the Object Detection tab. Returns ``(annotated_image_np, status_text)``:
812
+ - the image with one colour-coded box per detection (label + score),
813
+ - a text summary of how many of each class were found.
814
+ """
815
+ from PIL import Image, ImageDraw
816
+ import numpy as np
817
+
818
+ if pil_image is None:
819
+ return None, "Upload an image."
820
+
821
+ dfine_model = (dfine_model or "medium-obj2coco").strip().lower()
822
+ if dfine_model not in DFINE_MODEL_IDS:
823
+ dfine_model = "medium-obj2coco"
824
+ model_id = DFINE_MODEL_IDS[dfine_model]
825
+
826
+ device = device or ("cuda" if torch.cuda.is_available() else "cpu")
827
+
828
+ pil = pil_image.convert("RGB") if isinstance(pil_image, Image.Image) else Image.fromarray(pil_image).convert("RGB")
829
+
830
+ # Load / reuse D-FINE (shares the app-wide cache with the Detect & Classify tab).
831
+ global _APP_DFINE
832
+ if _APP_DFINE is None or _APP_DFINE[0] != dfine_model:
833
+ print(f"[*] Loading D-FINE ({model_id})...")
834
+ image_processor = AutoImageProcessor.from_pretrained(model_id)
835
+ dfine_model_obj = DFineForObjectDetection.from_pretrained(model_id)
836
+ dfine_model_obj = dfine_model_obj.to(device).eval()
837
+ person_car_ids = get_person_car_label_ids(dfine_model_obj)
838
+ _APP_DFINE = (dfine_model, image_processor, dfine_model_obj, person_car_ids)
839
+
840
+ _model_id, image_processor, dfine_model_obj, _person_car_ids = _APP_DFINE
841
+
842
+ # Use the model's actual device (it may have been loaded on GPU by another tab).
843
+ mdev = str(next(dfine_model_obj.parameters()).device)
844
+ detections = run_dfine(pil, image_processor, dfine_model_obj, mdev, det_threshold)
845
+ detections.sort(key=lambda d: -d["conf"])
846
+
847
+ out = pil.copy()
848
+ draw = ImageDraw.Draw(out)
849
+ W, H = out.size
850
+ font = _objdet_font(max(14, int(0.018 * max(W, H))))
851
+ lw = max(2, int(0.003 * max(W, H)))
852
+
853
+ for d in detections:
854
+ x1, y1, x2, y2 = [int(v) for v in d["box"]]
855
+ color = OBJDET_PALETTE[d["cls"] % len(OBJDET_PALETTE)]
856
+ draw.rectangle([x1, y1, x2, y2], outline=color, width=lw)
857
+ text = f"{d['label']} {d['conf']:.2f}"
858
+ tb = draw.textbbox((0, 0), text, font=font)
859
+ tw, th = tb[2] - tb[0], tb[3] - tb[1]
860
+ ty = max(0, y1 - th - 4)
861
+ draw.rectangle([x1, ty, x1 + tw + 6, ty + th + 4], fill=color)
862
+ draw.text((x1 + 3, ty + 2), text, fill=(255, 255, 255), font=font)
863
+
864
+ if not detections:
865
+ return np.array(out), (
866
+ f"No objects detected at threshold {det_threshold:.2f}. "
867
+ "Try lowering the threshold."
868
+ )
869
+
870
+ # Summary: which classes were detected in the image, and how many of each.
871
+ counts = {}
872
+ for d in detections:
873
+ counts[d["label"]] = counts.get(d["label"], 0) + 1
874
+
875
+ lines = [
876
+ f"{len(detections)} detection(s) at threshold {det_threshold:.2f} "
877
+ f"across {len(counts)} class(es):",
878
+ "",
879
+ ]
880
+ lines += [f" {name}: {n}" for name, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))]
881
+ lines += ["", "Per detection (sorted by score):"]
882
+ lines += [f" {d['label']} {d['conf']:.3f}" for d in detections]
883
+
884
+ return np.array(out), "\n".join(lines)
885
+
886
+
887
  if __name__ == "__main__":
888
  main()