File size: 20,923 Bytes
e1fd730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73b320b
fa77c71
 
 
 
 
 
63fdff1
ae94f26
 
 
 
 
 
 
 
 
 
 
fa77c71
 
63fdff1
 
 
3134165
 
 
 
63fdff1
 
 
 
fa77c71
3134165
fa77c71
3a094c8
63fdff1
 
 
97ddd81
 
 
 
 
fa77c71
97ddd81
 
 
fa77c71
 
97ddd81
63fdff1
 
 
fa77c71
 
97ddd81
fa77c71
 
97ddd81
 
fa77c71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1fd730
e10024a
 
e1fd730
 
 
73b320b
e10024a
e1fd730
e10024a
 
ae528b8
e1fd730
 
fb83b42
e1fd730
 
 
fb83b42
e1fd730
 
 
 
753c2e3
e1fd730
fb83b42
e1fd730
 
 
 
fb83b42
73b320b
 
 
 
 
 
 
 
 
fb83b42
e1fd730
fb83b42
fa77c71
e0960e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73b320b
e0960e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73b320b
 
 
 
 
 
 
 
 
e0960e9
 
 
 
fa77c71
 
 
 
63fdff1
 
 
fa77c71
 
e0960e9
fa77c71
73b320b
e0960e9
 
 
 
 
73b320b
e0960e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa77c71
 
97ddd81
e0960e9
97ddd81
 
e0960e9
97ddd81
 
 
 
 
 
 
 
e0960e9
 
 
 
 
 
97ddd81
 
 
e0960e9
97ddd81
 
 
 
 
 
 
 
 
 
 
e0960e9
 
97ddd81
 
 
 
 
 
 
e0960e9
 
97ddd81
 
 
 
 
 
 
 
e0960e9
 
 
 
 
 
 
 
 
97ddd81
 
e0960e9
97ddd81
 
e0960e9
97ddd81
e0960e9
 
12304ef
fa77c71
 
97ddd81
fa77c71
 
 
 
 
e0960e9
 
 
 
 
 
 
 
73b320b
 
 
e0960e9
 
 
73b320b
 
 
 
 
 
 
 
 
e0960e9
fa77c71
 
97ddd81
 
 
 
 
e1fd730
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# import gradio as gr
# import cv2
# import numpy as np
# from PIL import Image
# import torch
# from transformers import DetrImageProcessor, DetrForObjectDetection
# import os

# try:
#     import spaces
# except ImportError:
#     # Mocking spaces for local development
#     class spaces:
#         @staticmethod
#         def GPU(func):
#             def wrapper(*args, **kwargs):
#                 return func(*args, **kwargs)
#             return wrapper

# # Load DETR model for object detection
# try:
#     processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
#     model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
#     # Warm up the model
#     dummy_input = processor(images=np.zeros((100, 100, 3)), return_tensors="pt")
#     with torch.no_grad():
#         _ = model(**dummy_input)
# except Exception as e:
#     print(f"Error loading model: {e}")
#     processor = None
#     model = None

# @spaces.GPU
# def detect_objects(image):
    
#     if model is None or processor is None:
#         return image, "Model failed to load. Please check logs."

#     # Convert to PIL if it's already a numpy array (to avoid rescaling warnings in processor)
#     if isinstance(image, np.ndarray):
#         image_pil = Image.fromarray(image)
#     else:
#         image_pil = image
    
#     # Ensure it's RGB
#     if image_pil.mode != "RGB":
#         image_pil = image_pil.convert("RGB")
    
#     # Prepare image for the model
#     inputs = processor(images=image_pil, return_tensors="pt")
    
#     with torch.no_grad():
#         outputs = model(**inputs)
    
#     # Post-process outputs
#     target_sizes = torch.tensor([image_pil.size[::-1]]) # (height, width)
#     results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.7)[0]
    
#     # Draw bounding boxes on the image (using numpy for cv2)
#     annotated_image = np.array(image_pil)
    
#     detection_info = []
    
#     for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
#         box = [round(i, 2) for i in box.tolist()]
#         label_name = model.config.id2label[label.item()]
#         confidence = round(score.item(), 3)
        
