OTVision 0.5.3__py3-none-any.whl
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.
- OTVision/__init__.py +30 -0
- OTVision/application/__init__.py +0 -0
- OTVision/application/configure_logger.py +23 -0
- OTVision/application/detect/__init__.py +0 -0
- OTVision/application/detect/get_detect_cli_args.py +9 -0
- OTVision/application/detect/update_detect_config_with_cli_args.py +95 -0
- OTVision/application/get_config.py +25 -0
- OTVision/config.py +754 -0
- OTVision/convert/__init__.py +0 -0
- OTVision/convert/convert.py +318 -0
- OTVision/dataformat.py +70 -0
- OTVision/detect/__init__.py +0 -0
- OTVision/detect/builder.py +48 -0
- OTVision/detect/cli.py +166 -0
- OTVision/detect/detect.py +296 -0
- OTVision/detect/otdet.py +103 -0
- OTVision/detect/plugin_av/__init__.py +0 -0
- OTVision/detect/plugin_av/rotate_frame.py +37 -0
- OTVision/detect/yolo.py +277 -0
- OTVision/domain/__init__.py +0 -0
- OTVision/domain/cli.py +42 -0
- OTVision/helpers/__init__.py +0 -0
- OTVision/helpers/date.py +26 -0
- OTVision/helpers/files.py +538 -0
- OTVision/helpers/formats.py +139 -0
- OTVision/helpers/log.py +131 -0
- OTVision/helpers/machine.py +71 -0
- OTVision/helpers/video.py +54 -0
- OTVision/track/__init__.py +0 -0
- OTVision/track/iou.py +282 -0
- OTVision/track/iou_util.py +140 -0
- OTVision/track/preprocess.py +451 -0
- OTVision/track/track.py +422 -0
- OTVision/transform/__init__.py +0 -0
- OTVision/transform/get_homography.py +156 -0
- OTVision/transform/reference_points_picker.py +462 -0
- OTVision/transform/transform.py +352 -0
- OTVision/version.py +13 -0
- OTVision/view/__init__.py +0 -0
- OTVision/view/helpers/OTC.ico +0 -0
- OTVision/view/view.py +90 -0
- OTVision/view/view_convert.py +128 -0
- OTVision/view/view_detect.py +146 -0
- OTVision/view/view_helpers.py +417 -0
- OTVision/view/view_track.py +131 -0
- OTVision/view/view_transform.py +140 -0
- otvision-0.5.3.dist-info/METADATA +47 -0
- otvision-0.5.3.dist-info/RECORD +50 -0
- otvision-0.5.3.dist-info/WHEEL +4 -0
- otvision-0.5.3.dist-info/licenses/LICENSE +674 -0
OTVision/detect/yolo.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OTVision module to detect objects using yolov5
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Copyright (C) 2022 OpenTrafficCam Contributors
|
|
6
|
+
# <https://github.com/OpenTrafficCam
|
|
7
|
+
# <team@opentrafficcam.org>
|
|
8
|
+
#
|
|
9
|
+
# This program is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# This program is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from time import perf_counter
|
|
26
|
+
from typing import Callable, Generator
|
|
27
|
+
|
|
28
|
+
import av
|
|
29
|
+
import torch
|
|
30
|
+
from tqdm import tqdm
|
|
31
|
+
from ultralytics import YOLO as YOLOv8
|
|
32
|
+
from ultralytics.engine.results import Boxes
|
|
33
|
+
|
|
34
|
+
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
35
|
+
from OTVision.helpers import video
|
|
36
|
+
from OTVision.helpers.log import LOGGER_NAME
|
|
37
|
+
from OTVision.track.preprocess import Detection
|
|
38
|
+
|
|
39
|
+
DISPLAYMATRIX = "DISPLAYMATRIX"
|
|
40
|
+
|
|
41
|
+
log = logging.getLogger(LOGGER_NAME)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VideoFiletypeNotSupportedError(Exception):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class VideoFoundError(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class YOLOv5ModelNotFoundError(Exception):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ObjectDetection(ABC):
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def detect(
|
|
59
|
+
self,
|
|
60
|
+
video: Path,
|
|
61
|
+
detect_start: int | None = None,
|
|
62
|
+
detect_end: int | None = None,
|
|
63
|
+
) -> list[list[Detection]]:
|
|
64
|
+
"""Runs object detection on a video.
|
|
65
|
+
Args:
|
|
66
|
+
video (Path): the path to the video.
|
|
67
|
+
detect_start (int | None, optional): Start of the detection range
|
|
68
|
+
expressed in frames.
|
|
69
|
+
detect_end (int | None, optional): End of the detection range
|
|
70
|
+
expressed in frames. Defaults to None.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
list[list[Detection]]: nested list of detections. First level is frames,
|
|
74
|
+
second level is detections within frame
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Yolov8(ObjectDetection):
|
|
80
|
+
"""Wrapper to YOLOv8 object detection model.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
weights (str | Path): Custom model weights for prediction.
|
|
84
|
+
model: (YOLOv8): the YOLOv8 model to use for prediction.
|
|
85
|
+
confidence (float): the confidence threshold.
|
|
86
|
+
iou (float): the IOU threshold.
|
|
87
|
+
img_size (int): the YOLOv8 img size.
|
|
88
|
+
half_precision (bool): Whether to use half precision (FP16) for inference speed
|
|
89
|
+
up.
|
|
90
|
+
normalized (bool): Whether the bounding boxes are to be returned normalized.
|
|
91
|
+
frame_rotator: (AvVideoFrameRotator): use case to use rotate video frames.
|
|
92
|
+
get_number_of_frames: (Callable[[Path], int]): function to get the total number
|
|
93
|
+
of frames of a video.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
weights: str | Path,
|
|
99
|
+
model: YOLOv8,
|
|
100
|
+
confidence: float,
|
|
101
|
+
iou: float,
|
|
102
|
+
img_size: int,
|
|
103
|
+
half_precision: bool,
|
|
104
|
+
normalized: bool,
|
|
105
|
+
frame_rotator: AvVideoFrameRotator,
|
|
106
|
+
get_number_of_frames: Callable[[Path], int] = video.get_number_of_frames,
|
|
107
|
+
) -> None:
|
|
108
|
+
self.weights = weights
|
|
109
|
+
self.model = model
|
|
110
|
+
self.confidence = confidence
|
|
111
|
+
self.iou = iou
|
|
112
|
+
self.img_size = img_size
|
|
113
|
+
self.half_precision = half_precision
|
|
114
|
+
self.normalized = normalized
|
|
115
|
+
self._frame_rotator = frame_rotator
|
|
116
|
+
self._get_number_of_frames = get_number_of_frames
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def classifications(self) -> dict[int, str]:
|
|
120
|
+
"""The model's classes that it is able to predict.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
dict[int, str]: the classes
|
|
124
|
+
"""
|
|
125
|
+
return (
|
|
126
|
+
self.model.names
|
|
127
|
+
if self.model.names is not None
|
|
128
|
+
else self.model.predictor.model.names
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def detect(
|
|
132
|
+
self, file: Path, detect_start: int | None = None, detect_end: int | None = None
|
|
133
|
+
) -> list[list[Detection]]:
|
|
134
|
+
"""Run object detection on video and return detection result.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
file (Path): the video to run object detection on.
|
|
138
|
+
detect_start (int | None, optional): Start of the detection range in frames.
|
|
139
|
+
Defaults to None.
|
|
140
|
+
detect_end (int | None, optional): End of the detection range in frames.
|
|
141
|
+
Defaults to None.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
list[list[Detection]]: the detections for each frame in the video
|
|
145
|
+
"""
|
|
146
|
+
frames: list[list[Detection]] = []
|
|
147
|
+
length = self._get_number_of_frames(file)
|
|
148
|
+
for prediction_result in tqdm(
|
|
149
|
+
self._predict(file, detect_start, detect_end),
|
|
150
|
+
desc="Detected frames",
|
|
151
|
+
unit=" frames",
|
|
152
|
+
total=length,
|
|
153
|
+
):
|
|
154
|
+
frames.append(prediction_result)
|
|
155
|
+
|
|
156
|
+
return frames
|
|
157
|
+
|
|
158
|
+
def _predict(
|
|
159
|
+
self, video: Path, detect_start: int | None, detect_end: int | None
|
|
160
|
+
) -> Generator[list[Detection], None, None]:
|
|
161
|
+
start = 0
|
|
162
|
+
if detect_start is not None:
|
|
163
|
+
start = detect_start
|
|
164
|
+
|
|
165
|
+
with av.open(str(video.absolute())) as container:
|
|
166
|
+
container.streams.video[0].thread_type = "AUTO"
|
|
167
|
+
side_data = container.streams.video[0].side_data
|
|
168
|
+
for frame_number, frame in enumerate(container.decode(video=0), start=1):
|
|
169
|
+
if start <= frame_number and (
|
|
170
|
+
detect_end is None or frame_number < detect_end
|
|
171
|
+
):
|
|
172
|
+
rotated_image = self._frame_rotator.rotate(frame, side_data)
|
|
173
|
+
results = self.model.predict(
|
|
174
|
+
source=rotated_image,
|
|
175
|
+
conf=self.confidence,
|
|
176
|
+
iou=self.iou,
|
|
177
|
+
half=self.half_precision,
|
|
178
|
+
imgsz=self.img_size,
|
|
179
|
+
device=0 if torch.cuda.is_available() else "cpu",
|
|
180
|
+
stream=False,
|
|
181
|
+
verbose=False,
|
|
182
|
+
agnostic_nms=True,
|
|
183
|
+
)
|
|
184
|
+
for result in results:
|
|
185
|
+
yield self._parse_detections(result.boxes)
|
|
186
|
+
else:
|
|
187
|
+
yield []
|
|
188
|
+
|
|
189
|
+
def _parse_detections(self, detection_result: Boxes) -> list[Detection]:
|
|
190
|
+
bboxes = detection_result.xywhn if self.normalized else detection_result.xywh
|
|
191
|
+
detections: list[Detection] = []
|
|
192
|
+
for bbox, class_idx, confidence in zip(
|
|
193
|
+
bboxes, detection_result.cls, detection_result.conf
|
|
194
|
+
):
|
|
195
|
+
detections.append(
|
|
196
|
+
self._parse_detection(bbox, int(class_idx.item()), confidence.item())
|
|
197
|
+
)
|
|
198
|
+
return detections
|
|
199
|
+
|
|
200
|
+
def _parse_detection(
|
|
201
|
+
self, bbox: torch.Tensor, class_idx: int, confidence: float
|
|
202
|
+
) -> Detection:
|
|
203
|
+
x, y, width, height = bbox.tolist()
|
|
204
|
+
classification = self.classifications[class_idx]
|
|
205
|
+
|
|
206
|
+
return Detection(
|
|
207
|
+
label=classification,
|
|
208
|
+
conf=confidence,
|
|
209
|
+
x=x - width / 2,
|
|
210
|
+
y=y - height / 2,
|
|
211
|
+
w=width,
|
|
212
|
+
h=height,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def create_model(
|
|
217
|
+
weights: str | Path,
|
|
218
|
+
confidence: float,
|
|
219
|
+
iou: float,
|
|
220
|
+
img_size: int,
|
|
221
|
+
half_precision: bool,
|
|
222
|
+
normalized: bool,
|
|
223
|
+
) -> Yolov8:
|
|
224
|
+
"""Loads a custom trained or a pretrained YOLOv8 mode.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
weights (str | Path): Either path to custom model weights or pretrained model
|
|
228
|
+
name, i.e. 'yolov8s', 'yolov8m'.
|
|
229
|
+
confidence (float): the confidence threshold.
|
|
230
|
+
iou (float): the IOU threshold
|
|
231
|
+
img_size (int): the YOLOv8 image size
|
|
232
|
+
half_precision (bool): Whether to use half precision (FP16) for inference speed
|
|
233
|
+
up.
|
|
234
|
+
normalized (bool): Whether the bounding boxes are to be returned normalized
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Yolov8: the YOLOv8 model
|
|
238
|
+
"""
|
|
239
|
+
log.info(f"Try loading model {weights}")
|
|
240
|
+
t1 = perf_counter()
|
|
241
|
+
is_custom = Path(weights).is_file()
|
|
242
|
+
model = Yolov8(
|
|
243
|
+
weights=weights,
|
|
244
|
+
model=_load_model(weights),
|
|
245
|
+
confidence=confidence,
|
|
246
|
+
iou=iou,
|
|
247
|
+
img_size=img_size,
|
|
248
|
+
half_precision=half_precision,
|
|
249
|
+
normalized=normalized,
|
|
250
|
+
frame_rotator=AvVideoFrameRotator(),
|
|
251
|
+
)
|
|
252
|
+
t2 = perf_counter()
|
|
253
|
+
|
|
254
|
+
model_source = "Custom" if is_custom else "Pretrained"
|
|
255
|
+
model_type = "CUDA" if torch.cuda.is_available() else "CPU"
|
|
256
|
+
runtime = round(t2 - t1)
|
|
257
|
+
log.info(f"{model_source} {model_type} model loaded in {runtime} sec")
|
|
258
|
+
|
|
259
|
+
model_success_msg = f"Model {weights} prepared"
|
|
260
|
+
log.info(model_success_msg)
|
|
261
|
+
|
|
262
|
+
return model
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _load_model(weights: str | Path) -> YOLOv8:
|
|
266
|
+
"""Load a custom trained or a pretrained YOLOv8 model.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
weights (str | Path): Either path to custom model weights or pretrained model
|
|
270
|
+
name, i.e. 'yolov8s', 'yolov8m'.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
YOLOv8: the YOLOv8 model.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
model = YOLOv8(model=weights, task="detect")
|
|
277
|
+
return model
|
|
File without changes
|
OTVision/domain/cli.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CliArgs(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def get_config_file(self) -> Path | None:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CliParseError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DetectCliArgs(CliArgs):
|
|
19
|
+
expected_duration: timedelta | None
|
|
20
|
+
paths: list[Path] | None
|
|
21
|
+
config_file: Path | None
|
|
22
|
+
logfile: Path
|
|
23
|
+
logfile_overwrite: bool
|
|
24
|
+
log_level_console: str | None
|
|
25
|
+
log_level_file: str | None
|
|
26
|
+
weights: str | None = None
|
|
27
|
+
conf: float | None = None
|
|
28
|
+
iou: float | None = None
|
|
29
|
+
imagesize: int | None = None
|
|
30
|
+
half: bool | None = None
|
|
31
|
+
overwrite: bool | None = None
|
|
32
|
+
detect_start: int | None = None
|
|
33
|
+
detect_end: int | None = None
|
|
34
|
+
|
|
35
|
+
def get_config_file(self) -> Path | None:
|
|
36
|
+
return self.config_file
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DetectCliParser(ABC):
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def parse(self) -> DetectCliArgs:
|
|
42
|
+
raise NotImplementedError
|
|
File without changes
|
OTVision/helpers/date.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_date_string_to_utc_datime(date_string: str, date_format: str) -> datetime:
|
|
5
|
+
"""Parse a date string to a datetime object with UTC set as timezone.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
date_string (str): the date string
|
|
9
|
+
date_format (str): the date format
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
datetime: the datetime object with UTC as set timezone
|
|
13
|
+
"""
|
|
14
|
+
return datetime.strptime(date_string, date_format).replace(tzinfo=timezone.utc)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_timestamp_string_to_utc_datetime(timestamp: str | float) -> datetime:
|
|
18
|
+
"""Parse timestamp string to datetime object with UTC set as timezone.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
timestamp (str | float): the timestamp string to be parsed
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
datetime: the datetime object with UTC as set timezone
|
|
25
|
+
"""
|
|
26
|
+
return datetime.fromtimestamp(float(timestamp), timezone.utc)
|