Spaces:
Sleeping
Sleeping
Download app.py from JZVince/spotpredator: direct link, hf CLI and curl.
- Browser
- Download file 5.23 kB
-
https://huggingface.co/spaces/JZVince/spotpredator/resolve/main/app.py
- Command line
-
hf download hf://spaces/JZVince/spotpredator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/JZVince/spotpredator/resolve/main/app.py
5.23 kB
| """ | |
| app.py — Hugging Face Space to test the SpotPredator PicoDet model in the browser. | |
| Pulls the .tflite straight from the model repo, so testers install nothing — they | |
| drag an image in and see boxes. | |
| Model: JZVince/spotpredator-picodet (PicoDet, Apache-2.0) | |
| PicoDet is exported WITHOUT in-graph NMS, so the TFLite model has TWO outputs: | |
| boxes: (1, N, 4) already-decoded x1,y1,x2,y2 in input-pixel space (0..640) | |
| scores: (1, num_classes, N) per-class confidence | |
| Preprocess is /255 then ImageNet mean/std (RGB) — matches the training pipeline. | |
| """ | |
| import os | |
| import numpy as np | |
| from PIL import Image, ImageDraw | |
| import gradio as gr | |
| from huggingface_hub import hf_hub_download, list_repo_files | |
| MODEL_REPO = "JZVince/spotpredator-picodet" | |
| CLASS_NAMES = ["coyote", "fox", "raptor"] # must match training order / labels.txt | |
| # PicoDet (PaddleDetection) NormalizeImage: /255 then ImageNet mean/std, RGB. | |
| IMAGENET_MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32) | |
| IMAGENET_STD = np.array([0.229, 0.224, 0.225], dtype=np.float32) | |
| # Optional: set HF_TOKEN as a Space secret for higher rate limits / faster pulls. | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| # ---- fetch the .tflite from the model repo (auto-detect the filename) -------- | |
| tflite_name = next(f for f in list_repo_files(MODEL_REPO, token=HF_TOKEN) | |
| if f.endswith(".tflite")) | |
| MODEL_PATH = hf_hub_download(MODEL_REPO, tflite_name, token=HF_TOKEN) | |
| # LiteRT is the successor to tflite-runtime; fall back through older runtimes. | |
| try: | |
| from ai_edge_litert.interpreter import Interpreter | |
| except ImportError: | |
| try: | |
| from tflite_runtime.interpreter import Interpreter | |
| except ImportError: | |
| from tensorflow.lite import Interpreter | |
| interp = Interpreter(model_path=MODEL_PATH) | |
| interp.allocate_tensors() | |
| INP = interp.get_input_details()[0] | |
| OUTS = interp.get_output_details() | |
| IN_W = int(INP["shape"][2]) | |
| IN_H = int(INP["shape"][1]) | |
| NC = len(CLASS_NAMES) | |
| # PicoDet has two outputs; identify boxes (last dim == 4) vs scores by shape, so | |
| # output order doesn't matter. | |
| BOX_I = 0 if OUTS[0]["shape"][-1] == 4 else 1 | |
| SCORE_I = 1 - BOX_I | |
| # ---- NMS (class-aware, applied by caller) ----------------------------------- | |
| def nms(boxes, scores, iou_thr=0.45): | |
| x1, y1, x2, y2 = boxes.T | |
| areas = (x2 - x1) * (y2 - y1) | |
| order = scores.argsort()[::-1] | |
| keep = [] | |
| while order.size: | |
| 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, xx2 - xx1) | |
| h = np.maximum(0, yy2 - yy1) | |
| inter = w * h | |
| iou = inter / (areas[i] + areas[order[1:]] - inter + 1e-9) | |
| order = order[1:][iou <= iou_thr] | |
| return keep | |
| def detect(image, conf_thr=0.5, iou_thr=0.45): | |
| if image is None: | |
| return None, "Upload an image." | |
| img = image.convert("RGB") | |
| # Preprocess: plain resize to model input, /255, then ImageNet mean/std. | |
| resized = img.resize((IN_W, IN_H), Image.BILINEAR) | |
| x = np.asarray(resized, np.float32) / 255.0 | |
| x = ((x - IMAGENET_MEAN) / IMAGENET_STD).astype(np.float32)[None] | |
| interp.set_tensor(INP["index"], x) | |
| interp.invoke() | |
| boxes = interp.get_tensor(OUTS[BOX_I]["index"])[0] # (N, 4) x1,y1,x2,y2 in 0..IN_W | |
| scores = interp.get_tensor(OUTS[SCORE_I]["index"])[0].T # (N, num_classes) | |
| cid = scores.argmax(1) | |
| conf = scores.max(1) | |
| m = conf >= conf_thr | |
| boxes, conf, cid = boxes[m], conf[m], cid[m] | |
| # Scale boxes from model-input space (IN_W x IN_H) back to the original image. | |
| sx = img.width / IN_W | |
| sy = img.height / IN_H | |
| draw = ImageDraw.Draw(img) | |
| colors = [(220, 70, 60), (240, 160, 40), (60, 130, 200)] | |
| lines = [] | |
| if len(boxes): | |
| xyxy = boxes.astype(np.float32).copy() | |
| xyxy[:, [0, 2]] *= sx | |
| xyxy[:, [1, 3]] *= sy | |
| xyxy[:, [0, 2]] = xyxy[:, [0, 2]].clip(0, img.width) | |
| xyxy[:, [1, 3]] = xyxy[:, [1, 3]].clip(0, img.height) | |
| for c in np.unique(cid): | |
| idx = np.where(cid == c)[0] | |
| for k in nms(xyxy[idx], conf[idx], iou_thr): | |
| b = xyxy[idx][k] | |
| col = colors[int(c) % 3] | |
| draw.rectangle(list(b), outline=col, width=3) | |
| draw.text((b[0] + 3, max(0, b[1] - 12)), | |
| f"{CLASS_NAMES[int(c)]} {conf[idx][k]:.2f}", fill=col) | |
| lines.append(f"{CLASS_NAMES[int(c)]}: {conf[idx][k]:.2f}") | |
| return img, ("\n".join(lines) if lines else "No predators detected.") | |
| demo = gr.Interface( | |
| fn=detect, | |
| inputs=[gr.Image(type="pil", label="Field image"), | |
| gr.Slider(0.05, 0.9, 0.5, label="Confidence threshold"), | |
| gr.Slider(0.1, 0.9, 0.45, label="NMS IoU")], | |
| outputs=[gr.Image(type="pil", label="Detections"), | |
| gr.Textbox(label="Results")], | |
| title="SpotPredator — PicoDet", | |
| description="Off-grid farm predator detector (coyote / fox / raptor). " | |
| "Runs the PicoDet TFLite model directly (Apache-2.0). Drag in a field image.", | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |