import numpy as np import cv2 import gradio as gr from pathlib import Path from ultralytics import YOLO # ------------ CONFIG ------------ MODEL_PATH = Path(__file__).parent / "best.pt" if not MODEL_PATH.exists(): raise FileNotFoundError(f"Model not found at {MODEL_PATH}. Place your .pt next to app.py or change MODEL_PATH.") # Load model once model = YOLO(str(MODEL_PATH)) # ------------ HELPERS ------------ def build_mask_from_result(res, H, W): """ Return a single-channel uint8 mask HxW (0 background, 255 rockfall). If res.masks has polygons (res.masks.xy), rasterize them. Otherwise, fill bounding boxes. """ mask = np.zeros((H, W), dtype=np.uint8) # Try segmentation polygons first (res.masks.xy is list of polygons for each instance) try: if hasattr(res, "masks") and res.masks is not None and hasattr(res.masks, "xy") and len(res.masks.xy) > 0: # res.masks.xy is a list of lists of (x,y) points per instance for poly in res.masks.xy: # poly might be list of floats [x1,y1,x2,y2,...] or list of points pts = np.array(poly, dtype=np.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], 255) return mask except Exception: # fallback to boxes pass # fallback: use bounding boxes try: if hasattr(res, "boxes") and len(res.boxes) > 0: xyxy = res.boxes.xyxy.cpu().numpy() for b in xyxy: x1,y1,x2,y2 = map(int, b) # clip x1, y1 = max(0,x1), max(0,y1) x2, y2 = min(W-1,x2), min(H-1,y2) if x2 > x1 and y2 > y1: cv2.rectangle(mask, (x1,y1), (x2,y2), 255, thickness=-1) except Exception: pass return mask def detections_from_result(res): """ Return list of detections dicts: {class_id, class_name, confidence, bbox} """ detections = [] if hasattr(res, "boxes") and len(res.boxes) > 0: xyxy = res.boxes.xyxy.cpu().numpy() confs = res.boxes.conf.cpu().numpy() cls = res.boxes.cls.cpu().numpy().astype(int) for b, c, cl in zip(xyxy, confs, cls): x1,y1,x2,y2 = map(int, b) detections.append({ "class_id": int(cl), "class_name": model.names[int(cl)] if int(cl) in model.names else str(int(cl)), "confidence": float(c), "bbox": [x1,y1,x2,y2] }) return detections def annotate_image_from_result(res, img): """ res.plot() from Ultralytics returns an RGB numpy image already annotated by the library. Use it directly (no BGR/RGB conversion). If res.plot() fails, draw boxes ourselves from res.boxes. """ try: annotated = res.plot() # should be RGB HWC numpy # ensure dtype uint8 if annotated.dtype != np.uint8: annotated = (annotated * 255).astype(np.uint8) return annotated except Exception: # fallback: draw boxes on copy of original (which may be numpy RGB passed to predict) out = img.copy() dets = detections_from_result(res) for d in dets: x1,y1,x2,y2 = d["bbox"] label = f"{d['class_name']} {d['confidence']:.2f}" cv2.rectangle(out, (x1,y1), (x2,y2), (0,255,0), 2) (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 1) cv2.rectangle(out, (x1, y1 - th - 6), (x1 + tw + 6, y1), (0,255,0), -1) cv2.putText(out, label, (x1+3, y1-4), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0,0,0), 1, cv2.LINE_AA) return out # ------------ PREDICT FUNCTION ------------ def predict_gradio(image, conf=0.25, imgsz=640, return_mask=False, return_probability=True): """ image: numpy (HWC, RGB) provided by Gradio Image component (type="numpy") returns: annotated_image (RGB numpy), optional mask (HWC or 2D), detections (json), probability (float) """ if image is None: return None, None, [], 0.0 # Ultralytics accepts RGB numpy images; Gradio provides numpy in RGB already. # Run model results = model.predict(source=image, conf=float(conf), imgsz=int(imgsz), verbose=False) res = results[0] H, W = image.shape[0], image.shape[1] # get detections and probability detections = detections_from_result(res) probability = max([d["confidence"] for d in detections], default=0.0) # annotated image: use res.plot() (RGB) annotated = annotate_image_from_result(res, image) mask_out = None if return_mask: mask = build_mask_from_result(res, H, W) # single-channel 0/255 uint8 # Gradio Image expects either HxW (grayscale) or HxWx3; we return grayscale mask_out = mask # Return ordering depends on Gradio outputs setup. We will return (annotated, mask, detections, probability) return annotated, mask_out, detections, float(probability) # ------------ GRADIO UI ------------ with gr.Blocks(title="Rockfall Detection (YOLOv8)") as demo: gr.Markdown("# 🪨 Rockfall Detection (YOLOv8)") with gr.Row(): with gr.Column(scale=1): inp = gr.Image(type="numpy", label="Upload mine image (RGB)") conf = gr.Slider(0.05, 0.9, value=0.25, step=0.01, label="Confidence threshold") imgsz = gr.Slider(320, 1280, value=640, step=32, label="Inference image size") return_mask = gr.Checkbox(False, label="Return binary mask (0/255)") run_btn = gr.Button("Run") with gr.Column(scale=1): out_img = gr.Image(type="numpy", label="Annotated image (RGB)") out_mask = gr.Image(type="numpy", label="Binary mask (if requested)") out_json = gr.JSON(label="Detections (class/conf/bbox)") out_prob = gr.Textbox(label="Max rockfall probability") run_btn.click(fn=predict_gradio, inputs=[inp, conf, imgsz, return_mask, gr.State(True)], outputs=[out_img, out_mask, out_json, out_prob]) # launch if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, share=False)