File size: 9,795 Bytes
9a82b0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87a9d70
9a82b0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2e7a2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9a82b0d
 
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
"""
Rebar Detection with YOLO Models - Gradio Application

This application provides a web interface for detecting rebars in GPR images
using YOLO-based ONNX models. It includes features for tiled processing
with configurable overlap and confidence thresholds.

Developer: Ahmed Elseicy
Email: ahmedmossadibrahim.elseicy@uvigo.gal
Date: July 30, 2025
"""
import gradio as gr
import numpy as np
import onnxruntime as ort
from PIL import Image, ImageDraw
import os

# --- Configuration ---

MODEL_DIR = "models"
EXAMPLE_DIR = "examples"
# Ensure the directories exist
if not os.path.exists(MODEL_DIR):
    os.makedirs(MODEL_DIR)
if not os.path.exists(EXAMPLE_DIR):
    os.makedirs(EXAMPLE_DIR)

# --- Model Loading ---


def get_available_models():
    if not os.path.exists(MODEL_DIR) or not os.listdir(MODEL_DIR):
        print(
            f"Warning: No models found in '{MODEL_DIR}'. Please upload your .onnx files.")
        return ["No models found"]
    # Strip the .onnx extension for a cleaner display name
    return [os.path.splitext(f)[0] for f in os.listdir(MODEL_DIR) if f.endswith(".onnx")]


AVAILABLE_MODELS = get_available_models()

# --- Helper Function ---


def create_blank_image(width=512, height=512):
    """Creates a blank white PIL image."""
    return Image.new('RGB', (width, height), 'white')

# --- Image Processing and Inference Logic ---


def slice_image(image, tile_size=(256, 256), overlap_ratio=0.2):
    """Slices an image into overlapping tiles."""
    img_w, img_h = image.size
    tile_w, tile_h = tile_size
    stride_w = int(tile_w * (1 - overlap_ratio))
    stride_h = int(tile_h * (1 - overlap_ratio))

    for y in range(0, img_h, stride_h):
        for x in range(0, img_w, stride_w):
            box = (x, y, x + tile_w, y + tile_h)
            if box[2] > img_w:
                box = (img_w - tile_w, box[1], img_w, box[3])
            if box[3] > img_h:
                box = (box[0], img_h - tile_h, box[2], img_h)

            yield image.crop(box), (box[0], box[1])

            if box[2] >= img_w:
                break
        if box[3] >= img_h:
            break


def run_yolo_inference(session, image_tile):
    """
    Runs inference using a YOLO ONNX model and returns processed detections.
    """
    # 1. Preprocess the image
    input_image = np.array(image_tile.resize(
        (256, 256)), dtype=np.float32) / 255.0
    input_image = np.expand_dims(input_image, axis=0)
    input_image = np.transpose(input_image, (0, 3, 1, 2))

    # 2. Run inference
    input_name = session.get_inputs()[0].name
    output_name = session.get_outputs()[0].name
    result = session.run([output_name], {input_name: input_image})[0]

    # 3. Post-process the output
    detections = result[0].T
    boxes = []
    scores = []

    for row in detections:
        # For object detection, the row format is typically [cx, cy, w, h, class_confidence, ...]
        class_probs = row[4:]
        class_id = np.argmax(class_probs)
        confidence = class_probs[class_id]

        # Extract box and convert from [center_x, center_y, width, height] to [x1, y1, x2, y2]
        cx, cy, w, h = row[:4]
        x1 = cx - w / 2
        y1 = cy - h / 2
        x2 = cx + w / 2
        y2 = cy + h / 2

        boxes.append([x1, y1, x2, y2])
        scores.append(confidence)

    return np.array(boxes), np.array(scores)


def non_max_suppression(boxes, scores, iou_threshold):
    """Performs Non-Maximum Suppression to merge overlapping boxes."""
    if len(boxes) == 0:
        return []

    x1 = boxes[:, 0]
    y1 = boxes[:, 1]
    x2 = boxes[:, 2]
    y2 = boxes[:, 3]

    areas = (x2 - x1) * (y2 - y1)
    order = scores.argsort()[::-1]

    keep = []
    while order.size > 0:
        i = order[0]
        keep.append(i)

        xx1 = np.maximum(x1[i], x1[order[1:]])
        yy1 = np.maximum(y1[i], y1[order[1:]])
        xx2 = np.minimum(x2[i], x2[order[1:]])
        yy2 = np.minimum(y2[i], y2[order[1:]])

        w = np.maximum(0.0, xx2 - xx1)
        h = np.maximum(0.0, yy2 - yy1)

        intersection = w * h
        iou = intersection / (areas[i] + areas[order[1:]] - intersection)

        inds = np.where(iou <= iou_threshold)[0]
        order = order[inds + 1]

    return keep