#         # Draw rectangle
#         x1, y1, x2, y2 = map(int, box)
#         cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 2)
        
#         # Add label
#         label_text = f"{label_name}: {confidence}"
#         cv2.putText(annotated_image, label_text, (x1, y1 - 10), 
#                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        
#         detection_info.append(f"• {label_name} (confidence: {confidence})")
    
#     # Create summary text
#     summary = f"**Detected {len(detection_info)} object(s):**\n\n" + "\n".join(detection_info) if detection_info else "No objects detected with confidence > 0.7"
    
#     return annotated_image, summary

# def scan_edges(image):
#     """
#     Simple edge detection using OpenCV
#     """
#     # Convert PIL image to numpy array
#     if isinstance(image, Image.Image):
#         image = np.array(image)
    
#     # Convert to grayscale
#     gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    
#     clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    
#     # Apply Gaussian blur
#     blurred = cv2.GaussianBlur(gray, (5, 5), 0)

#     # enhanced = clahe.apply(gray_image)
    
#     # Edge detection using Canny
#     edges = cv2.Canny(blurred, 50, 150)
    
#     # Convert back to RGB for display
#     edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)
    
#     return edges_rgb

# # def scan_edges(image):

# #     # --- 1. Convert PIL image to numpy array if needed ---
# #     if isinstance(image, Image.Image):
# #         image = np.array(image)

# #     # --- 2. Convert to grayscale ---
# #     gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

# #     # --- 3. Gaussian blur BEFORE CLAHE to reduce high-freq noise ---
# #     #        that CLAHE would otherwise amplify
# #     blurred = cv2.GaussianBlur(gray, (5, 5), 0)

# #     # --- 4. Apply CLAHE on the blurred image ---
# #     #        clipLimit=2.0  → controls noise amplification in flat regions
# #     #        tileGridSize   → 8x8 tiles work well for engine block scale features;
# #     #                         increase (e.g. 16x16) if bearing saddles are small
# #     #                         relative to full image resolution
# #     clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# #     enhanced = clahe.apply(blurred)

# #     # --- 5. Canny edge detection on CLAHE-enhanced image ---
# #     #        Lower threshold (30) helps recover weak edges in shadow regions
# #     #        Upper threshold (120) keeps strong structural edges
# #     #        Tune these if you get too much noise or missing arcs
# #     edges = cv2.Canny(enhanced, 30, 120)

# #     # --- 6. Convert single-channel edge map back to RGB for display ---
# #     edges_rgb = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)

# #     return edges_rgb

# def process_image(image, mode):
#     """
#     Process image based on selected mode
#     """
#     if image is None:
#         return None, "Please upload an image."
        
#     if mode == "Object Detection":
#         return detect_objects(image)
#     else:  # Edge Detection
#         edges = scan_edges(image)
#         return edges, "Edge detection completed"

# # Create Gradio interface
# with gr.Blocks(title="Object Scanner") as demo:
#     gr.Markdown("Detect objects or scan edges using your camera or uploaded images")
    
#     with gr.Tabs():
#         with gr.TabItem(" Image Scanner"):
#             with gr.Row():
#                 with gr.Column():
#                     input_image = gr.Image(
#                         sources=["upload", "webcam"], 
#                         type="pil", 
#                         label="Upload or Capture Image"
#                     )
#                     mode = gr.Radio(
#                         choices=["Object Detection", "Edge Detection"],
#                         value="Object Detection",
#                         label="Scanning Mode"
#                     )
#                     scan_btn = gr.Button(" Process Image", variant="primary")
                
#                 with gr.Column():
#                     output_image = gr.Image(type="numpy", label="Processed Result")
#                     output_text = gr.Markdown(label="Detection Results")
            
#             # Examples
#             if os.path.exists("examples"):
#                 gr.Examples(
#                     examples=[
#                         ["examples/sample1.jpg", "Object Detection"],
#                         ["examples/sample2.jpg", "Edge Detection"],
#                     ],
#                     inputs=[input_image, mode],
#                     outputs=[output_image, output_text],
#                     fn=process_image,
#                     cache_examples=False,
#                 )

