yolo8 / inference.py
mayanktak15's picture
Upload folder using huggingface_hub
4c1b64e verified
Raw
History Blame Contribute Delete
7.17 kB
"""Standalone YOLO image inference for Hugging Face model repositories.
The module works with uploaded local weights such as models/yolo11n.pt and
falls back to Ultralytics model names such as yolo11n.pt when no local model is
present.
"""
from __future__ import annotations
import argparse
import json
import os
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable
from PIL import Image, ImageDraw, ImageFont
from ultralytics import YOLO
DEFAULT_MODEL_CANDIDATES = (
"models/best.pt",
"models/yolo11n.pt",
"models/yolov8n.pt",
"yolo11n.pt",
)
@dataclass(slots=True)
class Detection:
"""Serializable object detection result."""
class_id: int
class_name: str
confidence: float
bbox: list[float]
def resolve_model_path(model_path: str | os.PathLike[str] | None = None) -> str:
"""Return the model path/name to load.
Priority:
1. Explicit function/CLI argument.
2. MODEL_PATH environment variable.
3. Existing files under models/.
4. Ultralytics default model name, which downloads on first use.
"""
explicit = str(model_path or "").strip()
if explicit:
return explicit
env_model = os.getenv("MODEL_PATH", "").strip()
if env_model:
return env_model
for candidate in DEFAULT_MODEL_CANDIDATES:
if Path(candidate).exists():
return candidate
return DEFAULT_MODEL_CANDIDATES[-1]
def load_model(model_path: str | os.PathLike[str] | None = None) -> YOLO:
"""Load a YOLO model from a local file or Ultralytics model name."""
return YOLO(resolve_model_path(model_path))
def _resolve_classes(model: YOLO, classes: str | Iterable[str | int] | None) -> list[int] | None:
if classes is None:
return None
if isinstance(classes, str):
items = [item.strip() for item in classes.split(",") if item.strip()]
else:
items = [str(item).strip() for item in classes if str(item).strip()]
if not items:
return None
names = getattr(model, "names", {}) or {}
name_to_id = {str(name).lower(): int(idx) for idx, name in names.items()}
resolved: list[int] = []
for item in items:
if item.isdigit():
resolved.append(int(item))
else:
class_id = name_to_id.get(item.lower())
if class_id is not None:
resolved.append(class_id)
return resolved or None
def predict(
image: str | os.PathLike[str] | Image.Image,
model: YOLO | None = None,
model_path: str | os.PathLike[str] | None = None,
conf: float = 0.35,
iou: float = 0.5,
imgsz: int = 1280,
classes: str | Iterable[str | int] | None = "person",
device: str | None = None,
) -> tuple[list[Detection], Image.Image]:
"""Run YOLO detection on one image and return detections plus annotation."""
loaded_model = model or load_model(model_path)
pil_image = image if isinstance(image, Image.Image) else Image.open(image)
pil_image = pil_image.convert("RGB")
class_ids = _resolve_classes(loaded_model, classes)
results = loaded_model.predict(
source=pil_image,
conf=conf,
iou=iou,
imgsz=imgsz,
classes=class_ids,
device=device,
verbose=False,
)
detections: list[Detection] = []
names = getattr(loaded_model, "names", {}) or {}
for result in results:
if result.boxes is None:
continue
boxes = result.boxes.xyxy.cpu().numpy()
confidences = result.boxes.conf.cpu().numpy()
class_indexes = result.boxes.cls.cpu().numpy().astype(int)
for bbox, score, class_id in zip(boxes, confidences, class_indexes, strict=False):
detections.append(
Detection(
class_id=int(class_id),
class_name=str(names.get(int(class_id), class_id)),
confidence=round(float(score), 4),
bbox=[round(float(value), 2) for value in bbox],
)
)
annotated = draw_detections(pil_image, detections)
return detections, annotated
def draw_detections(image: Image.Image, detections: list[Detection]) -> Image.Image:
"""Draw boxes and confidence labels on an RGB image."""
annotated = image.copy()
draw = ImageDraw.Draw(annotated)
font = ImageFont.load_default()
for detection in detections:
x1, y1, x2, y2 = detection.bbox
color = _color_for_class(detection.class_id)
label = f"{detection.class_name} {detection.confidence:.2f}"
draw.rectangle((x1, y1, x2, y2), outline=color, width=3)
text_bbox = draw.textbbox((x1, y1), label, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
label_y = max(0, y1 - text_height - 8)
draw.rectangle((x1, label_y, x1 + text_width + 8, label_y + text_height + 6), fill=color)
draw.text((x1 + 4, label_y + 3), label, fill=(255, 255, 255), font=font)
return annotated
def _color_for_class(class_id: int) -> tuple[int, int, int]:
palette = (
(35, 100, 170),
(61, 163, 93),
(222, 122, 40),
(153, 80, 160),
(199, 62, 82),
)
return palette[class_id % len(palette)]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run YOLO detection on an image.")
parser.add_argument("--image", required=True, type=Path, help="Input image path")
parser.add_argument("--output", default=Path("examples/annotated_output.jpg"), type=Path)
parser.add_argument("--json", default=None, type=Path, help="Optional JSON detections path")
parser.add_argument("--model", default=None, help="Path/name of model weights")
parser.add_argument("--conf", default=0.35, type=float, help="Confidence threshold")
parser.add_argument("--iou", default=0.5, type=float, help="NMS IoU threshold")
parser.add_argument("--imgsz", default=1280, type=int, help="Inference image size")
parser.add_argument(
"--classes",
default="person",
help="Comma-separated class names or IDs. Use empty string for all classes.",
)
parser.add_argument("--device", default=None, help="Device, for example cpu, 0, or cuda:0")
return parser.parse_args()
def main() -> None:
args = parse_args()
class_filter = args.classes if args.classes.strip() else None
detections, annotated = predict(
image=args.image,
model_path=args.model,
conf=args.conf,
iou=args.iou,
imgsz=args.imgsz,
classes=class_filter,
device=args.device,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
annotated.save(args.output)
payload = [asdict(detection) for detection in detections]
if args.json:
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
else:
print(json.dumps(payload, indent=2))
print(f"Annotated image saved to {args.output}")
if __name__ == "__main__":
main()