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.
Files changed (50) hide show
  1. OTVision/__init__.py +30 -0
  2. OTVision/application/__init__.py +0 -0
  3. OTVision/application/configure_logger.py +23 -0
  4. OTVision/application/detect/__init__.py +0 -0
  5. OTVision/application/detect/get_detect_cli_args.py +9 -0
  6. OTVision/application/detect/update_detect_config_with_cli_args.py +95 -0
  7. OTVision/application/get_config.py +25 -0
  8. OTVision/config.py +754 -0
  9. OTVision/convert/__init__.py +0 -0
  10. OTVision/convert/convert.py +318 -0
  11. OTVision/dataformat.py +70 -0
  12. OTVision/detect/__init__.py +0 -0
  13. OTVision/detect/builder.py +48 -0
  14. OTVision/detect/cli.py +166 -0
  15. OTVision/detect/detect.py +296 -0
  16. OTVision/detect/otdet.py +103 -0
  17. OTVision/detect/plugin_av/__init__.py +0 -0
  18. OTVision/detect/plugin_av/rotate_frame.py +37 -0
  19. OTVision/detect/yolo.py +277 -0
  20. OTVision/domain/__init__.py +0 -0
  21. OTVision/domain/cli.py +42 -0
  22. OTVision/helpers/__init__.py +0 -0
  23. OTVision/helpers/date.py +26 -0
  24. OTVision/helpers/files.py +538 -0
  25. OTVision/helpers/formats.py +139 -0
  26. OTVision/helpers/log.py +131 -0
  27. OTVision/helpers/machine.py +71 -0
  28. OTVision/helpers/video.py +54 -0
  29. OTVision/track/__init__.py +0 -0
  30. OTVision/track/iou.py +282 -0
  31. OTVision/track/iou_util.py +140 -0
  32. OTVision/track/preprocess.py +451 -0
  33. OTVision/track/track.py +422 -0
  34. OTVision/transform/__init__.py +0 -0
  35. OTVision/transform/get_homography.py +156 -0
  36. OTVision/transform/reference_points_picker.py +462 -0
  37. OTVision/transform/transform.py +352 -0
  38. OTVision/version.py +13 -0
  39. OTVision/view/__init__.py +0 -0
  40. OTVision/view/helpers/OTC.ico +0 -0
  41. OTVision/view/view.py +90 -0
  42. OTVision/view/view_convert.py +128 -0
  43. OTVision/view/view_detect.py +146 -0
  44. OTVision/view/view_helpers.py +417 -0
  45. OTVision/view/view_track.py +131 -0
  46. OTVision/view/view_transform.py +140 -0
  47. otvision-0.5.3.dist-info/METADATA +47 -0
  48. otvision-0.5.3.dist-info/RECORD +50 -0
  49. otvision-0.5.3.dist-info/WHEEL +4 -0
  50. otvision-0.5.3.dist-info/licenses/LICENSE +674 -0
@@ -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
@@ -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)