#         with gr.TabItem("🎥 Live Edge Scan"):
#             gr.Markdown("### Real-time Edge Detection")
#             with gr.Row():
#                 with gr.Column():
#                     camera_input = gr.Image(
#                         sources=["webcam"], 
#                         streaming=True, 
#                         type="numpy",
#                         label="Live Feed"
#                     )
#                 with gr.Column():
#                     camera_output = gr.Image(
#                         label="Edge Stream"
#                     )
            
#             # Live stream logic for edges
#             camera_input.stream(
#                 fn=scan_edges,
#                 inputs=camera_input,
#                 outputs=camera_output
#             )
    
#     # Static scan logic
#     scan_btn.click(
#         fn=process_image,
#         inputs=[input_image, mode],
#         outputs=[output_image, output_text]
#     )

# if __name__ == "__main__":
#     demo.launch(
#         server_name="0.0.0.0", 
#         server_port=7860, 
#         theme=gr.themes.Soft(),
#         ssr_mode=False
#     )

import gradio as gr
import cv2
import numpy as np
from PIL import Image
import torch
from transformers import DetrImageProcessor, DetrForObjectDetection
import os

try:
    import spaces
except ImportError:
    # Mocking spaces for local development
    class spaces:
        @staticmethod
        def GPU(func):
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper

# Load DETR model for object detection
try:
    processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
    model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")
    # Warm up the model
    dummy_input = processor(images=np.zeros((100, 100, 3)), return_tensors="pt")
    with torch.no_grad():
        _ = model(**dummy_input)
except Exception as e:
    print(f"Error loading model: {e}")
    processor = None
    model = None

@spaces.GPU
def detect_objects(image):
    
    if model is None or processor is None:
        return image, "Model failed to load. Please check logs."

    # Convert to PIL if it's already a numpy array (to avoid rescaling warnings in processor)
    if isinstance(image, np.ndarray):
        image_pil = Image.fromarray(image)
    else:
        image_pil = image
    
    # Ensure it's RGB
    if image_pil.mode != "RGB":
        image_pil = image_pil.convert("RGB")
    
    # Prepare image for the model
    inputs = processor(images=image_pil, return_tensors="pt")
    
    with torch.no_grad():
        outputs = model(**inputs)
    
    # Post-process outputs
    target_sizes = torch.tensor([image_pil.size[::-1]]) # (height, width)
    results = processor.post_process_object_detection(outputs, target_sizes=target_sizes, threshold=0.7)[0]
    
    # Draw bounding boxes on the image (using numpy for cv2)
    annotated_image = np.array(image_pil)
    
    detection_info = []
    
    for score, label, box in zip(results["scores"], results["labels"], results["boxes"]):
        box = [round(i, 2) for i in box.tolist()]
        label_name = model.config.id2label[label.item()]
        confidence = round(score.item(), 3)
        
        # Draw rectangle
        x1, y1, x2, y2 = map(int, box)
        cv2.rectangle(annotated_image, (x1, y1), (x2, y2), (0, 255, 0), 2)
        
        # Add label
        label_text = f"{label_name}: {confidence}"
        cv2.putText(annotated_image, label_text, (x1, y1 - 10), 
                   cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
        
        detection_info.append(f"• {label_name} (confidence: {confidence})")
    
    # Create summary text
    summary = f"**Detected {len(detection_info)} object(s):**\n\n" + "\n".join(detection_info) if detection_info else "No objects detected with confidence > 0.7"
    
    return annotated_image, summary


def scan_edges(image):
    """
    Edge detection with CLAHE preprocessing to recover edges lost in
    shadowed regions (e.g. bearing saddle arcs on engine blocks).
    Pipeline:
        RGB → Grayscale → Gaussian Blur → CLAHE → Canny → Closing
    """
    # 1. Convert PIL image to numpy array if needed
    if isinstance(image, Image.Image):
        image = np.array(image)

    # 2. Convert to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

    # 3. Gaussian blur BEFORE CLAHE — prevents CLAHE from amplifying
    #    surface noise on metallic/reflective parts
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)

    # 4. CLAHE — boosts local contrast in dark/shadowed regions
    #    clipLimit=2.0  : caps noise amplification in uniform areas
    #    tileGridSize   : 8x8 tiles suit engine-block scale features;
    #                     use (16,16) if bearing saddles are small in frame
    clahe = cv2.createCLAHE(clipLimit=9.9, tileGridSize=(8, 8))
    enhanced = clahe.apply(blurred)

    # 5. Canny on CLAHE-enhanced image
    #    Thresholds lowered vs original (50,150) → (30,120) so weak edges
    #    in shadowed saddle arcs are no longer missed
    edges = cv2.Canny(enhanced, 30, 120)

    # 6. Morphological Closing Operation - Fills gaps in broken edges
    #    STEP 1: Dilation (Grow phase) - Expands white pixels to connect gaps
    #    STEP 2: Erosion (Shrink phase) - Shrinks back to original thickness
    #    kernel size 3x3 is suitable for small gaps; use 5x5 for larger gaps
    kernel = np.ones((3, 3), np.uint8)
    closed_edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=1)

    # 7. Convert back to RGB for Gradio display
    edges_rgb = cv2.cvtColor(closed_edges, cv2.COLOR_GRAY2RGB)

    return edges_rgb


