""" EleFind - Aerial Elephant Detection ===================================== A Gradio 6 web interface for detecting elephants in aerial/drone imagery using YOLOv11 with SAHI (Slicing Aided Hyper Inference). Features: - Upload aerial images and detect elephants with bounding boxes - Adjustable SAHI parameters (confidence, slice size, overlap) - Runtime CPU, CUDA, and Apple MPS selection - True Grad-CAM model explanations - Automatic model download from HuggingFace Hub - Confidence bar chart and detection data table Author: Helitha Guruge Project: EleFind (Undergraduate Research Project) """ from __future__ import annotations import hashlib import os import threading import uuid import warnings from pathlib import Path import cv2 import gradio as gr import numpy as np from packaging import version as _pkg_version from PIL import Image warnings.filterwarnings("ignore") # Gradio 6.x replaced show_fullscreen_button / show_download_button with buttons= _GRADIO_6 = _pkg_version.parse(gr.__version__) >= _pkg_version.parse("5.0.0") _IMG_BUTTONS = {"buttons": ["fullscreen"]} if _GRADIO_6 else {"show_fullscreen_button": True} _IMG_BUTTONS_DL = ( {"buttons": ["download"]} if _GRADIO_6 else {"show_fullscreen_button": False, "show_download_button": True} ) _VIEWER_HEAD = r""" """ def _viewer_toolbar(target_id: str) -> str: """Return controls for in-page image zooming and browser-tab viewing.""" return f"""
100%
""" # Optional pandas for chart data try: import pandas as pd _PANDAS = True _EMPTY_CHART = pd.DataFrame({"Elephant": pd.Series([], dtype=str), "Confidence": pd.Series([], dtype=float)}) except ImportError: _PANDAS = False _EMPTY_CHART = None # --------------------------------------------------------------------------- # Imports: detection libraries # --------------------------------------------------------------------------- try: import torch from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction except ImportError as e: raise SystemExit( f"Missing required packages: {e}\n" "Install with: pip install -r requirements.txt" ) # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- # HuggingFace model repository (update after you create the HF repo) HF_MODEL_REPO = os.environ.get("HF_MODEL_REPO", "iamhelitha/EleFind-yolo11-elephant") HF_MODEL_FILE = os.environ.get("HF_MODEL_FILE", "best.pt") # Local fallback: look for model in common locations LOCAL_MODEL_PATHS = [ Path(__file__).parent / "best.pt", Path(__file__).parent / "models" / "best.pt", Path(__file__).parent / "meeting_materials" / "models" / "best.pt", ] # Default SAHI parameters (optimized for elephant detection) DEFAULT_CONF = 0.30 DEFAULT_SLICE = 1024 DEFAULT_OVERLAP = 0.30 DEFAULT_IOU = 0.40 # Image size limit for CPU inference (avoid timeouts on free Spaces) MAX_IMAGE_DIMENSION = 6000 # SHA256 of the trusted best.pt — update this if you retrain or replace the model EXPECTED_MODEL_SHA256 = "7d6be7308bc11a58c32086345d8d09fb495630faa11039a43fd66e7f5750c4ff" # --------------------------------------------------------------------------- # Device detection # --------------------------------------------------------------------------- def get_available_devices() -> list[tuple[str, str]]: """Return Gradio choices for every usable accelerator plus CPU.""" devices: list[tuple[str, str]] = [] try: if torch.cuda.is_available(): for index in range(torch.cuda.device_count()): name = torch.cuda.get_device_name(index) devices.append((f"GPU {index}: {name} (CUDA)", f"cuda:{index}")) if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): devices.append(("GPU: Apple Silicon (MPS)", "mps")) except Exception: pass devices.append(("CPU", "cpu")) return devices def is_gpu_available() -> bool: """Return True when PyTorch can use a CUDA or Apple MPS GPU.""" return any(value != "cpu" for _, value in get_available_devices()) def get_device(requested: str | None = None) -> str: """Resolve and validate a requested device, or choose the fastest available.""" available = [value for _, value in get_available_devices()] if requested in (None, "", "auto"): return next((device for device in available if device != "cpu"), "cpu") if requested not in available: raise ValueError( f"Device '{requested}' is not available. Choose one of: {', '.join(available)}" ) return requested def get_device_status() -> tuple[str, str]: """Return a user-facing hardware report and the recommended device.""" choices = get_available_devices() selected = get_device() gpu_choices = [label for label, value in choices if value != "cpu"] if gpu_choices: summary = f"**GPU ready** · {', '.join(gpu_choices)}" else: summary = "**CPU mode** · No compatible GPU detected" return f"{summary} · Recommended: `{selected}`", selected # --------------------------------------------------------------------------- # Model loading # --------------------------------------------------------------------------- def _resolve_model_path() -> str: """Resolve the model path: try HuggingFace Hub first, then local.""" # 1. Try downloading from HuggingFace Hub if HF_MODEL_REPO: try: from huggingface_hub import hf_hub_download print(f"Downloading model from HuggingFace: {HF_MODEL_REPO}/{HF_MODEL_FILE}") path = hf_hub_download( repo_id=HF_MODEL_REPO, filename=HF_MODEL_FILE, repo_type="model", ) print(f"Model downloaded to: {path}") return path except Exception as e: print(f"HuggingFace download failed: {e}. Trying local paths...") # 2. Try local paths for local_path in LOCAL_MODEL_PATHS: if local_path.exists(): print(f"Using local model: {local_path}") return str(local_path) raise FileNotFoundError( "Model not found. Set HF_MODEL_REPO env var or place best.pt " "in the project root or models/ directory." ) def _verify_model_checksum(path: str) -> None: """Abort if the model file doesn't match the expected SHA256 hash.""" sha256 = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): sha256.update(chunk) digest = sha256.hexdigest() if digest != EXPECTED_MODEL_SHA256: raise RuntimeError( f"Model checksum mismatch!\n" f" Expected: {EXPECTED_MODEL_SHA256}\n" f" Got: {digest}\n" "The model file may be corrupt or tampered with. " "Delete the cached file and restart to re-download." ) print(f"Model checksum verified: {digest[:16]}...") _MODEL_CACHE: dict[str, AutoDetectionModel] = {} _MODEL_PATH: str | None = None _MODEL_LOCK = threading.RLock() def load_model(device: str | None = None) -> AutoDetectionModel: """Load the SAHI-wrapped detection model on the requested device.""" global _MODEL_PATH device = get_device(device) if _MODEL_PATH is None: _MODEL_PATH = _resolve_model_path() _verify_model_checksum(_MODEL_PATH) print(f"Loading model on device: {device}") model = AutoDetectionModel.from_pretrained( model_type="yolov8", # SAHI uses 'yolov8' for YOLOv8/v11 models model_path=_MODEL_PATH, confidence_threshold=DEFAULT_CONF, device=device, ) print("Model loaded successfully!") return model def get_detection_model(device: str | None = None) -> AutoDetectionModel: """Lazily create and cache one model per compute device.""" selected = get_device(device) with _MODEL_LOCK: if selected not in _MODEL_CACHE: _MODEL_CACHE[selected] = load_model(selected) return _MODEL_CACHE[selected] # --------------------------------------------------------------------------- # Detection functions # --------------------------------------------------------------------------- def validate_image(image_np: np.ndarray) -> np.ndarray: """Validate and optionally resize image to avoid CPU timeouts.""" h, w = image_np.shape[:2] if max(h, w) > MAX_IMAGE_DIMENSION: scale = MAX_IMAGE_DIMENSION / max(h, w) new_w, new_h = int(w * scale), int(h * scale) image_np = cv2.resize(image_np, (new_w, new_h), interpolation=cv2.INTER_AREA) print(f"Image resized from {w}x{h} to {new_w}x{new_h}") return image_np def run_detection( image_np: np.ndarray, conf_threshold: float = DEFAULT_CONF, slice_size: int = DEFAULT_SLICE, overlap_ratio: float = DEFAULT_OVERLAP, iou_threshold: float = DEFAULT_IOU, device: str | None = None, ) -> list: """Run SAHI sliced prediction and return a list of detection dicts.""" detection_model = get_detection_model(device) detection_model.confidence_threshold = conf_threshold # Save temp image for SAHI (requires file path) temp_path = Path(__file__).parent / f"temp_input_{uuid.uuid4().hex}.jpg" cv2.imwrite(str(temp_path), cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)) try: result = get_sliced_prediction( image=str(temp_path), detection_model=detection_model, slice_height=slice_size, slice_width=slice_size, overlap_height_ratio=overlap_ratio, overlap_width_ratio=overlap_ratio, postprocess_type="NMS", postprocess_match_threshold=iou_threshold, verbose=0, ) predictions = [] for obj in result.object_prediction_list: bbox = obj.bbox predictions.append( { "x1": int(bbox.minx), "y1": int(bbox.miny), "x2": int(bbox.maxx), "y2": int(bbox.maxy), "confidence": round(obj.score.value, 4), } ) finally: if temp_path.exists(): temp_path.unlink() return predictions def create_gradcam( image: np.ndarray, predictions: list | None = None, device: str | None = None, opacity: float = 0.45, input_size: int = 640, ) -> np.ndarray: """Create detection-targeted Grad-CAM overlays at the resolution used by SAHI.""" predictions = predictions or [] if not predictions: return image.copy() selected = get_device(device) detection_model = get_detection_model(selected) yolo = detection_model.model torch_model = yolo.model torch_model.eval() detect_head = torch_model.model[-1] feature_indexes = getattr(detect_head, "f", None) target_index = feature_indexes[0] if isinstance(feature_indexes, list) else -2 target_layer = torch_model.model[target_index] def explain_crop(crop: np.ndarray, target_box: tuple[int, int, int, int]) -> np.ndarray: """Explain candidates whose decoded centers fall inside one detection box.""" crop_h, crop_w = crop.shape[:2] scale = min(input_size / crop_w, input_size / crop_h) resized_w = max(1, round(crop_w * scale)) resized_h = max(1, round(crop_h * scale)) resized = cv2.resize(crop, (resized_w, resized_h), interpolation=cv2.INTER_LINEAR) canvas = np.full((input_size, input_size, 3), 114, dtype=np.uint8) left = (input_size - resized_w) // 2 top = (input_size - resized_h) // 2 canvas[top : top + resized_h, left : left + resized_w] = resized bx1, by1, bx2, by2 = target_box target_input = ( bx1 * scale + left, by1 * scale + top, bx2 * scale + left, by2 * scale + top, ) input_tensor = torch.from_numpy(canvas).permute(2, 0, 1).unsqueeze(0) input_tensor = input_tensor.to(selected, dtype=torch.float32) / 255.0 input_tensor.requires_grad_(True) activations: dict[str, torch.Tensor] = {} gradients: dict[str, torch.Tensor] = {} def save_features(_module, _inputs, output): if not isinstance(output, torch.Tensor): raise RuntimeError("Grad-CAM target layer did not return a tensor") activations["value"] = output output.register_hook(lambda grad: gradients.__setitem__("value", grad)) hook = target_layer.register_forward_hook(save_features) try: with torch.enable_grad(): output = torch_model(input_tensor) decoded = output[0] if isinstance(output, (tuple, list)) else output raw = output[1] if isinstance(output, (tuple, list)) and len(output) > 1 else None if not isinstance(raw, dict) or "scores" not in raw: raise RuntimeError("This Ultralytics model does not expose Grad-CAM score logits") feature_map = activations["value"] candidate_count = feature_map.shape[2] * feature_map.shape[3] scores = raw["scores"].sigmoid()[0, :, :candidate_count].amax(dim=0) centers = decoded[0, :2, :candidate_count] tx1, ty1, tx2, ty2 = target_input inside = ( (centers[0] >= tx1) & (centers[0] <= tx2) & (centers[1] >= ty1) & (centers[1] <= ty2) ) candidate_indexes = torch.nonzero(inside, as_tuple=False).flatten() if candidate_indexes.numel() == 0: target_x, target_y = (tx1 + tx2) / 2, (ty1 + ty2) / 2 distances = (centers[0] - target_x).square() + (centers[1] - target_y).square() candidate_indexes = distances.argmin().reshape(1) candidate_scores = scores[candidate_indexes] top_k = min(5, candidate_scores.numel()) target = torch.topk(candidate_scores, top_k).values.sum() torch_model.zero_grad(set_to_none=True) target.backward() gradient = gradients["value"] # HiResCAM's element-wise gradient weighting preserves the location # of small objects better than globally averaged Grad-CAM weights. cam = torch.relu((gradient * feature_map).sum(dim=1))[0] cam = cam.detach().float().cpu().numpy() finally: hook.remove() torch_model.zero_grad(set_to_none=True) cam = cv2.resize(cam, (input_size, input_size), interpolation=cv2.INTER_CUBIC) cam = cam[top : top + resized_h, left : left + resized_w] cam = cv2.resize(cam, (crop_w, crop_h), interpolation=cv2.INTER_CUBIC) cam -= cam.min() peak = cam.max() return cam / peak if peak > 0 else cam h, w = image.shape[:2] composite = np.zeros((h, w), dtype=np.float32) for pred in sorted(predictions, key=lambda item: item["confidence"], reverse=True)[:25]: box_w = max(1, pred["x2"] - pred["x1"]) box_h = max(1, pred["y2"] - pred["y1"]) context_size = int(np.clip(max(box_w, box_h) * 8, 256, 1024)) center_x = (pred["x1"] + pred["x2"]) // 2 center_y = (pred["y1"] + pred["y2"]) // 2 crop_x1 = max(0, center_x - context_size // 2) crop_y1 = max(0, center_y - context_size // 2) crop_x2 = min(w, crop_x1 + context_size) crop_y2 = min(h, crop_y1 + context_size) crop_x1 = max(0, crop_x2 - context_size) crop_y1 = max(0, crop_y2 - context_size) crop = image[crop_y1:crop_y2, crop_x1:crop_x2] local_box = ( pred["x1"] - crop_x1, pred["y1"] - crop_y1, pred["x2"] - crop_x1, pred["y2"] - crop_y1, ) crop_cam = explain_crop(crop, local_box) region = composite[crop_y1:crop_y2, crop_x1:crop_x2] np.maximum(region, crop_cam, out=region) heatmap = cv2.applyColorMap(np.uint8(composite * 255), cv2.COLORMAP_JET) heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB) # Keep this output visually distinct from the labeled detection image: # a dim grayscale context with saturated color only where gradients activate. gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) gray_rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB).astype(np.float32) # Suppress weak background responses so crop boundaries do not appear. activation = np.sqrt(np.clip((composite - 0.15) / 0.85, 0.0, 1.0))[..., None] output = gray_rgb * 0.32 + heatmap.astype(np.float32) * activation * (0.68 + opacity) return np.clip(output, 0, 255).astype(np.uint8) def draw_detections(image: np.ndarray, predictions: list) -> np.ndarray: """Draw bounding boxes and labels on the image.""" img = image.copy() for pred in predictions: x1, y1, x2, y2 = pred["x1"], pred["y1"], pred["x2"], pred["y2"] conf = pred["confidence"] # Green bounding box cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 3) # Label background + text label = f"Elephant {conf:.0%}" (lw, lh), baseline = cv2.getTextSize( label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2 ) cv2.rectangle(img, (x1, y1 - lh - 10), (x1 + lw + 5, y1), (0, 255, 0), -1) cv2.putText( img, label, (x1 + 2, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2 ) return img # --------------------------------------------------------------------------- # Normalisation helpers (handle various Gradio input types) # --------------------------------------------------------------------------- def _to_numpy_rgb(image): """Convert Gradio image input (PIL or numpy) to numpy RGB array.""" if image is None: return None if isinstance(image, Image.Image): return np.array(image.convert("RGB")) if isinstance(image, np.ndarray): if image.ndim == 2: return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) if image.shape[2] == 4: return cv2.cvtColor(image, cv2.COLOR_RGBA2RGB) return image return None # --------------------------------------------------------------------------- # Main processing function # --------------------------------------------------------------------------- def process_image( image, conf_threshold: float, slice_size: int, overlap_ratio: float, iou_threshold: float, device: str, generate_gradcam: bool, progress=gr.Progress(), ): """Run detection and optional Grad-CAM on the user-selected device.""" image_np = _to_numpy_rgb(image) if image_np is None: return None, None, 0, 0.0, 0.0, 0.0, "Upload an image first.", None, None try: selected_device = get_device(device) progress(0.05, desc="Validating image") image_np = validate_image(image_np) h, w = image_np.shape[:2] progress(0.10, desc=f"Running SAHI detection ({w}×{h})") predictions = run_detection( image_np, conf_threshold=conf_threshold, slice_size=int(slice_size), overlap_ratio=overlap_ratio, iou_threshold=iou_threshold, device=selected_device, ) progress(0.72, desc="Drawing detections") det_image = draw_detections(image_np, predictions) gradcam_image = None if generate_gradcam: progress(0.78, desc="Computing Grad-CAM explanation") gradcam_image = create_gradcam( image_np, predictions=predictions, device=selected_device, ) except Exception as e: import traceback err_msg = f"Error: {e}\n\n```\n{traceback.format_exc()}\n```" return None, None, 0, 0.0, 0.0, 0.0, err_msg, None, None # Compute stats n = len(predictions) avg_conf = sum(p["confidence"] for p in predictions) / n if n else 0.0 max_conf = max((p["confidence"] for p in predictions), default=0.0) min_conf = min((p["confidence"] for p in predictions), default=0.0) params_text = ( f"**Parameters:** Slice {int(slice_size)}x{int(slice_size)} px · " f"Overlap {overlap_ratio:.0%} · Confidence >= {conf_threshold:.0%} · " f"IoU {iou_threshold:.0%} · Image {w}x{h} px · Device `{selected_device}` · " f"Grad-CAM {'on' if generate_gradcam else 'off'}" ) progress(1.0, desc="Done") # Build chart / table data (pandas optional) conf_chart = None det_table = None if _PANDAS and predictions: det_table = pd.DataFrame( [ { "ID": i + 1, "Confidence": f"{p['confidence']:.1%}", "BBox (x1,y1,x2,y2)": f"({p['x1']},{p['y1']},{p['x2']},{p['y2']})", "Width (px)": p["x2"] - p["x1"], "Height (px)": p["y2"] - p["y1"], } for i, p in enumerate(predictions) ] ) conf_chart = pd.DataFrame( { "Elephant": [f"#{i+1}" for i in range(len(predictions))], "Confidence": [round(p["confidence"] * 100, 1) for p in predictions], } ) return ( Image.fromarray(det_image.astype(np.uint8)), Image.fromarray(gradcam_image) if gradcam_image is not None else None, n, avg_conf, max_conf, min_conf, params_text, conf_chart, det_table, ) # --------------------------------------------------------------------------- # Gradio UI – Gradio 6.x # --------------------------------------------------------------------------- _THEME = gr.themes.Soft( primary_hue="emerald", secondary_hue="green", neutral_hue="gray", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], ) def build_ui() -> gr.Blocks: """Construct the Gradio Blocks interface.""" blocks_kwargs = dict( theme=_THEME, title="EleFind – Aerial Elephant Detection", fill_width=True, fill_height=False, ) if not _GRADIO_6: blocks_kwargs["head"] = _VIEWER_HEAD with gr.Blocks(**blocks_kwargs) as demo: # ── Compact application header ────────────────────────────────────── gr.HTML( """

