michaelandrew35's picture
Clearer explanation
29fa77a verified
Raw History Blame Contribute Delete
5.68 kB
"""Hugging Face Space: upload a PCB image, get defect boxes from the INT8 ONNX model.
Pure ONNX Runtime + OpenCV (no PyTorch / Ultralytics), so the Space is small and
runs on any hardware tier's CPU. Put the quantized model next to this file as
model_int8.onnx (or set MODEL_PATH). Optional: a few test-split images in examples/.
"""
import ast
import os
import time
from pathlib import Path
import cv2
import gradio as gr
import numpy as np
import onnxruntime as ort
try: # Hugging Face ZeroGPU hardware expects at least one @spaces.GPU function to exist.
import spaces
@spaces.GPU
def _unused_gpu_placeholder():
"""Never called: inference below runs on CPU only."""
return None
except ImportError: # local runs / other hardware
pass
HERE = Path(__file__).parent
MODEL_PATH = os.environ.get("MODEL_PATH", str(HERE / "model_int8.onnx"))
IMGSZ = int(os.environ.get("IMGSZ", "1280"))
NMS_IOU = 0.7 # Ultralytics default
FALLBACK_NAMES = ["missing_hole", "mouse_bite", "open_circuit", "short", "spur", "spurious_copper"]
COLORS = [(56, 56, 255), (151, 157, 255), (31, 112, 255), (29, 178, 255), (49, 210, 207), (10, 249, 72)] # BGR
opts = ort.SessionOptions()
opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
session = ort.InferenceSession(MODEL_PATH, opts, providers=["CPUExecutionProvider"])
INPUT_NAME = session.get_inputs()[0].name
_meta = session.get_modelmeta().custom_metadata_map
NAMES = ast.literal_eval(_meta["names"]) if "names" in _meta else dict(enumerate(FALLBACK_NAMES))
def letterbox(img, size):
"""Resize keeping aspect ratio and pad to size x size (gray 114), like Ultralytics."""
h, w = img.shape[:2]
r = min(size / h, size / w)
nh, nw = round(h * r), round(w * r)
top, left = (size - nh) // 2, (size - nw) // 2
out = np.full((size, size, 3), 114, dtype=np.uint8)
out[top:top + nh, left:left + nw] = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
return out, r, left, top
def predict(bgr, conf):
"""Return a list of (class_id, score, x1, y1, x2, y2) in original-image pixels."""
padded, r, dx, dy = letterbox(bgr, IMGSZ)
x = np.ascontiguousarray(padded[:, :, ::-1].transpose(2, 0, 1), dtype=np.float32)[None] / 255.0
out = session.run(None, {INPUT_NAME: x})[0][0].T # (anchors, 4 + num_classes)
scores = out[:, 4:]
cls = scores.argmax(1)
score = scores[np.arange(len(cls)), cls]
keep = score >= conf
if not keep.any():
return []
xywh, cls, score = out[keep, :4], cls[keep], score[keep]
h, w = bgr.shape[:2]
x1 = np.clip((xywh[:, 0] - xywh[:, 2] / 2 - dx) / r, 0, w)
y1 = np.clip((xywh[:, 1] - xywh[:, 3] / 2 - dy) / r, 0, h)
x2 = np.clip((xywh[:, 0] + xywh[:, 2] / 2 - dx) / r, 0, w)
y2 = np.clip((xywh[:, 1] + xywh[:, 3] / 2 - dy) / r, 0, h)
boxes = np.stack([x1, y1, x2 - x1, y2 - y1], 1)
idx = cv2.dnn.NMSBoxesBatched(boxes.tolist(), score.tolist(), cls.tolist(), conf, NMS_IOU)
idx = np.asarray(idx, dtype=int).reshape(-1)
return [(int(cls[i]), float(score[i]), float(x1[i]), float(y1[i]), float(x2[i]), float(y2[i])) for i in idx]
def draw(bgr, dets):
out = bgr.copy()
thick = max(2, round(max(out.shape[:2]) / 600))
for c, s, x1, y1, x2, y2 in dets:
color = COLORS[c % len(COLORS)]
cv2.rectangle(out, (int(x1), int(y1)), (int(x2), int(y2)), color, thick)
cv2.putText(out, f"{NAMES[c]} {s:.2f}", (int(x1), max(20, int(y1) - 8)),
cv2.FONT_HERSHEY_SIMPLEX, thick / 2.5, color, thick, cv2.LINE_AA)
return out
def detect(image, conf):
if image is None:
return None, [], ""
bgr = np.ascontiguousarray(image[:, :, ::-1])
t0 = time.perf_counter()
dets = predict(bgr, conf)
ms = (time.perf_counter() - t0) * 1000
rows = [[NAMES[c], round(s, 3), int(x1), int(y1), int(x2), int(y2)] for c, s, x1, y1, x2, y2 in dets]
status = f"{len(rows)} defects found in {ms:.0f} ms (CPU, ONNX Runtime, preprocessing + model + NMS)"
return draw(bgr, dets)[:, :, ::-1], rows, status
# Example images: an examples/ folder, or .jpg files uploaded next to app.py.
EXAMPLES = {p.stem: str(p) for p in sorted([*HERE.glob("examples/*.jpg"), *HERE.glob("*.jpg")])}
def run_example(name, conf):
"""Load a bundled test image and detect on it in one step."""
if not name:
return None, None, [], ""
rgb = np.ascontiguousarray(cv2.imread(EXAMPLES[name])[:, :, ::-1])
annotated, rows, status = detect(rgb, conf)
return rgb, annotated, rows, status
with gr.Blocks(title="PCB defect detection (INT8, CPU)") as demo:
gr.Markdown(
"# PCB defect detection\n"
"YOLO11n quantized to INT8 with ONNX Runtime (detection head kept in FP32), running on CPU. "
"Trained on PKU-Market-PCB, a public research dataset, not production factory data. "
"Defects are tiny: zoom into the result to see the boxes."
)
with gr.Row():
inp = gr.Image(type="numpy", label="PCB image", format="jpeg")
out = gr.Image(type="numpy", label="Detections", format="jpeg")
conf = gr.Slider(0.05, 0.9, value=0.25, step=0.05, label="Confidence threshold")
status = gr.Markdown()
table = gr.Dataframe(headers=["class", "conf", "x1", "y1", "x2", "y2"], label="Boxes")
if EXAMPLES:
pick = gr.Radio(list(EXAMPLES), value=None, label="Or pick a test-split example, then press Detect")
gr.Button("Detect", variant="primary").click(detect, [inp, conf], [out, table, status])
if EXAMPLES:
pick.change(run_example, [pick, conf], [inp, out, table, status])
if __name__ == "__main__":
demo.launch()