def extract_green_channel(image):
    """
    Extract the green channel from an RGB image.
    Green channel often provides good contrast for vegetation and certain materials.
    """
    # 1. Convert PIL image to numpy array if needed
    if isinstance(image, Image.Image):
        image = np.array(image)
    
    # 2. Extract green channel (index 1 in RGB)
    green_channel = image[:, :, 1]
    
    # 3. Convert to RGB for display (all channels = green)
    green_rgb = cv2.cvtColor(green_channel, cv2.COLOR_GRAY2RGB)
    
    return green_rgb


def green_bilateral_edges(image):
    """
    Edge detection using green channel with bilateral filtering.
    Pipeline:
        RGB → Green Channel → Bilateral Filter → Canny Edge Detection → Closing
    
    Bilateral filtering preserves edges while reducing noise, making it ideal
    for edge detection on noisy or textured surfaces.
    """
    # 1. Convert PIL image to numpy array if needed
    if isinstance(image, Image.Image):
        image = np.array(image)
    
    # 2. Extract green channel
    green_channel = image[:, :, 1]
    
    # 3. Apply bilateral filter
    #    d=9           : diameter of pixel neighborhood
    #    sigmaColor=75 : filter sigma in the color space (larger = more colors mixed)
    #    sigmaSpace=75 : filter sigma in the coordinate space (larger = farther pixels influence)
    #    Bilateral filtering smooths flat regions while preserving sharp edges
    bilateral = cv2.bilateralFilter(green_channel, d=9, sigmaColor=75, sigmaSpace=75)
    
    # 4. Apply Canny edge detection
    #    Using moderate thresholds for balanced edge detection
    edges = cv2.Canny(bilateral, 50, 150)
    
    # 5. Morphological Closing Operation - Fills gaps in broken edges
    #    STEP 1: Dilation (Grow phase) - Expands white pixels to connect gaps
    #    STEP 2: Erosion (Shrink phase) - Shrinks back to original thickness
    #    kernel size 3x3 is suitable for small gaps; use 5x5 for larger gaps
    kernel = np.ones((3, 3), np.uint8)
    closed_edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=1)
    
    # 6. Convert back to RGB for Gradio display
    edges_rgb = cv2.cvtColor(closed_edges, cv2.COLOR_GRAY2RGB)
    
    return edges_rgb


def process_image(image, mode):
    """
    Process image based on selected mode
    """
    if image is None:
        return None, "Please upload an image."
        
    if mode == "Object Detection":
        return detect_objects(image)
    elif mode == "Edge Detection":
        edges = scan_edges(image)
        return edges, "Edge detection completed (CLAHE + Canny + Closing)"
    elif mode == "Green Channel":
        green = extract_green_channel(image)
        return green, "Green channel extracted"
    elif mode == "Green + Bilateral Edges":
        edges = green_bilateral_edges(image)
        return edges, "Edge detection completed (Green Channel + Bilateral Filter + Canny + Closing)"
    else:
        return image, "Unknown mode selected"