def detect_rebars(model_name, input_image, overlap_ratio, confidence_threshold, iou_threshold):
    """Main function to orchestrate the detection process."""
    if model_name is None or model_name == "No models found" or input_image is None:
        return create_blank_image(), "Please select a model and upload an image."

    try:
        # Add the .onnx extension back to the model name for file path
        model_path = os.path.join(MODEL_DIR, model_name + ".onnx")
        session = ort.InferenceSession(model_path)
    except Exception as e:
        return create_blank_image(), f"Error loading model: {e}"

    # Convert input image to RGB to ensure drawing works correctly
    original_image = Image.fromarray(input_image).convert("RGB")
    all_boxes = []
    all_scores = []

    for tile, (x_offset, y_offset) in slice_image(original_image, overlap_ratio=overlap_ratio):
        try:
            boxes_on_tile, scores_on_tile = run_yolo_inference(session, tile)
            for box, score in zip(boxes_on_tile, scores_on_tile):
                if score >= confidence_threshold:
                    x1, y1, x2, y2 = box
                    all_boxes.append(
                        [x1 + x_offset, y1 + y_offset, x2 + x_offset, y2 + y_offset])
                    all_scores.append(score)
        except Exception as e:
            return create_blank_image(), f"An error occurred during inference: {e}."

    if not all_boxes:
        return original_image, "Detection complete. No rebars found."

    # Apply Non-Maximum Suppression to all collected boxes
    final_indices = non_max_suppression(
        np.array(all_boxes), np.array(all_scores), iou_threshold)

    # Create a copy to draw on
    stitched_image = original_image.copy()
    draw = ImageDraw.Draw(stitched_image)

    for i in final_indices:
        box = all_boxes[i]
        draw.rectangle(box, outline="red", width=3)

    status_message = f"Detection complete. Found {len(final_indices)} rebars."
    return stitched_image, status_message


# --- Gradio Web Interface ---
with gr.Blocks(theme=gr.themes.Soft()) as demo:
    gr.Markdown("# Rebar Detection using YOLO Models")
    gr.Markdown(
        """
        Select a model, upload a GPR image, set processing parameters, and the model will predict rebar locations.
        **Note:** This is a prototype implementation running on a vCPU. The image slicing is currently based on pixels. For practical applications, slicing should be performed by distance (meters) in the horizontal direction.
        """
    )

    with gr.Row():
        with gr.Column(scale=1):
            model_selector = gr.Dropdown(
                label="Select Model", choices=AVAILABLE_MODELS, value=None)
            image_input = gr.Image(type="numpy", label="Upload GPR Image")

            # Add example images for users to test
            gr.Examples(
                examples=os.path.join(os.path.dirname(__file__), EXAMPLE_DIR),
                inputs=image_input,
                label="Example Images"
            )

            overlap_slider = gr.Slider(
                minimum=0.0, maximum=0.9, step=0.05, value=0.2, label="Overlap Ratio")
            confidence_slider = gr.Slider(
                minimum=0.0, maximum=1.0, step=0.05, value=0.25, label="Confidence Threshold")
            iou_slider = gr.Slider(
                minimum=0.0, maximum=1.0, step=0.05, value=0.45, label="IoU Threshold (for NMS)")
            submit_btn = gr.Button("Detect Rebars", variant="primary")

        with gr.Column(scale=2):
            status_output = gr.Textbox(label="Status", interactive=False)
            # Set height to "auto" to prevent shrinking with wide images
            image_output = gr.Image(
                type="pil", label="Detection Result", height="auto")


    submit_btn.click(
        fn=detect_rebars,
        inputs=[model_selector, image_input,
                overlap_slider, confidence_slider, iou_slider],
        outputs=[image_output, status_output]
    )

    
    attribution_info = """
    ## 📜 Paper Information
    This Space is based on the research presented in our paper for IWAGPR25:
    ```bibtex
    @inproceedings{elseicy2025rebar,
        title     = {Preliminary Study on Automating Rebar Detection in Reinforced Concrete Structures Using YOLOv11 and GPR Data},
        author    = {Elseicy, Ahmed and Solla, Mercedes and Novo, Alexandre},
        year      = {2025},
        month     = {July},
        booktitle = {2025 13th International Workshop on Advanced Ground Penetrating Radar (IWAGPR)},
        publisher = {IEEE},
        pages     = {323--328},
        isbn      = {979-8-3315-2335-0},
        issn      = {2687-7899}
    }
    ```
    ## 💾 Dataset Reference

    The full models and the dataset used in the project are published in Zenodo [DOI: 10.5281/zenodo.16638791](https://doi.org/10.5281/zenodo.16638791).

    ## 💰 Funding Acknowledgement

    This research and development were made possible through the OVERSIGHT project (PID2022-138526OB-I00) funded by MICIU/AEI/10.13039/501100011033/FEDER, UE.
    Grant PREP2022-000030 for the training of predoctoral researchers funded by MICIU/
    AEI/10.13039/501100011033 and by FSE+. 
    M. Solla acknowledges the Grant RYC2019–026604–I funded by MICIU/
    AEI/10.13039/501100011033 and by “ESF Investing in
    your future”.
"""
    with gr.Accordion("Show Publication Info, Dataset & Funding Details", open=True):
        gr.Markdown(attribution_info)

if __name__ == "__main__":
    demo.launch()