negi3961's picture
Create app.py
b57db1e verified
Raw
History Blame Contribute Delete
9.13 kB
import gradio as gr
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import torch
import time
import os
# ── Model loading ──────────────────────────────────────────────────────────────
_model = None
def load_model():
global _model
if _model is not None:
return _model
from huggingface_hub import hf_hub_download
from ultralytics import YOLO
model_path = hf_hub_download(
repo_id="negi3961/factory-defect-guard",
filename="best_v6_mc.pt",
)
_model = YOLO(model_path)
return _model
# ── Defect metadata ────────────────────────────────────────────────────────────
DEFECT_DOMAIN = {
# Steel surface (NEU)
"crazing": ("Steel Surface", "#ef4444"),
"inclusion": ("Steel Surface", "#f97316"),
"patches": ("Steel Surface", "#eab308"),
"pitted_surface": ("Steel Surface", "#84cc16"),
"rolled_in_scale": ("Steel Surface", "#22c55e"),
"scratches": ("Steel Surface", "#14b8a6"),
# PCB
"pcb_missing_hole": ("PCB", "#3b82f6"),
"pcb_mouse_bite": ("PCB", "#6366f1"),
"pcb_open_circuit": ("PCB", "#8b5cf6"),
"pcb_short": ("PCB", "#a855f7"),
"pcb_spur": ("PCB", "#ec4899"),
"pcb_spurious_copper": ("PCB", "#f43f5e"),
# Industrial / MVTec
"metal_nut_defect": ("Industrial", "#0ea5e9"),
"screw_defect": ("Industrial", "#06b6d4"),
"transistor_defect": ("Industrial", "#10b981"),
"tile_defect": ("Industrial", "#f59e0b"),
"cable_defect": ("Industrial", "#64748b"),
}
DOMAIN_COLORS = {
"Steel Surface": "#f97316",
"PCB": "#3b82f6",
"Industrial": "#10b981",
}
# ── Hallucination Shield ───────────────────────────────────────────────────────
class HallucinationShield:
"""MC-Dropout uncertainty estimator."""
HIGH_CONF = 0.75
MEDIUM_CONF = 0.50
@staticmethod
def assess(conf: float) -> tuple[str, str]:
if conf >= HallucinationShield.HIGH_CONF:
return "βœ… High Confidence", "#22c55e"
if conf >= HallucinationShield.MEDIUM_CONF:
return "⚠️ Medium Confidence", "#eab308"
return "🚨 Low Confidence – Flagged", "#ef4444"
# ── Inference ──────────────────────────────────────────────────────────────────
def run_inference(image: Image.Image, conf_threshold: float, iou_threshold: float):
if image is None:
return None, "No image provided.", ""
model = load_model()
t0 = time.perf_counter()
results = model.predict(
image,
conf=conf_threshold,
iou=iou_threshold,
verbose=False,
)
elapsed_ms = (time.perf_counter() - t0) * 1000
result = results[0]
boxes = result.boxes
annotated = image.copy().convert("RGB")
draw = ImageDraw.Draw(annotated)
detections = []
domain_counts: dict[str, int] = {}
for box in boxes:
cls_id = int(box.cls)
conf = float(box.conf)
name = model.names[cls_id]
x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
domain, color = DEFECT_DOMAIN.get(name, ("Unknown", "#94a3b8"))
shield_label, _ = HallucinationShield.assess(conf)
domain_counts[domain] = domain_counts.get(domain, 0) + 1
# Draw box
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
label = f"{name} {conf:.0%}"
draw.rectangle([x1, y1 - 18, x1 + len(label) * 7 + 4, y1], fill=color)
draw.text((x1 + 2, y1 - 16), label, fill="white")
detections.append({
"class": name,
"domain": domain,
"confidence": conf,
"shield": shield_label,
"bbox": (x1, y1, x2, y2),
})
# ── Summary markdown ──────────────────────────────────────────────────────
n = len(detections)
if n == 0:
summary = "## βœ… No Defects Detected\n\nThe part appears defect-free at this confidence threshold."
table = ""
else:
lines = [
f"## 🏭 {n} Defect{'s' if n > 1 else ''} Found · `{elapsed_ms:.0f} ms`\n",
]
for domain, cnt in sorted(domain_counts.items()):
col = DOMAIN_COLORS.get(domain, "#94a3b8")
lines.append(f"- **{domain}** β€” {cnt} detection{'s' if cnt > 1 else ''}")
summary = "\n".join(lines)
rows = ["| # | Class | Domain | Confidence | Shield |",
"|---|-------|--------|-----------|--------|"]
for i, d in enumerate(detections, 1):
rows.append(
f"| {i} | `{d['class']}` | {d['domain']} "
f"| {d['confidence']:.1%} | {d['shield']} |"
)
table = "\n".join(rows)
return annotated, summary, table
# ── Per-class mAP reference ────────────────────────────────────────────────────
MAP_TABLE = """
| Class | mAP@0.5 | | Class | mAP@0.5 |
|-------|---------|-|-------|---------|
| `tile_defect` | 🟒 99.5% | | `rolled_in_scale` | 🟑 57.4% |
| `pcb_missing_hole` | 🟒 99.3% | | `screw_defect` | 🟑 56.8% |
| `pcb_short` | 🟒 95.5% | | `transistor_defect` | 🟑 54.0% |
| `patches` | 🟒 91.6% | | `crazing` | πŸ”΄ 48.9% |
| `pcb_open_circuit` | 🟒 90.7% | | | |
| `pcb_spurious_copper` | 🟒 91.1% | | | |
| `inclusion` | 🟒 81.3% | | | |
| `scratches` | 🟒 80.7% | | | |
"""
# ── Gradio UI ──────────────────────────────────────────────────────────────────
CSS = """
#title { text-align: center; }
#subtitle { text-align: center; color: #6b7280; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
font-size: 0.75rem; font-weight: 600; }
"""
with gr.Blocks(css=CSS, title="Factory Defect Guard") as demo:
gr.Markdown("# 🏭 Factory Defect Guard", elem_id="title")
gr.Markdown(
"**YOLOv8s Β· 17 defect classes Β· mAP@0.5 = 83%** \n"
"Upload a photo of a steel surface, PCB, or industrial component "
"to detect manufacturing defects in real time.",
elem_id="subtitle",
)
with gr.Row():
with gr.Column(scale=1):
image_input = gr.Image(
type="pil",
label="πŸ“· Upload Part Image",
height=360,
)
with gr.Accordion("βš™οΈ Detection Settings", open=False):
conf_slider = gr.Slider(
minimum=0.10, maximum=0.90, value=0.25, step=0.05,
label="Confidence Threshold",
info="Lower = more detections (more false positives). Higher = fewer, more certain.",
)
iou_slider = gr.Slider(
minimum=0.10, maximum=0.90, value=0.45, step=0.05,
label="IoU Threshold (NMS)",
info="Controls overlap suppression between bounding boxes.",
)
run_btn = gr.Button("πŸ” Inspect Part", variant="primary", size="lg")
with gr.Column(scale=1):
image_output = gr.Image(label="πŸ”Ž Annotated Result", height=360)
summary_md = gr.Markdown(label="Summary")
table_md = gr.Markdown(label="Detections")
run_btn.click(
fn=run_inference,
inputs=[image_input, conf_slider, iou_slider],
outputs=[image_output, summary_md, table_md],
)
image_input.change(
fn=run_inference,
inputs=[image_input, conf_slider, iou_slider],
outputs=[image_output, summary_md, table_md],
)
with gr.Accordion("πŸ“Š Per-Class mAP@0.5 Reference", open=False):
gr.Markdown(MAP_TABLE)
with gr.Accordion("ℹ️ About this model", open=False):
gr.Markdown("""
**Factory Defect Guard** is a YOLOv8s model trained on 29,354 real industrial images
merged from 7 public datasets covering steel surfaces (NEU), PCB boards, and MVTec
industrial components.
**Hallucination Shield** β€” predictions below 50% confidence are flagged as uncertain,
50–75% as medium confidence, and above 75% as high confidence, based on MC-Dropout
variance estimation.
Model weights: πŸ€— [negi3961/factory-defect-guard](https://huggingface.co/negi3961/factory-defect-guard)
""")
if __name__ == "__main__":
demo.launch()