yolonnx 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
yolonnx-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Kasper Fromm Pedersen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
yolonnx-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.1
2
+ Name: yolonnx
3
+ Version: 0.1.1
4
+ Summary: You Only Look ONNX
5
+ Keywords: ONNX,YOLOv8,onnxruntime,vision
6
+ Home-page: https://www.cs.aau.dk/
7
+ Author-Email: Kasper Fromm Pedersen <kasperf@cs.aau.dk>
8
+ License: MIT
9
+ Project-URL: Homepage, https://www.cs.aau.dk/
10
+ Project-URL: Repository, https://github.com/fromm1990/yolonnx
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: numpy==1.26.*
13
+ Requires-Dist: aau-label==0.2.*
14
+ Description-Content-Type: text/markdown
15
+
16
+ # You Only Look ONNX
17
+ This repository is a light weight library to ease the use of ONNX models exported by the Ultralytics YOLOv8 framework.
18
+
19
+ ## Example Detector Usage
20
+ ```python
21
+ from pathlib import Path
22
+
23
+ from onnxruntime import InferenceSession
24
+ from PIL import Image
25
+
26
+ from yolonnx.services import Detector
27
+ from yolonnx.to_tensor_strategies import PillowToTensorContainStrategy
28
+
29
+ model = Path("path/to/file.onnx")
30
+ session = InferenceSession(
31
+ model.as_posix(),
32
+ providers=[
33
+ "CUDAExecutionProvider",
34
+ "CPUExecutionProvider",
35
+ ],
36
+ )
37
+
38
+ predictor = Detector(session, PillowToTensorContainStrategy())
39
+ img = Image.open("path/to/image.jpg")
40
+ print(predictor.run(img))
41
+ ```
42
+
43
+ ## Example Classifier Usage
44
+ ```python
45
+ from pathlib import Path
46
+
47
+ from onnxruntime import InferenceSession
48
+ from PIL import Image
49
+
50
+ from yolonnx.services import Classifier
51
+ from yolonnx.to_tensor_strategies import PillowToTensorContainStrategy
52
+
53
+ model = Path("path/to/file.onnx")
54
+ session = InferenceSession(
55
+ model.as_posix(),
56
+ providers=[
57
+ "CUDAExecutionProvider",
58
+ "CPUExecutionProvider",
59
+ ],
60
+ )
61
+
62
+ predictor = Classifier(session, PillowToTensorContainStrategy())
63
+ img = Image.open("path/to/image.jpg")
64
+ print(predictor.run(img))
65
+
66
+ ```
@@ -0,0 +1,51 @@
1
+ # You Only Look ONNX
2
+ This repository is a light weight library to ease the use of ONNX models exported by the Ultralytics YOLOv8 framework.
3
+
4
+ ## Example Detector Usage
5
+ ```python
6
+ from pathlib import Path
7
+
8
+ from onnxruntime import InferenceSession
9
+ from PIL import Image
10
+
11
+ from yolonnx.services import Detector
12
+ from yolonnx.to_tensor_strategies import PillowToTensorContainStrategy
13
+
14
+ model = Path("path/to/file.onnx")
15
+ session = InferenceSession(
16
+ model.as_posix(),
17
+ providers=[
18
+ "CUDAExecutionProvider",
19
+ "CPUExecutionProvider",
20
+ ],
21
+ )
22
+
23
+ predictor = Detector(session, PillowToTensorContainStrategy())
24
+ img = Image.open("path/to/image.jpg")
25
+ print(predictor.run(img))
26
+ ```
27
+
28
+ ## Example Classifier Usage
29
+ ```python
30
+ from pathlib import Path
31
+
32
+ from onnxruntime import InferenceSession
33
+ from PIL import Image
34
+
35
+ from yolonnx.services import Classifier
36
+ from yolonnx.to_tensor_strategies import PillowToTensorContainStrategy
37
+
38
+ model = Path("path/to/file.onnx")
39
+ session = InferenceSession(
40
+ model.as_posix(),
41
+ providers=[
42
+ "CUDAExecutionProvider",
43
+ "CPUExecutionProvider",
44
+ ],
45
+ )
46
+
47
+ predictor = Classifier(session, PillowToTensorContainStrategy())
48
+ img = Image.open("path/to/image.jpg")
49
+ print(predictor.run(img))
50
+
51
+ ```
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "yolonnx"
3
+ version = "0.1.1"
4
+ description = "You Only Look ONNX"
5
+ authors = [
6
+ { name = "Kasper Fromm Pedersen", email = "kasperf@cs.aau.dk" },
7
+ ]
8
+ dependencies = [
9
+ "numpy==1.26.*",
10
+ "aau-label==0.2.*",
11
+ ]
12
+ requires-python = ">=3.10"
13
+ readme = "README.md"
14
+ keywords = [
15
+ "ONNX",
16
+ "YOLOv8",
17
+ "onnxruntime",
18
+ "vision",
19
+ ]
20
+
21
+ [project.license]
22
+ text = "MIT"
23
+
24
+ [project.urls]
25
+ homepage = "https://www.cs.aau.dk/"
26
+ repository = "https://github.com/fromm1990/yolonnx"
27
+
28
+ [build-system]
29
+ requires = [
30
+ "pdm-backend",
31
+ ]
32
+ build-backend = "pdm.backend"
33
+
34
+ [tool.pdm]
35
+ distribution = true
36
+
37
+ [tool.pdm.dev-dependencies]
38
+ test = [
39
+ "onnxruntime==1.17.*",
40
+ ]
File without changes
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from aau_label.model import AAULabel
6
+ from numpy.typing import NDArray
7
+
8
+
9
+ @dataclass
10
+ class Size:
11
+ width: float
12
+ height: float
13
+
14
+
15
+ @dataclass
16
+ class ImgTensor:
17
+ scale: Size
18
+ data: NDArray
19
+
20
+
21
+ @dataclass
22
+ class ClassifierResult:
23
+ name: str
24
+ score: float
25
+
26
+
27
+ @dataclass
28
+ class DetectorResult(ClassifierResult, AAULabel): ...
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Protocol, TypeVar, runtime_checkable
4
+
5
+ from numpy import int64
6
+ from numpy.typing import NDArray
7
+
8
+ from .model import ImgTensor
9
+
10
+ T = TypeVar("T", contravariant=True)
11
+
12
+
13
+ class PNodeArg(Protocol):
14
+ @property
15
+ def name(self) -> str: ...
16
+
17
+ @property
18
+ def shape(self) -> Any: ...
19
+
20
+ @property
21
+ def type(self) -> str: ...
22
+
23
+
24
+ @runtime_checkable
25
+ class SparseTensorProtocol(Protocol):
26
+ def dense_shape(self) -> NDArray[int64]: ...
27
+ def values(self) -> NDArray: ...
28
+ def device_name(self) -> str: ...
29
+
30
+
31
+ class ModelMetadataProtocol(Protocol):
32
+ @property
33
+ def custom_metadata_map(self) -> dict: ...
34
+
35
+
36
+ class InferenceSessionProtocol(Protocol):
37
+ def run(
38
+ self, output_names, input_feed: dict[str, Any], run_options=None
39
+ ) -> list[NDArray] | list[list] | list[dict] | list[SparseTensorProtocol]: ...
40
+
41
+ def run_async(
42
+ self,
43
+ output_names,
44
+ input_feed: dict[str, Any],
45
+ callback: Callable,
46
+ user_data: Any,
47
+ run_options=None,
48
+ ): ...
49
+
50
+ def get_inputs(self) -> list[PNodeArg]: ...
51
+
52
+ def get_modelmeta(self) -> ModelMetadataProtocol: ...
53
+
54
+
55
+ class ToTensorStrategyProtocol(Protocol[T]):
56
+ def __call__(self, img: T, tensor_width: int, tensor_height: int) -> ImgTensor: ...
@@ -0,0 +1,4 @@
1
+ from .classifier import Classifier
2
+ from .detector import Detector
3
+
4
+ __all__ = ["Classifier", "Detector"]
@@ -0,0 +1,71 @@
1
+ import ast
2
+ import logging
3
+ from typing import Generic, Sequence, TypeVar
4
+
5
+ import numpy
6
+ from numpy import float32
7
+ from numpy.typing import NDArray
8
+
9
+ from ..model import ClassifierResult
10
+ from ..protocols import (
11
+ InferenceSessionProtocol,
12
+ SparseTensorProtocol,
13
+ ToTensorStrategyProtocol,
14
+ )
15
+
16
+ T = TypeVar("T")
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Classifier(Generic[T]):
21
+ def __init__(
22
+ self,
23
+ session: InferenceSessionProtocol,
24
+ to_tensor_strategy: ToTensorStrategyProtocol[T],
25
+ threshold: float = 0.1,
26
+ ) -> None:
27
+ self.__session = session
28
+ self.__to_tensor_strategy = to_tensor_strategy
29
+ self.__threshold = threshold
30
+ meta = self.__session.get_modelmeta()
31
+ self.__names: dict[int, str] = ast.literal_eval(
32
+ meta.custom_metadata_map["names"]
33
+ )
34
+
35
+ @property
36
+ def threshold(self) -> float:
37
+ return self.__threshold
38
+
39
+ @property
40
+ def shape(self) -> tuple[int, int]:
41
+ return self.__session.get_inputs()[0].shape[2:]
42
+
43
+ @property
44
+ def names(self) -> dict[int, str]:
45
+ return self.__names
46
+
47
+ def warmup(self) -> None:
48
+ tensor = numpy.zeros((1, 3, self.shape[0], self.shape[1]), float32)
49
+ self.__session.run(None, {"images": tensor})
50
+
51
+ def __result_handler(self, results: NDArray) -> Sequence[ClassifierResult]:
52
+ labels_idx = numpy.argwhere(results > self.threshold)
53
+ scores: NDArray[float32] = results[labels_idx]
54
+
55
+ rv: list[ClassifierResult] = []
56
+ for i in range(len(labels_idx)):
57
+ id = labels_idx[i][0]
58
+ label_name = self.names[id]
59
+ rv.append(ClassifierResult(name=label_name, score=scores[i]))
60
+
61
+ rv.sort(key=lambda x: x.score, reverse=True)
62
+ return rv
63
+
64
+ def run(self, img: T) -> Sequence[ClassifierResult]:
65
+ tensor = self.__to_tensor_strategy(img, *self.shape)
66
+ results = self.__session.run(None, {"images": tensor.data})[0]
67
+
68
+ if isinstance(results, SparseTensorProtocol):
69
+ results = results.values()
70
+
71
+ return self.__result_handler(results[0])
@@ -0,0 +1,94 @@
1
+ import ast
2
+ import logging
3
+ from typing import Generic, Sequence, TypeVar, cast
4
+
5
+ import numpy
6
+ from numpy.typing import NDArray
7
+
8
+ from .. import utils
9
+ from ..model import DetectorResult, ImgTensor
10
+ from ..protocols import InferenceSessionProtocol, ToTensorStrategyProtocol
11
+
12
+ T = TypeVar("T")
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Detector(Generic[T]):
17
+ def __init__(
18
+ self,
19
+ session: InferenceSessionProtocol,
20
+ to_tensor_strategy: ToTensorStrategyProtocol[T],
21
+ conf_threshold: float = 0.25,
22
+ iou_threshold: float = 0.7,
23
+ ) -> None:
24
+ self.__session = session
25
+ self.__to_tensor_strategy = to_tensor_strategy
26
+ self.__conf_threshold = conf_threshold
27
+ self.__iou_threshold = iou_threshold
28
+
29
+ meta = self.__session.get_modelmeta()
30
+ self.__names: dict[int, str] = ast.literal_eval(
31
+ meta.custom_metadata_map["names"]
32
+ )
33
+
34
+ @property
35
+ def conf_threshold(self) -> float:
36
+ return self.__conf_threshold
37
+
38
+ @property
39
+ def iou_threshold(self) -> float:
40
+ return self.__iou_threshold
41
+
42
+ @property
43
+ def shape(self) -> tuple[int, int]:
44
+ return self.__session.get_inputs()[0].shape[2:]
45
+
46
+ @property
47
+ def names(self) -> dict[int, str]:
48
+ return self.__names
49
+
50
+ def __result_handler(
51
+ self, results: NDArray, tensor: ImgTensor
52
+ ) -> Sequence[DetectorResult]:
53
+ predictions = numpy.squeeze(results[0]).T
54
+
55
+ scores = numpy.max(predictions[:, 4:], axis=1)
56
+ keep = scores > self.__conf_threshold
57
+ predictions = predictions[keep, :]
58
+ scores = scores[keep]
59
+ class_ids = numpy.argmax(predictions[:, 4:], axis=1)
60
+
61
+ boxes = predictions[:, :4]
62
+ # Make x0, y0 left upper corner instead of box center
63
+ boxes[:, 0:2] -= boxes[:, 2:4] / 2
64
+ boxes /= numpy.array(
65
+ [
66
+ tensor.scale.width,
67
+ tensor.scale.height,
68
+ tensor.scale.width,
69
+ tensor.scale.height,
70
+ ],
71
+ dtype=numpy.float32,
72
+ )
73
+ boxes = boxes.astype(numpy.int32)
74
+
75
+ keep = utils.nms(boxes, scores, self.__iou_threshold)
76
+ rv = []
77
+ for bbox, label, score in zip(boxes[keep], class_ids[keep], scores[keep]):
78
+ rv.append(
79
+ DetectorResult(
80
+ x=bbox[0].item(),
81
+ y=bbox[1].item(),
82
+ width=bbox[2].item(),
83
+ height=bbox[3].item(),
84
+ name=self.__names[label],
85
+ score=score.item(),
86
+ )
87
+ )
88
+
89
+ return rv
90
+
91
+ def run(self, img: T) -> Sequence[DetectorResult]:
92
+ tensor = self.__to_tensor_strategy(img, *self.shape)
93
+ results = cast(list[NDArray], self.__session.run(None, {"images": tensor.data}))
94
+ return self.__result_handler(results[0], tensor)
@@ -0,0 +1,44 @@
1
+ import numpy
2
+ from PIL import ImageOps
3
+ from PIL.Image import Image as ImageType
4
+ from PIL.Image import Resampling
5
+
6
+ from .model import ImgTensor, Size
7
+ from .protocols import ToTensorStrategyProtocol
8
+
9
+
10
+ class PillowToTensorContainStrategy(ToTensorStrategyProtocol[ImageType]):
11
+ """
12
+ Takes a Pillow image and scales it such it fits inside the tensor. The image aspect ratio is preserved.
13
+
14
+ The image is then padded with the color (114, 114, 114) to make the image fit inside the tensor.
15
+ """
16
+
17
+ def __call__(
18
+ self, img: ImageType, tensor_width: int, tensor_height: int
19
+ ) -> ImgTensor:
20
+ tensor_dims = (tensor_width, tensor_height)
21
+ original_width, original_height = img.size
22
+
23
+ img = ImageOps.contain(img, tensor_dims, Resampling.BILINEAR)
24
+ new_width, new_height = img.size
25
+
26
+ img = ImageOps.pad(
27
+ img,
28
+ tensor_dims,
29
+ Resampling.BILINEAR,
30
+ (114, 114, 114),
31
+ (0, 0),
32
+ )
33
+ data = numpy.array(img)
34
+
35
+ data = data / 255.0
36
+ data = data.transpose(2, 0, 1)
37
+ tensor = data[numpy.newaxis, :, :, :].astype(numpy.float32)
38
+
39
+ scale = Size(
40
+ width=new_width / original_width,
41
+ height=new_height / original_height,
42
+ )
43
+
44
+ return ImgTensor(scale, tensor)
@@ -0,0 +1,48 @@
1
+ import numpy
2
+ from numpy import float32, float64, int32, int64
3
+ from numpy.typing import NDArray
4
+
5
+
6
+ def compute_iou(box: NDArray[int32], boxes: NDArray[int32]) -> NDArray[float64]:
7
+ # Compute xmin, ymin, xmax, ymax for both boxes
8
+ xmin = numpy.minimum(box[0], boxes[:, 0])
9
+ ymin = numpy.minimum(box[1], boxes[:, 1])
10
+ xmax = numpy.maximum(box[0] + box[2], boxes[:, 0] + boxes[:, 2])
11
+ ymax = numpy.maximum(box[1] + box[3], boxes[:, 1] + boxes[:, 3])
12
+
13
+ # Compute intersection area
14
+ intersection_area = numpy.maximum(0, xmax - xmin) * numpy.maximum(0, ymax - ymin)
15
+
16
+ # Compute union area
17
+ box_area = box[2] * box[3]
18
+ boxes_area = boxes[:, 2] * boxes[:, 3]
19
+ union_area = box_area + boxes_area - intersection_area
20
+
21
+ # Compute IoU
22
+ iou = intersection_area / union_area
23
+
24
+ return iou
25
+
26
+
27
+ def nms(
28
+ boxes: NDArray[int32], scores: NDArray[float32], iou_threshold: float
29
+ ) -> list[int64]:
30
+ # Sort by score
31
+ sorted_indices = numpy.argsort(scores)[::-1]
32
+
33
+ keep_boxes = []
34
+ while sorted_indices.size > 0:
35
+ # Pick the last box
36
+ box_id = sorted_indices[0]
37
+ keep_boxes.append(box_id)
38
+
39
+ # Compute IoU of the picked box with the rest
40
+ ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :])
41
+
42
+ # Remove boxes with IoU over the threshold
43
+ keep_indices = numpy.where(ious < iou_threshold)[0]
44
+
45
+ # print(keep_indices.shape, sorted_indices.shape)
46
+ sorted_indices = sorted_indices[keep_indices + 1]
47
+
48
+ return keep_boxes
File without changes