Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sahi/auto_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
MODEL_TYPE_TO_MODEL_CLASS_NAME = {
"ultralytics": "UltralyticsDetectionModel",
"rtdetr": "RTDetrDetectionModel",
"libreyolo": "LibreYoloDetectionModel",
"mmdet": "MmdetDetectionModel",
"yolov5": "Yolov5DetectionModel",
"detectron2": "Detectron2DetectionModel",
Expand Down
87 changes: 87 additions & 0 deletions sahi/models/libreyolo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""LibreYOLO detection model wrapper for SAHI.

Provides integration with LibreYOLO (MIT-licensed) object detection models.
Supports detection, segmentation, and oriented bounding box (OBB) tasks.

LibreYOLO GitHub: https://github.com/LibreYOLO/libreyolo
"""

from __future__ import annotations

import numpy as np

from sahi.models.ultralytics import UltralyticsDetectionModel


class LibreYoloDetectionModel(UltralyticsDetectionModel):
"""LibreYOLO object detection model.

Wraps LibreYOLO models for sliced inference via SAHI.
LibreYOLO provides an MIT-licensed alternative to Ultralytics
with a compatible API (Results, Boxes, Masks, OBB).
"""

def check_dependencies(self, packages: list[str] | None = None) -> None:
"""Check for libreyolo instead of ultralytics."""
super().check_dependencies(packages=["libreyolo"])

def load_model(self) -> None:
"""Initialize the LibreYOLO model and assign it to self.model."""
from libreyolo import LibreYOLO

try:
model_source = self.model_path or "LibreYOLO9t.pt"
model = LibreYOLO(model_source, device=self.device)
self.set_model(model)
except Exception as e:
raise TypeError("model_path is not a valid LibreYOLO model path: ", e)

def perform_batch_inference(self, images: list[np.ndarray]) -> None:
"""Perform batch inference, without the 'cfg' kwarg that LibreYOLO rejects."""
if self.model is None:
raise ValueError("Model is not loaded, load it by calling .load_model()")

kwargs = {"verbose": False, "conf": self.confidence_threshold, "device": self.device}

if self.image_size is not None:
kwargs = {"imgsz": self.image_size, **kwargs}

images_bgr = [img[:, :, ::-1] for img in images]
prediction_result = self.model(images_bgr, **kwargs)

self._original_predictions = self._extract_predictions(prediction_result)
self._original_shapes = [img.shape for img in images]

def _extract_predictions(self, prediction_result: list) -> list:
"""Extract predictions using libreyolo.Masks instead of ultralytics.Masks."""
import torch

if self.has_mask:
from libreyolo import Masks

for result in prediction_result:
if not result.masks:
device = getattr(self.model, "device", "cpu")
result.masks = Masks(torch.tensor([], device=device), result.boxes.orig_shape)

return [(result.boxes.data, result.masks.data) for result in prediction_result]
elif self.is_obb:
device = getattr(self.model, "device", "cpu")
return [
(
torch.cat(
[
result.obb.xyxy,
result.obb.conf.unsqueeze(-1),
result.obb.cls.unsqueeze(-1),
],
dim=1,
)
if result.obb is not None
else torch.empty((0, 6), device=device),
result.obb.xyxyxyxy if result.obb is not None else torch.empty((0, 4, 2), device=device),
)
for result in prediction_result
]
else:
return [result.boxes.data for result in prediction_result]
28 changes: 28 additions & 0 deletions sahi/utils/libreyolo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""LibreYOLO model utilities and constants."""

from __future__ import annotations

import urllib.request
from os import path
from pathlib import Path


class LibreYoloTestConstants:
"""LibreYOLO test model configurations."""

LIBREYOLO9T_MODEL_URL = "https://huggingface.co/LibreYOLO/LibreYOLO9t/resolve/main/LibreYOLO9t.pt"
LIBREYOLO9T_MODEL_PATH = "tests/data/models/libreyolo/LibreYOLO9t.pt"


def download_libreyolo9t_model(destination_path: str | None = None) -> None:
"""Download the LibreYOLO9t model for testing."""
if destination_path is None:
destination_path = LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH

Path(destination_path).parent.mkdir(parents=True, exist_ok=True)

if not path.exists(destination_path):
urllib.request.urlretrieve(
LibreYoloTestConstants.LIBREYOLO9T_MODEL_URL,
destination_path,
)
137 changes: 137 additions & 0 deletions tests/test_libreyolo_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
"""Tests for LibreYOLO detection model integration."""

from __future__ import annotations

import importlib

import pytest

from sahi.prediction import ObjectPrediction
from sahi.utils.cv import read_image
from sahi.utils.libreyolo import LibreYoloTestConstants, download_libreyolo9t_model

pytestmark = pytest.mark.skipif(importlib.util.find_spec("libreyolo") is None, reason="libreyolo is not installed")

MODEL_DEVICE = "cpu"
CONFIDENCE_THRESHOLD = 0.3
IMAGE_SIZE = 640


class TestLibreYoloDetectionModel:
"""Test LibreYOLO detection model functionality."""

def test_load_model(self) -> None:
"""Test loading a LibreYOLO detection model."""
from sahi.models.libreyolo import LibreYoloDetectionModel

download_libreyolo9t_model()

libreyolo_detection_model = LibreYoloDetectionModel(
model_path=LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH,
confidence_threshold=CONFIDENCE_THRESHOLD,
device=MODEL_DEVICE,
category_remapping=None,
load_at_init=True,
)

assert libreyolo_detection_model.model is not None

def test_set_model(self) -> None:
"""Test setting a pre-loaded LibreYOLO model."""
from libreyolo import LibreYOLO

from sahi.models.libreyolo import LibreYoloDetectionModel

download_libreyolo9t_model()

libreyolo_model = LibreYOLO(LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH)

libreyolo_detection_model = LibreYoloDetectionModel(
model=libreyolo_model,
confidence_threshold=CONFIDENCE_THRESHOLD,
device=MODEL_DEVICE,
category_remapping=None,
load_at_init=True,
)

assert libreyolo_detection_model.model is not None

def test_perform_inference(self) -> None:
"""Test inference with LibreYOLO model."""
from sahi.models.libreyolo import LibreYoloDetectionModel

download_libreyolo9t_model()

libreyolo_detection_model = LibreYoloDetectionModel(
model_path=LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH,
confidence_threshold=CONFIDENCE_THRESHOLD,
device=MODEL_DEVICE,
category_remapping=None,
load_at_init=True,
image_size=IMAGE_SIZE,
)

# prepare image
image_path = "tests/data/small-vehicles1.jpeg"
image = read_image(image_path)

# perform inference
libreyolo_detection_model.perform_inference(image)
original_predictions = libreyolo_detection_model.original_predictions

boxes = original_predictions
assert boxes is not None

# verify confidence threshold is respected
for box in boxes[0]:
assert box[4].item() >= CONFIDENCE_THRESHOLD

# verify category names are loaded
assert len(libreyolo_detection_model.category_names) == 80

def test_convert_original_predictions(self) -> None:
"""Test converting LibreYOLO predictions to ObjectPrediction."""
from sahi.models.libreyolo import LibreYoloDetectionModel

download_libreyolo9t_model()

libreyolo_detection_model = LibreYoloDetectionModel(
model_path=LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH,
confidence_threshold=CONFIDENCE_THRESHOLD,
device=MODEL_DEVICE,
category_remapping=None,
load_at_init=True,
image_size=IMAGE_SIZE,
)

# prepare image
image_path = "tests/data/small-vehicles1.jpeg"
image = read_image(image_path)

# perform inference
libreyolo_detection_model.perform_inference(image)

# convert predictions to ObjectPrediction list
libreyolo_detection_model.convert_original_predictions()
object_prediction_list = libreyolo_detection_model.object_prediction_list

# verify predictions are ObjectPrediction instances
assert len(object_prediction_list) > 0
for object_prediction in object_prediction_list:
assert isinstance(object_prediction, ObjectPrediction)
assert object_prediction.score.value >= CONFIDENCE_THRESHOLD

def test_auto_model_type(self) -> None:
"""Test that LibreYOLO can be loaded via AutoDetectionModel."""
from sahi import AutoDetectionModel

download_libreyolo9t_model()

detection_model = AutoDetectionModel.from_pretrained(
model_type="libreyolo",
model_path=LibreYoloTestConstants.LIBREYOLO9T_MODEL_PATH,
confidence_threshold=CONFIDENCE_THRESHOLD,
device=MODEL_DEVICE,
)

assert detection_model.model is not None