Skip to content

Commit

Permalink
Add yolov8 to SAHI (#833)
Browse files Browse the repository at this point in the history
Co-authored-by: Your Name <[email protected]>
  • Loading branch information
NguyenTheAn and Your Name authored Feb 23, 2023
1 parent c845417 commit 906dd4d
Show file tree
Hide file tree
Showing 9 changed files with 993 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ jobs:
run: >
pip install pycocotools==2.0.6
- name: Install ultralytics
run: >
pip install ultralytics==8.0.43
- name: Unittest for SAHI+YOLOV5/MMDET/Detectron2 on all platforms
run: |
python -m unittest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci_torch1.10.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ jobs:
run: >
pip install pycocotools==2.0.6
- name: Install ultralytics
run: >
pip install ultralytics==8.0.43
- name: Unittest for SAHI+YOLOV5/MMDET/Detectron2 on all platforms
run: |
python -m unittest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/package_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ jobs:
run: >
pip install pycocotools==2.0.6
- name: Install ultralytics
run: >
pip install ultralytics==8.0.43
- name: Install latest SAHI package
run: >
pip install --upgrade --force-reinstall sahi
Expand Down
6 changes: 3 additions & 3 deletions demo/inference_for_yolov5.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.8.8 ('sahi')",
"display_name": "test",
"language": "python",
"name": "python3"
},
Expand All @@ -650,11 +650,11 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.8"
"version": "3.8.15"
},
"vscode": {
"interpreter": {
"hash": "f2680c47b11e1b3873482f0b7ab37c9292181f84f7b4413a77eb41d52d22c05d"
"hash": "244b47d5824a96a4079632e50977464d968e13d2c337f65c905f8da81a0b4f95"
}
}
},
Expand Down
634 changes: 634 additions & 0 deletions demo/inference_for_yolov8.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions sahi/auto_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sahi.utils.file import import_model_class