EleFind

YOLO11 · SAHI · Grad-CAM
""" ) # ── Main two-column layout ───────────────────────────────────────── with gr.Row(equal_height=True, elem_id="workspace"): # ── LEFT: Input panel ───────────────────────────────────────── with gr.Column(scale=4, min_width=320, elem_id="input-panel"): input_image = gr.Image( label="Upload Aerial / Drone Image", type="pil", sources=["upload", "clipboard"], height=320, elem_id="input-image", **_IMG_BUTTONS, ) # ── Example images directly below the upload area ───────── example_dir = Path(__file__).parent / "examples" example_files = sorted(example_dir.glob("*.jpg")) if example_dir.exists() else [] if example_files: with gr.Accordion( "Example aerial images", open=True, elem_id="examples-panel" ): gr.Examples( examples=[[str(f)] for f in example_files], inputs=[input_image], label=None, ) with gr.Group(elem_id="hardware-panel"): device_status, recommended_device = get_device_status() hardware_status = gr.Markdown(device_status) with gr.Row(equal_height=True): device_selector = gr.Dropdown( choices=get_available_devices(), value=recommended_device, label="Processing device", interactive=True, scale=3, min_width=170, ) refresh_devices_btn = gr.Button( "↻ Refresh", size="sm", scale=0, min_width=88 ) gradcam_toggle = gr.Checkbox( value=True, label="Grad-CAM", scale=1, min_width=105, ) with gr.Accordion( "SAHI Detection Parameters", open=False, elem_id="sahi-parameters" ): with gr.Row(equal_height=True): with gr.Group(elem_classes=["parameter-section"]): gr.Markdown( "Detection filtering", elem_classes=["parameter-title"], ) conf_slider = gr.Slider( minimum=0.05, maximum=0.95, value=DEFAULT_CONF, step=0.05, label="Confidence threshold", info="Minimum score retained", ) iou_slider = gr.Slider( minimum=0.10, maximum=0.80, value=DEFAULT_IOU, step=0.05, label="IoU threshold", info="Duplicate suppression overlap", ) with gr.Group(elem_classes=["parameter-section"]): gr.Markdown( "SAHI tiling", elem_classes=["parameter-title"], ) slice_slider = gr.Slider( minimum=256, maximum=2048, value=DEFAULT_SLICE, step=128, label="Slice size (px)", info="Tile width and height", ) overlap_slider = gr.Slider( minimum=0.05, maximum=0.50, value=DEFAULT_OVERLAP, step=0.05, label="Tile overlap ratio", info="Overlap between adjacent tiles", ) detect_btn = gr.Button( "Detect Elephants →", variant="primary", size="lg", elem_id="detect-button", ) # ── RIGHT: Output tabs ──────────────────────────────────────── with gr.Column(scale=6, min_width=400, elem_id="output-panel"): with gr.Tabs(elem_id="result-tabs") as result_tabs: # ── Tab 1: Detection image ──────────────────────────── with gr.Tab("Detections", id="tab_det"): detection_output = gr.Image( label="Annotated detections", type="pil", interactive=False, height=420, elem_id="detection-output", **_IMG_BUTTONS_DL, ) gr.HTML(_viewer_toolbar("detection-output")) with gr.Tab("Grad-CAM", id="tab_gradcam"): gradcam_output = gr.Image( label="Gradient-weighted activation map", type="pil", interactive=False, height=420, elem_id="gradcam-output", **_IMG_BUTTONS_DL, ) gr.HTML(_viewer_toolbar("gradcam-output")) gr.Markdown( "This view is intentionally different from the labeled detection image: " "the context is dark grayscale and only gradient activations are colored. " "Use the controls above to zoom or open the full-resolution image in a browser tab." ) # ── Tab 2: Statistics ───────────────────────────────── with gr.Tab("Statistics", id="tab_stats"): with gr.Row(): stat_count = gr.Number( label="Elephants Detected", value=0, interactive=False, ) stat_avg = gr.Number( label="Avg Confidence", value=0.0, interactive=False, precision=2, ) stat_max = gr.Number( label="Highest Confidence", value=0.0, interactive=False, precision=2, ) stat_min = gr.Number( label="Lowest Confidence", value=0.0, interactive=False, precision=2, ) params_md = gr.Markdown() with gr.Accordion("Detection Table", open=True): det_table_out = gr.Dataframe( headers=["ID", "Confidence", "BBox (x1,y1,x2,y2)", "Width (px)", "Height (px)"], label=None, interactive=False, wrap=True, ) with gr.Accordion("Confidence Chart", open=True): if _PANDAS: conf_chart_out = gr.BarPlot( value=_EMPTY_CHART, x="Elephant", y="Confidence", title="Detection Confidence per Elephant", x_title="Elephant ID", y_title="Confidence (%)", color="Confidence", height=280, label="", show_label=False, ) else: conf_chart_out = gr.Markdown( "_Install pandas for the confidence chart._" ) # ── Event wiring ─────────────────────────────────────────────────── _outputs = [ detection_output, gradcam_output, stat_count, stat_avg, stat_max, stat_min, params_md, conf_chart_out, det_table_out, ] detect_btn.click( fn=process_image, inputs=[ input_image, conf_slider, slice_slider, overlap_slider, iou_slider, device_selector, gradcam_toggle, ], outputs=_outputs, concurrency_limit=1, api_name="detect", ) def refresh_devices(): status, selected = get_device_status() return gr.Dropdown(choices=get_available_devices(), value=selected), status refresh_devices_btn.click( fn=refresh_devices, outputs=[device_selector, hardware_status], concurrency_limit=1, ) return demo # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- demo = build_ui() if __name__ == "__main__": demo.queue(max_size=10) launch_kwargs = dict( server_name="0.0.0.0", show_error=True, ) if _GRADIO_6: launch_kwargs["head"] = _VIEWER_HEAD demo.launch(**launch_kwargs)