OTVision 0.6.3__py3-none-any.whl → 0.6.5__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 (53) hide show
  1. OTVision/abstraction/defaults.py +15 -0
  2. OTVision/application/buffer.py +2 -1
  3. OTVision/application/config.py +475 -0
  4. OTVision/application/config_parser.py +280 -0
  5. OTVision/application/configure_logger.py +1 -1
  6. OTVision/application/detect/factory.py +1 -1
  7. OTVision/application/detect/update_detect_config_with_cli_args.py +2 -1
  8. OTVision/application/get_config.py +2 -1
  9. OTVision/application/get_current_config.py +1 -1
  10. OTVision/application/track/__init__.py +0 -0
  11. OTVision/application/track/get_track_cli_args.py +20 -0
  12. OTVision/application/track/update_current_track_config.py +42 -0
  13. OTVision/application/track/update_track_config_with_cli_args.py +52 -0
  14. OTVision/application/update_current_config.py +1 -1
  15. OTVision/config.py +61 -668
  16. OTVision/convert/convert.py +3 -3
  17. OTVision/detect/builder.py +27 -20
  18. OTVision/detect/cli.py +3 -3
  19. OTVision/detect/detected_frame_buffer.py +6 -0
  20. OTVision/detect/file_based_detect_builder.py +19 -0
  21. OTVision/detect/otdet.py +54 -1
  22. OTVision/detect/otdet_file_writer.py +3 -2
  23. OTVision/detect/rtsp_based_detect_builder.py +37 -0
  24. OTVision/detect/rtsp_input_source.py +199 -0
  25. OTVision/detect/timestamper.py +1 -1
  26. OTVision/detect/video_input_source.py +3 -3
  27. OTVision/detect/yolo.py +17 -1
  28. OTVision/domain/cli.py +31 -1
  29. OTVision/domain/current_config.py +1 -1
  30. OTVision/domain/frame.py +9 -0
  31. OTVision/domain/object_detection.py +6 -1
  32. OTVision/domain/serialization.py +12 -0
  33. OTVision/domain/time.py +13 -0
  34. OTVision/helpers/files.py +14 -15
  35. OTVision/plugin/__init__.py +0 -0
  36. OTVision/plugin/yaml_serialization.py +20 -0
  37. OTVision/track/builder.py +132 -0
  38. OTVision/track/cli.py +128 -0
  39. OTVision/track/exporter/filebased_exporter.py +2 -1
  40. OTVision/track/id_generator.py +15 -0
  41. OTVision/track/model/track_exporter.py +2 -1
  42. OTVision/track/model/tracking_interfaces.py +6 -6
  43. OTVision/track/parser/frame_group_parser_plugins.py +35 -5
  44. OTVision/track/track.py +54 -133
  45. OTVision/track/tracker/filebased_tracking.py +8 -7
  46. OTVision/track/tracker/tracker_plugin_iou.py +14 -9
  47. OTVision/transform/transform.py +2 -2
  48. OTVision/version.py +1 -1
  49. otvision-0.6.5.dist-info/METADATA +182 -0
  50. {otvision-0.6.3.dist-info → otvision-0.6.5.dist-info}/RECORD +52 -35
  51. otvision-0.6.3.dist-info/METADATA +0 -49
  52. {otvision-0.6.3.dist-info → otvision-0.6.5.dist-info}/WHEEL +0 -0
  53. {otvision-0.6.3.dist-info → otvision-0.6.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,15 @@
1
+ def value_or_default[T](value: T | None, default: T) -> T:
2
+ """
3
+ Returns the provided value if it is not None; otherwise, returns a default value.
4
+
5
+ Args:
6
+ value (T | None): The value to be evaluated.
7
+ default (T): The fallback value if 'value' is None.
8
+
9
+ Returns:
10
+ T: The provided value or the default value if the original is None.
11
+ """
12
+
13
+ if value is not None:
14
+ return value
15
+ return default
@@ -22,7 +22,8 @@ class Buffer[T, SUBJECT_TYPE, OBSERVING_TYPE](Observable[SUBJECT_TYPE], Filter[T
22
22
  return self._buffer
23
23
 
24
24
  def _reset_buffer(self) -> None:
25
- self._buffer = []
25
+ del self._buffer
26
+ self._buffer = list()
26
27
 
27
28
  def on_flush(self, event: OBSERVING_TYPE) -> None:
28
29
  buffered_elements = self._get_buffered_elements()
@@ -0,0 +1,475 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime, timedelta
3
+ from pathlib import Path
4
+
5
+ AVAILABLE_WEIGHTS = "AVAILABLEWEIGHTS"
6
+ CALIBRATIONS = "CALIBRATIONS"
7
+ COL_WIDTH = "COLWIDTH"
8
+ CONF = "CONF"
9
+ CONVERT = "CONVERT"
10
+ DEFAULT_FILETYPE = "DEFAULT_FILETYPE"
11
+ DELETE_INPUT = "DELETE_INPUT"
12
+ ROTATION = "ROTATION"
13
+ DETECT = "DETECT"
14
+ DETECTIONS = "DETECTIONS"
15
+ FILETYPES = "FILETYPES"
16
+ FONT = "FONT"
17
+ FONT_SIZE = "FONTSIZE"
18
+ FPS_FROM_FILENAME = "FPS_FROM_FILENAME"
19
+ FRAME_WIDTH = "FRAMEWIDTH"
20
+ GUI = "GUI"
21
+ HALF_PRECISION = "HALF_PRECISION"
22
+ INPUT_FPS = "INPUT_FPS"
23
+ IMG = "IMG"
24
+ IMG_SIZE = "IMGSIZE"
25
+ IOU = "IOU"
26
+ LAST_PATHS = "LAST PATHS"
27
+ LOCATION_X = "LOCATION_X"
28
+ LOCATION_Y = "LOCATION_Y"
29
+ NORMALIZED = "NORMALIZED"
30
+ OTC_ICON = "OTC ICON"
31
+ OUTPUT_FPS = "OUTPUT_FPS"
32
+ OUTPUT_FILETYPE = "OUTPUT_FILETYPE"
33
+ OVERWRITE = "OVERWRITE"
34
+ PATHS = "PATHS"
35
+ RUN_CHAINED = "RUN_CHAINED"
36
+ EXPECTED_DURATION = "EXPECTED_DURATION"
37
+ REFPTS = "REFPTS"
38
+ SEARCH_SUBDIRS = "SEARCH_SUBDIRS"
39
+ SIGMA_H = "SIGMA_H"
40
+ SIGMA_IOU = "SIGMA_IOU"
41
+ SIGMA_L = "SIGMA_L"
42
+ T_MIN = "T_MIN"
43
+ T_MISS_MAX = "T_MISS_MAX"
44
+ TRACK = "TRACK"
45
+ TRACKS = "TRACKS"
46
+ TRANSFORM = "TRANSFORM"
47
+ UNDISTORT = "UNDISTORT"
48
+ VID = "VID"
49
+ VID_ROTATABLE = "VID_ROTATABLE"
50
+ VIDEOS = "VIDEOS"
51
+ WEIGHTS = "WEIGHTS"
52
+ WINDOW = "WINDOW"
53
+ YOLO = "YOLO"
54
+ LOG = "LOG"
55
+ LOG_LEVEL_CONSOLE = "LOG_LEVEL_CONSOLE"
56
+ LOG_LEVEL_FILE = "LOG_LEVEL_FILE"
57
+ LOG_DIR = "LOG_DIR"
58
+ START_TIME = "START_TIME"
59
+ DETECT_END = "DETECT_END"
60
+ DETECT_START = "DETECT_START"
61
+ DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S"
62
+ DEFAULT_EXPECTED_DURATION: timedelta = timedelta(minutes=15)
63
+ """Default length of a video is 15 minutes."""
64
+ STREAM = "STREAM"
65
+ STREAM_SAVE_DIR = "SAVE_DIR"
66
+ STREAM_NAME = "NAME"
67
+ STREAM_SOURCE = "SOURCE"
68
+ FLUSH_BUFFER_SIZE = "FLUSH_BUFFER_SIZE"
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class _LogConfig:
73
+ log_level_console: str = "WARNING"
74
+ log_level_file: str = "DEBUG"
75
+
76
+ def to_dict(self) -> dict:
77
+ return {
78
+ LOG_LEVEL_CONSOLE: self.log_level_console,
79
+ LOG_LEVEL_FILE: self.log_level_file,
80
+ }
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class _DefaultFiletype:
85
+ video: str = ".mp4"
86
+ image: str = ".jpg"
87
+ detect: str = ".otdet"
88
+ track: str = ".ottrk"
89
+ refpts: str = ".otrfpts"
90
+
91
+ def to_dict(self) -> dict:
92
+ return {
93
+ VID: self.video,
94
+ IMG: self.image,
95
+ DETECT: self.detect,
96
+ TRACK: self.track,
97
+ REFPTS: self.refpts,
98
+ }
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class _VideoFiletypes:
103
+ avi: str = ".avi"
104
+ mkv: str = ".mkv"
105
+ mov: str = ".mov"
106
+ mp4: str = ".mp4"
107
+
108
+ def to_list(self) -> list:
109
+ return [
110
+ self.avi,
111
+ self.mkv,
112
+ self.mov,
113
+ self.mp4,
114
+ ]
115
+
116
+ def rotatable_to_list(self) -> list:
117
+ return [self.mov, self.mp4]
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class _ImageFiletypes:
122
+ jpg: str = ".jpg"
123
+ jpeg: str = ".jpeg"
124
+ png: str = ".png"
125
+
126
+ def to_list(self) -> list:
127
+ return [self.jpg, self.jpeg, self.png]
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class _Filetypes:
132
+ video_filetypes: _VideoFiletypes = _VideoFiletypes()
133
+ image_filetypes: _ImageFiletypes = _ImageFiletypes()
134
+ detect: str = _DefaultFiletype.detect
135
+ track: str = _DefaultFiletype.track
136
+ refpts: str = _DefaultFiletype.refpts
137
+ transform: str = ".gpkg"
138
+
139
+ def to_dict(self) -> dict:
140
+ return {
141
+ VID: self.video_filetypes.to_list(),
142
+ VID_ROTATABLE: self.video_filetypes.rotatable_to_list(),
143
+ IMG: self.image_filetypes.to_list(),
144
+ DETECT: [self.detect],
145
+ TRACK: [self.track],
146
+ REFPTS: [self.refpts],
147
+ TRANSFORM: [self.transform],
148
+ }
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class _LastPaths:
153
+ videos: list = field(default_factory=list)
154
+ detections: list = field(default_factory=list)
155
+ tracks: list = field(default_factory=list)
156
+ calibrations: list = field(default_factory=list)
157
+ refpts: list = field(default_factory=list)
158
+
159
+ def to_dict(self) -> dict:
160
+ return {
161
+ VIDEOS: self.videos,
162
+ DETECTIONS: self.detections,
163
+ TRACKS: self.tracks,
164
+ CALIBRATIONS: self.calibrations,
165
+ REFPTS: self.refpts,
166
+ }
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class ConvertConfig:
171
+ paths: list[str] = field(default_factory=list)
172
+ run_chained: bool = True
173
+ output_filetype: str = _VideoFiletypes.mp4
174
+ input_fps: float = 20.0
175
+ output_fps: float = 20.0
176
+ fps_from_filename: bool = True
177
+ delete_input: bool = False
178
+ rotation: int = 0
179
+ overwrite: bool = True
180
+
181
+ def to_dict(self) -> dict:
182
+ return {
183
+ PATHS: self.paths,
184
+ RUN_CHAINED: self.run_chained,
185
+ OUTPUT_FILETYPE: self.output_filetype,
186
+ INPUT_FPS: self.input_fps,
187
+ OUTPUT_FPS: self.output_fps,
188
+ FPS_FROM_FILENAME: self.fps_from_filename,
189
+ DELETE_INPUT: self.delete_input,
190
+ ROTATION: self.rotation,
191
+ OVERWRITE: self.overwrite,
192
+ }
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class _YoloWeights:
197
+ yolov8s: str = "yolov8s"
198
+ yolov8m: str = "yolov8m"
199
+ yolov8l: str = "yolov8l"
200
+ yolov8x: str = "yolov8x"
201
+
202
+ def to_list(self) -> list:
203
+ return [self.yolov8s, self.yolov8m, self.yolov8l, self.yolov8x]
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class YoloConfig:
208
+ """Represents the configuration for the YOLO model.
209
+
210
+ Attributes:
211
+ weights (str): Path to YOLO model weights.
212
+ available_weights (_YoloWeights): List of available default YOLO model weights.
213
+ conf (float): Confidence threshold.
214
+ iou (float): Intersection over union threshold.
215
+ img_size (int): Size of the input image.
216
+ chunk_size (int): Chunk size for processing.
217
+ normalized (bool): Whether to normalize the bounding boxes.
218
+ """
219
+
220
+ weights: str = _YoloWeights.yolov8s
221
+ available_weights: _YoloWeights = _YoloWeights()
222
+ conf: float = 0.25
223
+ iou: float = 0.45
224
+ img_size: int = 640
225
+ chunk_size: int = 1
226
+ normalized: bool = False
227
+
228
+ def to_dict(self) -> dict:
229
+ return {
230
+ WEIGHTS: self.weights,
231
+ AVAILABLE_WEIGHTS: self.available_weights.to_list(),
232
+ CONF: self.conf,
233
+ IOU: self.iou,
234
+ IMG_SIZE: self.img_size,
235
+ NORMALIZED: self.normalized,
236
+ }
237
+
238
+
239
+ @dataclass(frozen=True)
240
+ class DetectConfig:
241
+ """Represents the configuration for the `detect` command.
242
+
243
+ Attributes:
244
+ paths (list[Path]): List of files to be processed.
245
+ run_chained (bool): Whether to run chained commands.
246
+ yolo_config (YoloConfig): Configuration for the YOLO model.
247
+ expected_duration (timedelta | None): Expected duration of the video.
248
+ `None` if unknown.
249
+ overwrite (bool): Whether to overwrite existing files.
250
+ half_precision (bool): Whether to use half precision.
251
+ detect_start (int | None): Start frame for detection expressed in seconds.
252
+ Value `None` marks the start of the video.
253
+ detect_end (int | None): End frame for detection expressed in seconds.
254
+ Value `None` marks the end of the video.
255
+
256
+ """
257
+
258
+ @property
259
+ def confidence(self) -> float:
260
+ """Gets the confidence level set in the YOLO configuration.
261
+
262
+ Returns:
263
+ float: The intersection over union threshold value.
264
+ """
265
+ return self.yolo_config.conf
266
+
267
+ @property
268
+ def weights(self) -> str:
269
+ return self.yolo_config.weights
270
+
271
+ @property
272
+ def iou(self) -> float:
273
+ return self.yolo_config.iou
274
+
275
+ @property
276
+ def img_size(self) -> int:
277
+ return self.yolo_config.img_size
278
+
279
+ @property
280
+ def normalized(self) -> bool:
281
+ return self.yolo_config.normalized
282
+
283
+ paths: list[str] = field(default_factory=list)
284
+ run_chained: bool = True
285
+ yolo_config: YoloConfig = YoloConfig()
286
+ expected_duration: timedelta | None = None
287
+ overwrite: bool = True
288
+ half_precision: bool = False
289
+ start_time: datetime | None = None
290
+ detect_start: int | None = None
291
+ detect_end: int | None = None
292
+
293
+ def to_dict(self) -> dict:
294
+ expected_duration = (
295
+ int(self.expected_duration.total_seconds())
296
+ if self.expected_duration is not None
297
+ else None
298
+ )
299
+ return {
300
+ PATHS: [str(p) for p in self.paths],
301
+ RUN_CHAINED: self.run_chained,
302
+ YOLO: self.yolo_config.to_dict(),
303
+ EXPECTED_DURATION: expected_duration,
304
+ OVERWRITE: self.overwrite,
305
+ HALF_PRECISION: self.half_precision,
306
+ START_TIME: self.start_time,
307
+ DETECT_START: self.detect_start,
308
+ DETECT_END: self.detect_end,
309
+ }
310
+
311
+
312
+ @dataclass(frozen=True)
313
+ class _TrackIouConfig:
314
+ sigma_l: float = 0.27
315
+ sigma_h: float = 0.42
316
+ sigma_iou: float = 0.38
317
+ t_min: int = 5
318
+ t_miss_max: int = 51
319
+
320
+ def to_dict(self) -> dict:
321
+ return {
322
+ SIGMA_L: self.sigma_l,
323
+ SIGMA_H: self.sigma_h,
324
+ SIGMA_IOU: self.sigma_iou,
325
+ T_MIN: self.t_min,
326
+ T_MISS_MAX: self.t_miss_max,
327
+ }
328
+
329
+
330
+ @dataclass(frozen=True)
331
+ class TrackConfig:
332
+ @property
333
+ def sigma_l(self) -> float:
334
+ return self.iou.sigma_l
335
+
336
+ @property
337
+ def sigma_h(self) -> float:
338
+ return self.iou.sigma_h
339
+
340
+ @property
341
+ def sigma_iou(self) -> float:
342
+ return self.iou.sigma_iou
343
+
344
+ @property
345
+ def t_min(self) -> int:
346
+ return self.iou.t_min
347
+
348
+ @property
349
+ def t_miss_max(self) -> int:
350
+ return self.iou.t_miss_max
351
+
352
+ paths: list[str] = field(default_factory=list)
353
+ run_chained: bool = True
354
+ iou: _TrackIouConfig = _TrackIouConfig()
355
+ overwrite: bool = True
356
+
357
+ def to_dict(self) -> dict:
358
+ return {
359
+ PATHS: [str(p) for p in self.paths],
360
+ RUN_CHAINED: self.run_chained,
361
+ IOU: self.iou.to_dict(),
362
+ OVERWRITE: self.overwrite,
363
+ }
364
+
365
+
366
+ @dataclass(frozen=True)
367
+ class _UndistortConfig:
368
+ overwrite: bool = False
369
+
370
+ def to_dict(self) -> dict:
371
+ return {OVERWRITE: self.overwrite}
372
+
373
+
374
+ @dataclass(frozen=True)
375
+ class _TransformConfig:
376
+ paths: list[str] = field(default_factory=list)
377
+ run_chained: bool = True
378
+ overwrite: bool = True
379
+
380
+ def to_dict(self) -> dict:
381
+ return {
382
+ PATHS: [str(p) for p in self.paths],
383
+ RUN_CHAINED: self.run_chained,
384
+ OVERWRITE: self.overwrite,
385
+ }
386
+
387
+
388
+ @dataclass(frozen=True)
389
+ class _GuiWindowConfig:
390
+ location_x: int = 0
391
+ location_y: int = 0
392
+
393
+ def to_dict(self) -> dict:
394
+ return {LOCATION_X: self.location_x, LOCATION_Y: self.location_y}
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class _GuiConfig:
399
+ otc_icon: str = str(Path(__file__).parents[0] / r"view" / r"helpers" / r"OTC.ico")
400
+ font: str = "Open Sans"
401
+ font_size: int = 12
402
+ window_config: _GuiWindowConfig = _GuiWindowConfig()
403
+ frame_width: int = 80
404
+ col_width: int = 50
405
+
406
+ def to_dict(self) -> dict:
407
+ return {
408
+ OTC_ICON: self.otc_icon,
409
+ FONT: self.font,
410
+ FONT_SIZE: self.font_size,
411
+ WINDOW: self.window_config.to_dict(),
412
+ FRAME_WIDTH: self.frame_width,
413
+ COL_WIDTH: self.col_width,
414
+ }
415
+
416
+
417
+ @dataclass(frozen=True)
418
+ class StreamConfig:
419
+ name: str
420
+ source: str
421
+ save_dir: Path
422
+ flush_buffer_size: int
423
+
424
+ def to_dict(self) -> dict:
425
+ return {
426
+ STREAM_NAME: self.name,
427
+ STREAM_SOURCE: self.source,
428
+ STREAM_SAVE_DIR: str(self.save_dir),
429
+ FLUSH_BUFFER_SIZE: self.flush_buffer_size,
430
+ }
431
+
432
+
433
+ @dataclass
434
+ class Config:
435
+ """Represents the OTVision config file.
436
+
437
+ Provides methods to parse in a custom config file from a dict or a YAML file.
438
+ Updates the default configuration with the custom config.
439
+ """
440
+
441
+ log: _LogConfig = _LogConfig()
442
+ search_subdirs: bool = True
443
+ default_filetype: _DefaultFiletype = _DefaultFiletype()
444
+ filetypes: _Filetypes = _Filetypes()
445
+ last_paths: _LastPaths = _LastPaths()
446
+ convert: ConvertConfig = ConvertConfig()
447
+ detect: DetectConfig = DetectConfig()
448
+ track: TrackConfig = TrackConfig()
449
+ undistort: _UndistortConfig = _UndistortConfig()
450
+ transform: _TransformConfig = _TransformConfig()
451
+ gui: _GuiConfig = _GuiConfig()
452
+ stream: StreamConfig | None = None
453
+
454
+ def to_dict(self) -> dict:
455
+ """Returns the OTVision config as a dict.
456
+
457
+ Returns:
458
+ dict: The OTVision config.
459
+ """
460
+ data = {
461
+ LOG: self.log.to_dict(),
462
+ SEARCH_SUBDIRS: self.search_subdirs,
463
+ DEFAULT_FILETYPE: self.default_filetype.to_dict(),
464
+ FILETYPES: self.filetypes.to_dict(),
465
+ LAST_PATHS: self.last_paths.to_dict(),
466
+ CONVERT: self.convert.to_dict(),
467
+ DETECT: self.detect.to_dict(),
468
+ TRACK: self.track.to_dict(),
469
+ UNDISTORT: self.undistort.to_dict(),
470
+ TRANSFORM: self.transform.to_dict(),
471
+ GUI: self.gui.to_dict(),
472
+ }
473
+ if self.stream is not None:
474
+ data[STREAM] = self.stream.to_dict()
475
+ return data