|
|
|
|
|
from typing import Dict, Any, List |
|
|
import base64, io, os, json |
|
|
from PIL import Image |
|
|
import numpy as np |
|
|
|
|
|
|
|
|
from ultralytics import YOLO |
|
|
|
|
|
|
|
|
def load_image_from_payload(payload: Dict[str, Any]) -> Image.Image: |
|
|
""" |
|
|
Supports: |
|
|
- base64 image: payload["inputs"] = "data:image/...;base64,...." or raw base64 string |
|
|
- image URL: payload["image_url"] = "https://..." |
|
|
""" |
|
|
if "inputs" in payload and isinstance(payload["inputs"], str): |
|
|
s = payload["inputs"] |
|
|
|
|
|
if s.startswith("data:image"): |
|
|
s = s.split(",", 1)[1] |
|
|
img_bytes = base64.b64decode(s) |
|
|
return Image.open(io.BytesIO(img_bytes)).convert("RGB") |
|
|
|
|
|
if "image_url" in payload: |
|
|
|
|
|
import requests |
|
|
r = requests.get(payload["image_url"], timeout=10) |
|
|
r.raise_for_status() |
|
|
return Image.open(io.BytesIO(r.content)).convert("RGB") |
|
|
|
|
|
raise ValueError("No image provided. Use 'inputs' (base64) or 'image_url'.") |
|
|
|
|
|
def yolo_to_coco(result) -> List[Dict[str, Any]]: |
|
|
""" |
|
|
Convert Ultralytics result to COCO-ish detections: |
|
|
[{"bbox":[x,y,w,h], "score":float, "category_id":int, "category_name":str}] |
|
|
""" |
|
|
detections = [] |
|
|
names = result.names |
|
|
for b in result.boxes: |
|
|
xywh = b.xywh.cpu().numpy().tolist()[0] |
|
|
|
|
|
x, y, w, h = xywh |
|
|
x0 = x - w/2 |
|
|
y0 = y - h/2 |
|
|
conf = float(b.conf.cpu().item()) |
|
|
cls_id = int(b.cls.cpu().item()) |
|
|
detections.append({ |
|
|
"bbox": [float(x0), float(y0), float(w), float(h)], |
|
|
"score": conf, |
|
|
"category_id": cls_id, |
|
|
"category_name": names.get(cls_id, str(cls_id)), |
|
|
}) |
|
|
return detections |
|
|
|
|
|
class EndpointHandler: |
|
|
def __init__(self, path: str = ""): |
|
|
""" |
|
|
`path` points to the repo files checked out on the endpoint. |
|
|
If yolov8n.pt is present in the repo, we load it directly. |
|
|
Otherwise we fallback to downloading 'yolov8n.pt'. |
|
|
""" |
|
|
weights_path = os.path.join(path, "yolov8n.pt") |
|
|
if not os.path.exists(weights_path): |
|
|
|
|
|
weights_path = "yolov8n.pt" |
|
|
self.model = YOLO(weights_path) |
|
|
|
|
|
|
|
|
|
|
|
dummy = Image.new("RGB", (320, 320), (128, 128, 128)) |
|
|
_ = self.model.predict(dummy, imgsz=320, conf=0.25, verbose=False) |
|
|
|
|
|
def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: |
|
|
""" |
|
|
Expected payload: |
|
|
{ |
|
|
"inputs": "<base64-image>" # OR "image_url": "https://..." |
|
|
"imgsz": (optional int, default 640) |
|
|
"conf": (optional float, default 0.25) |
|
|
"iou": (optional float, default 0.45) |
|
|
"max_det":(optional int, default 300) |
|
|
} |
|
|
Returns: |
|
|
{ |
|
|
"detections": [ {bbox, score, category_id, category_name}, ... ], |
|
|
"image_shape": [h,w], |
|
|
"model": "yolov8n" |
|
|
} |
|
|
""" |
|
|
imgsz = int(data.get("imgsz", 640)) |
|
|
conf = float(data.get("conf", 0.25)) |
|
|
iou = float(data.get("iou", 0.45)) |
|
|
max_det = int(data.get("max_det", 300)) |
|
|
|
|
|
img = load_image_from_payload(data) |
|
|
w, h = img.size |
|
|
|
|
|
results = self.model.predict( |
|
|
img, imgsz=imgsz, conf=conf, iou=iou, max_det=max_det, verbose=False |
|
|
) |
|
|
detections = yolo_to_coco(results[0]) |
|
|
|
|
|
return { |
|
|
"detections": detections, |
|
|
"image_shape": [h, w], |
|
|
"model": "yolov8n" |
|
|
} |