Download app.py from michaelandrew35/pcb-defect-detection: direct link, hf CLI and curl.
- Browser
- Download file 5.68 kB
-
https://huggingface.co/spaces/michaelandrew35/pcb-defect-detection/resolve/main/app.py
- Command line
-
hf download hf://spaces/michaelandrew35/pcb-defect-detection/app.py
-
curl -L -o app.py https://huggingface.co/spaces/michaelandrew35/pcb-defect-detection/resolve/main/app.py
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 | |
| 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() | |