def process_live_stream(image, mode):
    """
    Process live stream based on selected mode
    """
    if image is None:
        return None
        
    if mode == "Edge Detection":
        return scan_edges(image)
    elif mode == "Green Channel":
        return extract_green_channel(image)
    elif mode == "Green + Bilateral Edges":
        return green_bilateral_edges(image)
    else:
        return scan_edges(image)  # Default to edge detection


# Create Gradio interface
with gr.Blocks(title="Object Scanner") as demo:
    gr.Markdown("# 🔍 Object Scanner\nDetect objects, scan edges, or extract green channel using your camera or uploaded images")
    
    with gr.Tabs():
        with gr.TabItem("📷 Image Scanner"):
            with gr.Row():
                with gr.Column():
                    input_image = gr.Image(
                        sources=["upload", "webcam"], 
                        type="pil", 
                        label="Upload or Capture Image"
                    )
                    mode = gr.Radio(
                        choices=[
                            "Object Detection", 
                            "Edge Detection", 
                            "Green Channel",
                            "Green + Bilateral Edges"
                        ],
                        value="Object Detection",
                        label="Scanning Mode"
                    )
                    scan_btn = gr.Button("🔍 Process Image", variant="primary")
                
                with gr.Column():
                    output_image = gr.Image(type="numpy", label="Processed Result")
                    output_text = gr.Markdown(label="Detection Results")
            
            # Examples
            if os.path.exists("examples"):
                gr.Examples(
                    examples=[
                        ["examples/sample1.jpg", "Object Detection"],
                        ["examples/sample2.jpg", "Edge Detection"],
                        ["examples/sample1.jpg", "Green Channel"],
                        ["examples/sample2.jpg", "Green + Bilateral Edges"],
                    ],
                    inputs=[input_image, mode],
                    outputs=[output_image, output_text],
                    fn=process_image,
                    cache_examples=False,
                )

        with gr.TabItem("🎥 Live Processing"):
            gr.Markdown("### Real-time Image Processing")
            with gr.Row():
                with gr.Column():
                    camera_input = gr.Image(
                        sources=["webcam"], 
                        streaming=True, 
                        type="numpy",
                        label="Live Feed"
                    )
                    live_mode = gr.Radio(
                        choices=[
                            "Edge Detection",
                            "Green Channel",
                            "Green + Bilateral Edges"
                        ],
                        value="Edge Detection",
                        label="Processing Mode"
                    )
                with gr.Column():
                    camera_output = gr.Image(
                        label="Processed Stream"
                    )
            
            # Live stream logic
            camera_input.stream(
                fn=lambda img, mode: process_live_stream(img, mode),
                inputs=[camera_input, live_mode],
                outputs=camera_output
            )
    
    # Static scan logic
    scan_btn.click(
        fn=process_image,
        inputs=[input_image, mode],
        outputs=[output_image, output_text]
    )
    
    # Info section
    with gr.Accordion("ℹ️ Mode Information", open=False):
        gr.Markdown("""
        ### Available Modes:
        
        **Object Detection** - Uses DETR model to detect and label objects with bounding boxes
        
        **Edge Detection** - CLAHE-enhanced Canny edge detection with morphological closing to fill gaps
        - Pipeline: Grayscale → Gaussian Blur → CLAHE → Canny → Closing
        - Closing operation connects broken edges (useful for interrupted arcs and curves)
        
        **Green Channel** - Extracts the green channel, useful for vegetation and certain materials
        
        **Green + Bilateral Edges** - Combines green channel extraction with bilateral filtering and closing
        - Pipeline: Green Channel → Bilateral Filter → Canny → Closing
        - Bilateral filtering preserves edges while reducing noise
        - Closing fills gaps in broken edges, ideal for textured surfaces
        
        ### Morphological Closing:
        - **Step 1 (Dilation)**: Expands white pixels to connect nearby edges
        - **Step 2 (Erosion)**: Shrinks pixels back to original size while keeping gaps filled
        - **Result**: Broken arcs and interrupted lines become continuous edges
        """)

if __name__ == "__main__":
    demo.launch(
        server_name="0.0.0.0", 
        server_port=7860, 
        theme=gr.themes.Soft(),
        ssr_mode=False
    )