MODEL_TYPE_TO_MODEL_CLASS_NAME = {
"yolov8": "Yolov8DetectionModel",
"mmdet": "MmdetDetectionModel",
"yolov5": "Yolov5DetectionModel",
"detectron2": "Detectron2DetectionModel",
Expand Down
160 changes: 160 additions & 0 deletions sahi/models/yolov8.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# OBSS SAHI Tool
# Code written by AnNT, 2023.

import logging
from typing import Any, Dict, List, Optional

import numpy as np

logger = logging.getLogger(__name__)

from sahi.models.base import DetectionModel
from sahi.prediction import ObjectPrediction
from sahi.utils.compatibility import fix_full_shape_list, fix_shift_amount_list
from sahi.utils.import_utils import check_requirements


class Yolov8DetectionModel(DetectionModel):
def check_dependencies(self) -> None:
check_requirements(["ultralytics"])

def load_model(self):
"""
Detection model is initialized and set to self.model.
"""

from ultralytics import YOLO

try:
model = YOLO(self.model_path)
model.to(self.device)
self.set_model(model)
except Exception as e:
raise TypeError("model_path is not a valid yolov8 model path: ", e)

def set_model(self, model: Any):
"""
Sets the underlying YOLOv8 model.
Args:
model: Any
A YOLOv8 model
"""

# if model.__class__.__module__ not in ["yolov5.models.common", "models.common"]:
# raise Exception(f"Not a yolov5 model: {type(model)}")

# model.conf = self.confidence_threshold
self.model = model

# set category_mapping
if not self.category_mapping:
category_mapping = {str(ind): category_name for ind, category_name in enumerate(self.category_names)}
self.category_mapping = category_mapping

def perform_inference(self, image: np.ndarray):
"""
Prediction is performed using self.model and the prediction result is set to self._original_predictions.
Args:
image: np.ndarray
A numpy array that contains the image to be predicted. 3 channel image should be in RGB order.
"""

# Confirm model is loaded
if self.model is None:
raise ValueError("Model is not loaded, load it by calling .load_model()")
prediction_result = self.model(image, verbose=False)
prediction_result = [
result.boxes.boxes[result.boxes.boxes[:, 4] >= self.confidence_threshold] for result in prediction_result
]

self._original_predictions = prediction_result

@property
def category_names(self):
return self.model.names.values()

@property
def num_categories(self):
"""
Returns number of categories
"""
return len(self.model.names)

@property
def has_mask(self):
"""
Returns if model output contains segmentation mask
"""
return False # fix when yolov5 supports segmentation models

def _create_object_prediction_list_from_original_predictions(
self,
shift_amount_list: Optional[List[List[int]]] = [[0, 0]],
full_shape_list: Optional[List[List[int]]] = None,
):
"""
self._original_predictions is converted to a list of prediction.ObjectPrediction and set to
self._object_prediction_list_per_image.
Args:
shift_amount_list: list of list
To shift the box and mask predictions from sliced image to full sized image, should
be in the form of List[[shift_x, shift_y],[shift_x, shift_y],...]
full_shape_list: list of list
Size of the full image after shifting, should be in the form of
List[[height, width],[height, width],...]
"""
original_predictions = self._original_predictions

# compatilibty for sahi v0.8.15
shift_amount_list = fix_shift_amount_list(shift_amount_list)
full_shape_list = fix_full_shape_list(full_shape_list)

# handle all predictions
object_prediction_list_per_image = []
for image_ind, image_predictions_in_xyxy_format in enumerate(original_predictions):
shift_amount = shift_amount_list[image_ind]
full_shape = None if full_shape_list is None else full_shape_list[image_ind]
object_prediction_list = []

# process predictions
for prediction in image_predictions_in_xyxy_format.cpu().detach().numpy():
x1 = prediction[0]
y1 = prediction[1]
x2 = prediction[2]
y2 = prediction[3]
bbox = [x1, y1, x2, y2]
score = prediction[4]
category_id = int(prediction[5])
category_name = self.category_mapping[str(category_id)]

# fix negative box coords
bbox[0] = max(0, bbox[0])
bbox[1] = max(0, bbox[1])
bbox[2] = max(0, bbox[2])
bbox[3] = max(0, bbox[3])

# fix out of image box coords
if full_shape is not None:
bbox[0] = min(full_shape[1], bbox[0])
bbox[1] = min(full_shape[0], bbox[1])
bbox[2] = min(full_shape[1], bbox[2])
bbox[3] = min(full_shape[0], bbox[3])

# ignore invalid predictions
if not (bbox[0] < bbox[2]) or not (bbox[1] < bbox[3]):
logger.warning(f"ignoring invalid prediction with bbox: {bbox}")
continue

object_prediction = ObjectPrediction(
bbox=bbox,
category_id=category_id,
score=score,
bool_mask=None,
category_name=category_name,
shift_amount=shift_amount,
full_shape=full_shape,
)
object_prediction_list.append(object_prediction)
object_prediction_list_per_image.append(object_prediction_list)

self._object_prediction_list_per_image = object_prediction_list_per_image
43 changes: 43 additions & 0 deletions sahi/utils/yolov8.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import urllib.request
from os import path
from pathlib import Path
from typing import Optional


class Yolov8TestConstants:
YOLOV8N_MODEL_URL = "https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt"
YOLOV8N_MODEL_PATH = "tests/data/models/yolov8/yolov8n.pt"

YOLOV8S_MODEL_URL = "https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8s.pt"
YOLOV8S_MODEL_PATH = "tests/data/models/yolov8/yolov8s.pt"

YOLOV8M_MODEL_URL = "https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8m.pt"
YOLOV8M_MODEL_PATH = "tests/data/models/yolov8/yolov8m.pt"


def download_yolov8n_model(destination_path: Optional[str] = None):

if destination_path is None:
destination_path = Yolov8TestConstants.YOLOV8N_MODEL_PATH

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

if not path.exists(destination_path):
urllib.request.urlretrieve(
Yolov8TestConstants.YOLOV8N_MODEL_URL,
destination_path,
)


def download_yolov8s_model(destination_path: Optional[str] = None):

if destination_path is None:
destination_path = Yolov8TestConstants.YOLOV8S_MODEL_PATH

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

if not path.exists(destination_path):
urllib.request.urlretrieve(
Yolov8TestConstants.YOLOV8S_MODEL_URL,
destination_path,
)
Loading

0 comments on commit 906dd4d

Please sign in to comment.