File size: 5,937 Bytes
58aa9b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import gradio as gr
import numpy as np
import onnxruntime as ort
from PIL import Image, ImageDraw, ImageFont
import cv2
# ββ Configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MODEL_PATH = "best.onnx"
INPUT_SIZE = 640
CONF_THRESHOLD = 0.4
IOU_THRESHOLD = 0.45
CLASS_NAMES = ["crack", "spalling", "pothole"]
CLASS_COLORS = {
"crack": (255, 0, 0), # red
"spalling": (255, 165, 0), # orange
"pothole": (255, 255, 0), # yellow
}
# ββ Load ONNX model once at startup βββββββββββββββββββββββββββββββββββββββββ
session = ort.InferenceSession(MODEL_PATH, providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name
# ββ Preprocessing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def preprocess(image: Image.Image):
original_w, original_h = image.size
resized = image.resize((INPUT_SIZE, INPUT_SIZE))
img_array = np.array(resized).astype(np.float32) / 255.0
# HWC -> CHW
img_array = img_array.transpose(2, 0, 1)
# Add batch dimension
img_array = np.expand_dims(img_array, axis=0)
return img_array, original_w, original_h
# ββ IoU + NMS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def compute_iou(box1, box2):
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
inter_area = max(0, x2 - x1) * max(0, y2 - y1)
box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])
box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])
union_area = box1_area + box2_area - inter_area
return inter_area / union_area if union_area > 0 else 0
def non_max_suppression(detections, iou_threshold):
detections = sorted(detections, key=lambda d: d["confidence"], reverse=True)
keep = []
while detections:
best = detections.pop(0)
keep.append(best)
detections = [
d for d in detections
if compute_iou(best["bbox"], d["bbox"]) < iou_threshold
]
return keep
# ββ Parse YOLOv8 raw output: shape [1, 7, 8400] ββββββββββββββββββββββββββββ
# 7 = 4 (cx, cy, w, h) + 3 (class scores, no separate objectness column)
# 8400 = number of candidate detections across all scales
def parse_yolo_output(output, original_w, original_h):
output = output[0] # shape: [7, 8400]
output = output.T # transpose -> shape: [8400, 7]
scale_x = original_w / INPUT_SIZE
scale_y = original_h / INPUT_SIZE
detections = []
for row in output:
cx, cy, w, h = row[0], row[1], row[2], row[3]
class_scores = row[4:4 + len(CLASS_NAMES)]
class_id = int(np.argmax(class_scores))
confidence = float(class_scores[class_id])
if confidence < CONF_THRESHOLD:
continue
cx, cy, w, h = cx * scale_x, cy * scale_y, w * scale_x, h * scale_y
x1, y1 = cx - w / 2, cy - h / 2
x2, y2 = cx + w / 2, cy + h / 2
detections.append({
"bbox": [x1, y1, x2, y2],
"confidence": confidence,
"class_id": class_id,
"class_name": CLASS_NAMES[class_id],
})
return non_max_suppression(detections, IOU_THRESHOLD)
# ββ Draw detections on the image ββββββββββββββββββββββββββββββββββββββββ
def draw_detections(image: Image.Image, detections):
image = image.copy()
draw = ImageDraw.Draw(image)
for det in detections:
x1, y1, x2, y2 = det["bbox"]
color = CLASS_COLORS.get(det["class_name"], (255, 255, 255))
label = f"{det['class_name']} {det['confidence']:.2f}"
draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
text_bbox = draw.textbbox((x1, y1), label)
draw.rectangle(text_bbox, fill=color)
draw.text((x1, y1), label, fill=(0, 0, 0))
return image
# ββ Main inference function (called by Gradio) βββββββββββββββββββββββββββββ
def detect_damage(input_image: Image.Image):
if input_image is None:
return None, "No image provided."
input_image = input_image.convert("RGB")
img_array, original_w, original_h = preprocess(input_image)
outputs = session.run(None, {input_name: img_array})
detections = parse_yolo_output(outputs[0], original_w, original_h)
result_image = draw_detections(input_image, detections)
if not detections:
summary = "No damage detected."
else:
lines = [f"Found {len(detections)} detection(s):"]
for d in detections:
lines.append(
f"- {d['class_name']} (confidence: {d['confidence']:.2f})"
)
summary = "\n".join(lines)
return result_image, summary
# ββ Gradio interface ββββββββββββββββββββββββββββββββββββββββββββββββββββ
demo = gr.Interface(
fn=detect_damage,
inputs=gr.Image(type="pil", label="Upload an image"),
outputs=[
gr.Image(type="pil", label="Detection result"),
gr.Textbox(label="Summary"),
],
title="Structural Damage Detection (Crack / Spalling / Pothole)",
description="Upload an image of a road or concrete structure to detect cracks, spalling, and potholes using a YOLOv8 model.",
)
if __name__ == "__main__":
demo.launch() |