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,462 @@
1
+ """
2
+ OTVision tool to select reference points for transformation
3
+ from pixel to world coordinates
4
+ """
5
+
6
+ # Copyright (C) 2022 OpenTrafficCam Contributors
7
+ # <https://github.com/OpenTrafficCam
8
+ # <team@opentrafficcam.org>
9
+ #
10
+ # This program is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
22
+
23
+
24
+ import json
25
+ import logging
26
+ import tkinter as tk
27
+ from pathlib import Path
28
+ from random import randrange
29
+ from tkinter import ttk
30
+ from tkinter.simpledialog import Dialog
31
+ from typing import Any, Union
32
+
33
+ import cv2
34
+
35
+ from OTVision.helpers.files import is_image, is_video
36
+ from OTVision.helpers.log import LOGGER_NAME
37
+
38
+ log = logging.getLogger(LOGGER_NAME)
39
+
40
+
41
+ class ReferencePointsPicker:
42
+ """Class to pick reference points in pixel coordinates for transform subpackage.
43
+
44
+ Instructions for using the gui:
45
+ Hold left mouse button Find spot for reference point with magnifier
46
+ Release left mouse button Mark reference point
47
+ CTRL + Z Unmark last reference point
48
+ CTRL + Y Remark last reference point
49
+ CTRL + S Save image with markers
50
+ CTRL + N Show next frame (disabled if image was loaded)
51
+ CTRL + R Show random frame (disabled if image was loaded)
52
+ ESC Close window and return reference points
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ file: Path,
58
+ title: str = "Reference Points Picker",
59
+ popup_root: Union[tk.Tk, None] = None,
60
+ ):
61
+ # Attributes
62
+ self.title = title
63
+ self.left_button_down = False
64
+ self.refpts: dict = {}
65
+ self.historic_refpts: dict = {}
66
+ self.file = file
67
+ self.refpts_file = file.with_suffix(".otrfpts")
68
+ self.image = None
69
+ self.video = None
70
+ self.popup_root = popup_root
71
+
72
+ # Initial method calls
73
+ self.update_base_image()
74
+ self.show()
75
+
76
+ # ----------- Handle OpenCV gui -----------
77
+
78
+ def update_base_image(self, random_frame: bool = False):
79
+ log.debug("update base image")
80
+ if is_image(self.file):
81
+ self.get_image()
82
+ self.video = None
83
+ elif is_video(self.file):
84
+ self.get_frame_frome_video(random_frame=random_frame)
85
+ else:
86
+ raise ValueError("The file path provided has to be an image or a video")
87
+ self.draw_refpts()
88
+
89
+ def get_image(self):
90
+ """Set base_image with image from a file
91
+
92
+ Raises:
93
+ ImageWontOpenError: If image wont open
94
+ """
95
+ try:
96
+ self.base_image = cv2.imread(self.image_file)
97
+ except Exception as e:
98
+ raise ImageWontOpenError(
99
+ f"Error opening this image file: {self.image_file}"
100
+ ) from e
101
+
102
+ def get_frame_frome_video(self, random_frame: bool):
103
+ """Set base image with next or random frame from a video
104
+
105
+ Args:
106
+ random_frame (bool): If true gets a random frame from the video.
107
+ If false gets the next frame of the video.
108
+
109
+ Raises:
110
+ VideoWontOpenError: If video wont open
111
+ FrameNotAvailableError: If frame cannt be read
112
+ """
113
+ if not self.video:
114
+ self.video = cv2.VideoCapture(str(self.file))
115
+ if not self.video.isOpened():
116
+ raise VideoWontOpenError(f"Error opening this video file: {self.file}")
117
+ total_frames = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT))
118
+ if random_frame:
119
+ frame_nr = randrange(0, total_frames)
120
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, frame_nr)
121
+ else:
122
+ frame_nr = self.video.get(cv2.CAP_PROP_POS_FRAMES)
123
+ if frame_nr >= total_frames:
124
+ self.video.set(cv2.CAP_PROP_POS_FRAMES, 0)
125
+ ret, self.base_image = self.video.read()
126
+ if not ret:
127
+ raise FrameNotAvailableError("Video Frame cannot be read correctly")
128
+
129
+ def update_image(self):
130
+ """Show the current image"""
131
+ if not self.left_button_down:
132
+ log.debug("update image")
133
+ cv2.imshow(self.title, self.image)
134
+
135
+ def show(self):
136
+ """Enter OpenCV event loop"""
137
+ cv2.imshow(self.title, self.image)
138
+ cv2.setMouseCallback(self.title, self.handle_mouse_events)
139
+
140
+ while True:
141
+ # wait for a key press to close the window (0 = indefinite loop)
142
+ key = cv2.waitKey(-1) & 0xFF # BUG: #150 on mac
143
+
144
+ window_visible = (
145
+ cv2.getWindowProperty(self.title, cv2.WND_PROP_VISIBLE) >= 1
146
+ )
147
+ if key == 27 or not window_visible:
148
+ break # Exit loop and collapse OpenCV window
149
+ else:
150
+ self.handle_keystrokes(key)
151
+
152
+ cv2.destroyAllWindows()
153
+
154
+ def handle_mouse_events(
155
+ self,
156
+ event: int,
157
+ x_px: int,
158
+ y_px: int,
159
+ flags, # TODO: Correct type hint
160
+ params: Any, # TODO: Correct type hint
161
+ ):
162
+ """Read the current mouse position with a left click and write it
163
+ to the end of the array refpkte and increases the counter by one"""
164
+ if event == cv2.EVENT_LBUTTONUP:
165
+ self.left_button_down = False
166
+ self.add_refpt(x_px, y_px)
167
+ elif event == cv2.EVENT_LBUTTONDOWN:
168
+ self.left_button_down = True
169
+ elif event == cv2.EVENT_MOUSEMOVE:
170
+ if self.left_button_down:
171
+ self.draw_magnifier(x_px, y_px)
172
+ elif event == cv2.EVENT_MOUSEWHEEL:
173
+ if self.left_button_down:
174
+ self.zoom_magnifier(x_px, y_px)
175
+
176
+ def handle_keystrokes(self, key: int):
177
+ if key == 26: # ctrl + z
178
+ self.undo_last_refpt()
179
+ elif key == 25: # ctrl + y
180
+ self.redo_last_refpt()
181
+ elif key == 14: # ctrl + n
182
+ self.update_base_image(random_frame=False)
183
+ elif key == 18: # ctrl + r
184
+ self.update_base_image(random_frame=True)
185
+ elif key == 23: # ctrl+w
186
+ self._write_refpts()
187
+
188
+ # ----------- Handle connections -----------
189
+
190
+ def add_refpt(self, x_px: int, y_px: int):
191
+ """Add a reference point
192
+
193
+ Args:
194
+ x_px (int): x coordinate [px] (horizontal distance from the left)
195
+ y_px (int): y coordinate [px] (vertical distance from the top)
196
+ """
197
+ log.debug("add refpt")
198
+ new_refpt_px = {"x_px": x_px, "y_px": y_px}
199
+ self.draw_refpts(temp_refpt=new_refpt_px)
200
+ if new_refpt_utm := self.get_refpt_utm_from_popup():
201
+ new_refpt = {**new_refpt_px, **new_refpt_utm}
202
+ self.refpts = self.append_refpt(refpts=self.refpts, new_refpt=new_refpt)
203
+ self._log_refpts()
204
+ self.draw_refpts()
205
+
206
+ def undo_last_refpt(self):
207
+ """Remove the last reference point"""
208
+ if self.refpts:
209
+ log.debug("undo last refpt")
210
+ self.refpts, undone_refpt = self.pop_refpt(refpts=self.refpts)
211
+ log.debug(self.refpts)
212
+ log.debug(undone_refpt)
213
+ self.historic_refpts = self.append_refpt(
214
+ refpts=self.historic_refpts, new_refpt=undone_refpt
215
+ )
216
+ self.draw_refpts()
217
+ self._log_refpts()
218
+ else:
219
+ log.debug("refpts empty, cannot undo last refpt")
220
+
221
+ def redo_last_refpt(self):
222
+ """Again add the last removed reference point"""
223
+ if self.historic_refpts:
224
+ log.debug("redo last refpt")
225
+ self.historic_refpts, redone_refpt = self.pop_refpt(
226
+ refpts=self.historic_refpts
227
+ )
228
+ self.append_refpt(refpts=self.refpts, new_refpt=redone_refpt)
229
+ self.draw_refpts()
230
+ self._log_refpts()
231
+ else:
232
+ log.debug("no historc refpts, cannot redo last refpt")
233
+
234
+ def get_refpt_utm_from_popup(self) -> dict:
235
+ """Open a popup to enter utm lat and lon coordinates of the reference point
236
+
237
+ Returns:
238
+ dict: dict of utm coordinates including hemisphere and utm zone
239
+ """
240
+ # TODO: Refactor to smaller methods
241
+ if not self.popup_root:
242
+ self.popup_root = tk.Tk()
243
+ self.popup_root.overrideredirect(True)
244
+ self.popup_root.withdraw()
245
+ refpt_utm_correct = False
246
+ try_again = False
247
+ while not refpt_utm_correct:
248
+ # Get utm part of refpt from pupup
249
+ new_refpt_utm = DialogUTMCoordinates(
250
+ parent=self.popup_root, try_again=try_again
251
+ ).coords_utm
252
+ # Check utm part of refpt
253
+ log.debug(new_refpt_utm)
254
+ if new_refpt_utm: # BUG: ValueError: could not convert string to float: ''
255
+ # BUG: Cancel cancels whole refpt
256
+ # (even pixel coordinates, otherwise stuck in infinite loop)
257
+ hemisphere_correct = isinstance(
258
+ new_refpt_utm["hemisphere"], str
259
+ ) and new_refpt_utm["hemisphere"] in ["N", "S"]
260
+ zone_correct = isinstance(new_refpt_utm["zone_utm"], int) and (
261
+ 1 <= new_refpt_utm["zone_utm"] <= 60
262
+ )
263
+ lon_utm_correct = isinstance(
264
+ new_refpt_utm["lon_utm"], (float, int)
265
+ ) and (100000 <= new_refpt_utm["lon_utm"] <= 900000)
266
+ lat_utm_correct = isinstance(
267
+ new_refpt_utm["lat_utm"], (float, int)
268
+ ) and (0 <= new_refpt_utm["lat_utm"] <= 10000000)
269
+ if (
270
+ hemisphere_correct
271
+ and zone_correct
272
+ and lon_utm_correct
273
+ and lat_utm_correct
274
+ ):
275
+ refpt_utm_correct = True
276
+ else:
277
+ try_again = True
278
+ else:
279
+ break
280
+ return new_refpt_utm
281
+
282
+ # ----------- Edit refpts dict -----------
283
+
284
+ def append_refpt(self, refpts: dict, new_refpt: dict) -> dict:
285
+ """Append a reference point to a dict of reference points.
286
+ Used for valid reference points as well as for deleted reference points.
287
+
288
+ Args:
289
+ refpts (dict): old reference points
290
+ new_refpt (dict): new reference point
291
+
292
+ Returns:
293
+ dict: updated reference points
294
+ """
295
+ log.debug("append refpts")
296
+ new_idx = len(refpts) + 1
297
+ refpts[new_idx] = new_refpt
298
+ return refpts
299
+
300
+ def pop_refpt(self, refpts: dict) -> tuple[dict, dict]:
301
+ """Remove reference point from a dict of reference points.
302
+ Used for valid reference points as well as for deleted reference points.
303
+
304
+ Args:
305
+ refpts (dict): old reference points
306
+
307
+ Returns:
308
+ dict: new reference points
309
+ dict: removed reference point
310
+ """
311
+ log.debug("pop refpts")
312
+ popped_refpt = refpts.popitem()[1]
313
+ return refpts, popped_refpt
314
+
315
+ # ----------- Draw on image -----------
316
+
317
+ def draw_magnifier(self, x_px, y_px):
318
+ log.debug("#TODO: draw magnifier")
319
+
320
+ def zoom_magnifier(self, x_px, y_px):
321
+ log.debug(" # TODO: zoom magnifier")
322
+
323
+ def draw_refpts(self, temp_refpt: Union[dict, None] = None):
324
+ """Draw all the reference points an self.image
325
+
326
+ Args:
327
+ temp_refpt (dict, optional): possible reference point from left button down.
328
+ Defaults to None.
329
+ """
330
+ FONT = cv2.FONT_ITALIC
331
+ FONT_SIZE_REL = 0.02
332
+ MARKER_SIZE_REL = 0.02
333
+ FONT_SIZE_PX = round(self.base_image.shape[0] * FONT_SIZE_REL)
334
+ MARKER_SIZE_PX = round(self.base_image.shape[0] * MARKER_SIZE_REL)
335
+ log.debug("draw refpts")
336
+ self.image = self.base_image.copy()
337
+ refpts = self.refpts.copy()
338
+ if temp_refpt:
339
+ refpts = self.append_refpt(refpts=refpts, new_refpt=temp_refpt)
340
+ for idx, refpt in refpts.items():
341
+ x_px = refpt["x_px"]
342
+ y_px = refpt["y_px"]
343
+ marker_bottom = (x_px, y_px + MARKER_SIZE_PX)
344
+ marker_top = (x_px, y_px - MARKER_SIZE_PX)
345
+ marker_left = (x_px - MARKER_SIZE_PX, y_px)
346
+ marker_right = (x_px + MARKER_SIZE_PX, y_px)
347
+ cv2.line(self.image, marker_bottom, marker_top, (0, 0, 255), 1)
348
+ cv2.line(self.image, marker_left, marker_right, (0, 0, 255), 1)
349
+ cv2.putText(
350
+ self.image,
351
+ str(idx),
352
+ marker_top,
353
+ FONT,
354
+ cv2.getFontScaleFromHeight(FONT, FONT_SIZE_PX),
355
+ (0, 0, 255),
356
+ 1,
357
+ cv2.LINE_AA,
358
+ )
359
+ self.update_image()
360
+
361
+ # ----------- Complete job -----------
362
+
363
+ def _write_refpts(self):
364
+ """Write reference points to a json file"""
365
+ log.debug("write refpts")
366
+ # Only necessary if the reference points picker is used as standalone tool
367
+ with open(self.refpts_file, "w") as f:
368
+ json.dump(self.refpts, f, indent=4)
369
+
370
+ def write_image(self):
371
+ """This is done via on-board resources of OpenCV"""
372
+ # Alternative: Own implementation using cv2.imwrite()
373
+ # Problem: Ctrl+S shortcut is occupied by this
374
+ pass
375
+
376
+ # ----------- Helper functions -----------
377
+
378
+ def _log_refpts(self):
379
+ """Helper for logging"""
380
+ log.debug("-------------------------")
381
+ log.debug("refpts:")
382
+ log.debug(self.refpts)
383
+ log.debug("-------------------------")
384
+ log.debug("historic refpts:")
385
+ log.debug(self.historic_refpts)
386
+ log.debug("-------------------------")
387
+
388
+
389
+ class DialogUTMCoordinates(Dialog):
390
+ """Modificatoin of tkinter.simpledialog.Dialog to get utm coordinates and zone and
391
+ hemisphere of sreference point
392
+ """
393
+
394
+ def __init__(self, try_again=False, **kwargs):
395
+ self.coords_utm = None
396
+ self.try_again = try_again
397
+ super().__init__(**kwargs)
398
+
399
+ def body(
400
+ self, master: tk.Frame
401
+ ): # sourcery skip: assign-if-exp, swap-if-expression
402
+ # Labels
403
+ if not self.try_again:
404
+ text_provide = "Provide reference point\nin UTM coordinates!"
405
+ else:
406
+ text_provide = "No valid UTM!\nProvide reference point\nin UTM coordinates!"
407
+ tk.Label(master, text=text_provide).grid(row=0, columnspan=3)
408
+ tk.Label(master, text="UTM zone:").grid(row=1, sticky="e")
409
+ tk.Label(master, text="Longitude (E):").grid(row=2, sticky="e")
410
+ tk.Label(master, text="Latitude (N):").grid(row=3, sticky="e")
411
+
412
+ # Zone
413
+ zones: list = list(range(1, 61))
414
+ self.combo_zone = ttk.Combobox(
415
+ master=master, values=zones, width=3, state="readonly"
416
+ )
417
+ self.combo_zone.set(32)
418
+ self.combo_zone.grid(row=1, column=1, sticky="ew")
419
+
420
+ # Hemisphere
421
+ hemispheres = ["N", "S"]
422
+ self.combo_hemisphere = ttk.Combobox(
423
+ master=master, values=hemispheres, width=2, state="readonly"
424
+ )
425
+ self.combo_hemisphere.set("N")
426
+ self.combo_hemisphere.grid(row=1, column=2, sticky="ew")
427
+
428
+ self.input_lon = tk.Entry(master, width=5)
429
+ self.input_lat = tk.Entry(master, width=5)
430
+ self.input_lon.grid(row=2, column=1, columnspan=2, sticky="ew")
431
+ self.input_lat.grid(row=3, column=1, columnspan=2, sticky="ew")
432
+
433
+ return self.combo_zone # initial focus
434
+
435
+ def apply(self):
436
+ self.zone = int(self.combo_zone.get())
437
+ self.hemisphere = self.combo_hemisphere.get()
438
+ self.lon = float(self.input_lon.get())
439
+ self.lat = float(self.input_lat.get())
440
+ self.coords_utm = {
441
+ "hemisphere": self.hemisphere,
442
+ "zone_utm": self.zone,
443
+ "lon_utm": self.lon,
444
+ "lat_utm": self.lat,
445
+ }
446
+ log.debug(self.coords_utm)
447
+
448
+
449
+ class NoPathError(Exception):
450
+ pass
451
+
452
+
453
+ class ImageWontOpenError(Exception):
454
+ pass
455
+
456
+
457
+ class VideoWontOpenError(Exception):
458
+ pass
459
+
460
+
461
+ class FrameNotAvailableError(Exception):
462
+ pass