Spaces:
Running
Running
Add Object Detection tab (full-frame D-FINE, class summary)
Browse filesThird 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>
- app.py +78 -2
- dfine_jina_pipeline.py +108 -0
app.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
""" Gradio app: D-FINE + SigLIP Classify,
|
| 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()
|