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
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OTVision helpers for filehandling
|
|
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 bz2
|
|
23
|
+
import logging
|
|
24
|
+
import shutil
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Iterable, Union
|
|
28
|
+
|
|
29
|
+
import ijson
|
|
30
|
+
import ujson
|
|
31
|
+
|
|
32
|
+
from OTVision import dataformat
|
|
33
|
+
from OTVision.config import CONFIG
|
|
34
|
+
from OTVision.dataformat import INPUT_FILE_PATH, METADATA
|
|
35
|
+
from OTVision.helpers.log import LOGGER_NAME
|
|
36
|
+
|
|
37
|
+
log = logging.getLogger(LOGGER_NAME)
|
|
38
|
+
|
|
39
|
+
ENCODING = "UTF-8"
|
|
40
|
+
COMPRESSED_FILETYPE = ".bz2"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
START_DATE = "start_date"
|
|
44
|
+
FILE_NAME_PATTERN = r".*(?P<start_date>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}).*"
|
|
45
|
+
HOSTNAME = "hostname"
|
|
46
|
+
FULL_FILE_NAME_PATTERN = (
|
|
47
|
+
r"(?P<hostname>.*)_(?P<start_date>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}).*"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_files(
|
|
52
|
+
paths: list[Path],
|
|
53
|
+
filetypes: Union[list[str], None] = None,
|
|
54
|
+
search_subdirs: bool = True,
|
|
55
|
+
) -> list[Path]:
|
|
56
|
+
"""
|
|
57
|
+
Generates a list of files ending with filename based on filenames or the
|
|
58
|
+
(recursive) content of folders.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
paths (list[Path]): where to find the files.
|
|
62
|
+
filetype (list[str]): ending of files to find. Preceding "_" prevents adding a
|
|
63
|
+
'.'
|
|
64
|
+
If no filetype is given, filetypes of file paths given are used and
|
|
65
|
+
directories are ignored. Defaults to None.
|
|
66
|
+
search_subdirs (bool): Wheter or not to search subdirs of dirs given as paths.
|
|
67
|
+
Defaults to True.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
TypeError: If type of paths is not list
|
|
71
|
+
TypeError: If type of path in paths is not Path or subclass
|
|
72
|
+
TypeError: If type of filetypes is not list
|
|
73
|
+
TypeError: If type of filetype in filetypes is not str
|
|
74
|
+
TypeError: If path in paths is neither valid file nor dir
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
list[Path]: List of files
|
|
78
|
+
"""
|
|
79
|
+
files = set()
|
|
80
|
+
if type(paths) is not list:
|
|
81
|
+
raise TypeError("Paths needs to be a list of pathlib.Path")
|
|
82
|
+
if filetypes:
|
|
83
|
+
if type(filetypes) is not list:
|
|
84
|
+
raise TypeError("Filetypes needs to be a list of str")
|
|
85
|
+
for idx, filetype in enumerate(filetypes):
|
|
86
|
+
if type(filetype) is not str:
|
|
87
|
+
raise TypeError("Filetypes needs to be a list of str")
|
|
88
|
+
if not filetype.startswith("_"):
|
|
89
|
+
if not filetype.startswith("."):
|
|
90
|
+
filetype = f".{filetype}"
|
|
91
|
+
filetypes[idx] = filetype.lower()
|
|
92
|
+
for path in paths:
|
|
93
|
+
if not isinstance(path, Path):
|
|
94
|
+
raise TypeError("Paths needs to be a list of pathlib.Path")
|
|
95
|
+
if path.is_file():
|
|
96
|
+
file = path
|
|
97
|
+
if filetypes:
|
|
98
|
+
for filetype in filetypes:
|
|
99
|
+
if path.suffix.lower() == filetype:
|
|
100
|
+
files.add(path)
|
|
101
|
+
else:
|
|
102
|
+
files.add(path)
|
|
103
|
+
elif path.is_dir():
|
|
104
|
+
if filetypes:
|
|
105
|
+
for filetype in filetypes:
|
|
106
|
+
for file in path.glob("**/*" if search_subdirs else "*"):
|
|
107
|
+
if file.is_file() and file.suffix.lower() == filetype:
|
|
108
|
+
files.add(file)
|
|
109
|
+
else:
|
|
110
|
+
raise TypeError("Paths needs to be a list of pathlib.Path")
|
|
111
|
+
|
|
112
|
+
return sorted(list(files))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def replace_filetype(
|
|
116
|
+
files: list[Path], new_filetype: str, old_filetype: Union[str, None] = None
|
|
117
|
+
) -> list[Path]:
|
|
118
|
+
"""In a list of files, replace the filetype of all files of a certain old_filetype
|
|
119
|
+
by a new_filetype. If no old_filetype is given, replace tha filetype of all files.
|
|
120
|
+
Directories remain unchanged in the new list.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
files (list[Path]): List of paths (can be files or directories).
|
|
124
|
+
new_filetype (str): New file type after replacement.
|
|
125
|
+
old_filetype (str): File type to be replaced. If None, filetypes of all files
|
|
126
|
+
will be replaced.
|
|
127
|
+
Defaults to None.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
TypeError: If files is not a list of pathlib.Path
|
|
131
|
+
TypeError: If one of the files is not a file (but, for example, a dir)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
list[Path]: List of paths with file type replaced
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
if type(files) is not list:
|
|
138
|
+
raise TypeError("Paths needs to be a list of pathlib.Path")
|
|
139
|
+
new_paths = []
|
|
140
|
+
for path in files:
|
|
141
|
+
if not isinstance(path, Path):
|
|
142
|
+
raise TypeError("Paths needs to be a list of pathlib.Path")
|
|
143
|
+
if path.is_file():
|
|
144
|
+
if old_filetype and path.suffix.lower() != old_filetype.lower():
|
|
145
|
+
continue
|
|
146
|
+
new_path = path.with_suffix(new_filetype)
|
|
147
|
+
new_paths.append(new_path)
|
|
148
|
+
elif path.is_dir():
|
|
149
|
+
raise TypeError("files has to be a list of files without dirs")
|
|
150
|
+
else:
|
|
151
|
+
raise TypeError("files has to be a list of existing files")
|
|
152
|
+
|
|
153
|
+
return new_paths
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def check_if_all_paths_exist(paths: list[Path]) -> None:
|
|
157
|
+
for path in paths:
|
|
158
|
+
if not path.expanduser().resolve().exists():
|
|
159
|
+
raise FileNotFoundError(f"{path} is not an existing file or directory")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _remove_dir(dir_to_remove: Path) -> None:
|
|
163
|
+
"""Helper to remove a directory and all of its subdirectories.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
dir_to_remove (Path): directory to remove
|
|
167
|
+
"""
|
|
168
|
+
for path in dir_to_remove.glob("*"):
|
|
169
|
+
if path.is_file():
|
|
170
|
+
path.unlink()
|
|
171
|
+
else:
|
|
172
|
+
_remove_dir(path)
|
|
173
|
+
dir_to_remove.rmdir()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def read_json_bz2_event_stream(path: Path) -> Iterable[tuple[str, str, str]]:
|
|
177
|
+
"""
|
|
178
|
+
Provide lazy data stream reading the bzip2 compressed file
|
|
179
|
+
at the given path and interpreting it as json objects.
|
|
180
|
+
"""
|
|
181
|
+
# TODO error handling
|
|
182
|
+
stream = bz2.BZ2File(path)
|
|
183
|
+
return ijson.parse(stream)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def metadata_from_json_events(parse_events: Iterable[tuple[str, str, str]]) -> dict:
|
|
187
|
+
"""
|
|
188
|
+
Extract the metadata block of the ottrk data format
|
|
189
|
+
from the given json parser event stream.
|
|
190
|
+
"""
|
|
191
|
+
result: dict
|
|
192
|
+
for data in ijson.items(parse_events, METADATA):
|
|
193
|
+
result = data
|
|
194
|
+
break
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def read_json_bz2_metadata(path: Path) -> dict:
|
|
199
|
+
try:
|
|
200
|
+
return metadata_from_json_events(read_json_bz2_event_stream(path))
|
|
201
|
+
except EOFError as cause:
|
|
202
|
+
log.exception(f'Unable to read "{path}" as JSON.', exc_info=cause)
|
|
203
|
+
raise cause
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def read_json(
|
|
207
|
+
json_file: Path,
|
|
208
|
+
filetype: str = ".json",
|
|
209
|
+
decompress: bool = True,
|
|
210
|
+
) -> dict:
|
|
211
|
+
"""Read a json file of a specific filetype to a dict.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
json_file (Path): json file to read
|
|
215
|
+
filetype (str, optional): filetype to check json file against.
|
|
216
|
+
Defaults to ".json".
|
|
217
|
+
decompress: (bool, optional): decompress output with bzip2.
|
|
218
|
+
If `filetype` is not `.bz2`, decompress will be set to False.
|
|
219
|
+
Defaults to True.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
TypeError: If file is not pathlib.Path
|
|
223
|
+
ValueError: If file is not of filetype given
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict: dict read from json file
|
|
227
|
+
"""
|
|
228
|
+
if not isinstance(json_file, Path):
|
|
229
|
+
raise TypeError("json_file has to be of type pathlib.Path")
|
|
230
|
+
filetype = json_file.suffix
|
|
231
|
+
if json_file.suffix != filetype:
|
|
232
|
+
raise ValueError(f"Wrong filetype {str(json_file)}, has to be {filetype}")
|
|
233
|
+
try:
|
|
234
|
+
t_json_start = time.perf_counter()
|
|
235
|
+
if decompress:
|
|
236
|
+
log.debug(f"Read and decompress {json_file}")
|
|
237
|
+
with bz2.open(json_file, "rt", encoding=ENCODING) as input:
|
|
238
|
+
dict_from_json_file = ujson.load(input)
|
|
239
|
+
else:
|
|
240
|
+
log.debug(f"Read {json_file} withoud decompression")
|
|
241
|
+
with open(json_file, "r", encoding=ENCODING) as input:
|
|
242
|
+
dict_from_json_file = ujson.load(input)
|
|
243
|
+
log.debug(f"Succesfully read {json_file}")
|
|
244
|
+
t_json_end = time.perf_counter()
|
|
245
|
+
log.debug(f"Reading {json_file} took: {t_json_end - t_json_start:0.4f}s")
|
|
246
|
+
return dict_from_json_file
|
|
247
|
+
except OSError as cause:
|
|
248
|
+
log.exception(f"Could not open {json_file}", exc_info=cause)
|
|
249
|
+
raise cause
|
|
250
|
+
except ujson.JSONDecodeError as cause:
|
|
251
|
+
log.exception(f'Unable to decode "{json_file}" as JSON.', exc_info=cause)
|
|
252
|
+
raise cause
|
|
253
|
+
except Exception as cause:
|
|
254
|
+
log.exception("", exc_info=cause)
|
|
255
|
+
raise cause
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# TODO: Type hint nested dict during refactoring
|
|
259
|
+
def write_json(
|
|
260
|
+
dict_to_write: dict,
|
|
261
|
+
file: Path,
|
|
262
|
+
filetype: str = ".json",
|
|
263
|
+
overwrite: bool = False,
|
|
264
|
+
compress: bool = True,
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Write a json file from a dict to a specific filetype.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
dict_to_write (dict): dict to write
|
|
270
|
+
file (Path): file path. Can have other filetype, which will be overwritten.
|
|
271
|
+
filetype (str, optional): filetype of file to be written.
|
|
272
|
+
Defaults to ".json".
|
|
273
|
+
overwrite (bool, optional): Whether or not to overwrite an existing file.
|
|
274
|
+
Defaults to False.
|
|
275
|
+
compress: (bool, optional): compress input with bzip2.
|
|
276
|
+
If `filetype` is not `.bz2`, compress will be set to False.
|
|
277
|
+
Defaults to True.
|
|
278
|
+
"""
|
|
279
|
+
outfile = Path(file).with_suffix(filetype)
|
|
280
|
+
outfile_already_exists = outfile.is_file()
|
|
281
|
+
if overwrite or not outfile_already_exists:
|
|
282
|
+
t_json_start = time.perf_counter()
|
|
283
|
+
if compress:
|
|
284
|
+
log.debug(f"Compress and write {outfile}")
|
|
285
|
+
with bz2.open(outfile, "wt", encoding=ENCODING) as output:
|
|
286
|
+
ujson.dump(dict_to_write, output)
|
|
287
|
+
else:
|
|
288
|
+
log.debug(f"Write {outfile} without compression")
|
|
289
|
+
with open(outfile, "w", encoding=ENCODING) as output:
|
|
290
|
+
ujson.dump(dict_to_write, output)
|
|
291
|
+
t_json_end = time.perf_counter()
|
|
292
|
+
|
|
293
|
+
if not outfile_already_exists:
|
|
294
|
+
log.debug(f"Successfully wrote {outfile}")
|
|
295
|
+
else:
|
|
296
|
+
log.debug(f"Successfully overwrote {outfile}")
|
|
297
|
+
|
|
298
|
+
log.debug(f"Writing {outfile} took: {t_json_end - t_json_start:0.4f}s")
|
|
299
|
+
else:
|
|
300
|
+
log.debug(f"{outfile} already exists, not overwritten. Set overwrite=True")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# TODO: Type hint nested dict during refactoring
|
|
304
|
+
def get_metadata(otdict: dict) -> dict:
|
|
305
|
+
"""Check if dict of detections or tracks has subdict metadata.
|
|
306
|
+
If not, try to convert from historic format.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
otdict (dict): dict of detections or tracks
|
|
310
|
+
"""
|
|
311
|
+
if dataformat.METADATA in otdict:
|
|
312
|
+
return otdict[dataformat.METADATA]
|
|
313
|
+
try:
|
|
314
|
+
metadata = {}
|
|
315
|
+
if "vid_config" in otdict:
|
|
316
|
+
metadata[dataformat.VIDEO] = otdict["vid_config"]
|
|
317
|
+
if "det_config" in otdict:
|
|
318
|
+
metadata[dataformat.DETECTION] = otdict["det_config"]
|
|
319
|
+
if "trk_config" in otdict:
|
|
320
|
+
metadata[dataformat.TRACKING] = otdict["trk_config"]
|
|
321
|
+
log.info("new metadata created from historic information")
|
|
322
|
+
return metadata
|
|
323
|
+
|
|
324
|
+
except Exception:
|
|
325
|
+
log.exception("Metadata not found and not in historic config format")
|
|
326
|
+
raise
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# TODO: Type hint nested dict during refactoring
|
|
330
|
+
def denormalize_bbox(
|
|
331
|
+
otdict: dict,
|
|
332
|
+
keys_width: Union[list[str], None] = None,
|
|
333
|
+
keys_height: Union[list[str], None] = None,
|
|
334
|
+
metadata: dict[str, dict] = {},
|
|
335
|
+
) -> dict:
|
|
336
|
+
"""Denormalize all bbox references in detections or tracks dict from percent to px.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
otdict (dict): dict of detections or tracks
|
|
340
|
+
keys_width (list[str], optional): list of keys describing horizontal position.
|
|
341
|
+
Defaults to ["x", "w"].
|
|
342
|
+
keys_height (list[str], optional): list of keys describing vertical position.
|
|
343
|
+
Defaults to ["y", "h"].
|
|
344
|
+
metadata (dict[str, dict]): dict of metadata per input file.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
_type_: Denormalized dict.
|
|
348
|
+
"""
|
|
349
|
+
if keys_width is None:
|
|
350
|
+
keys_width = [dataformat.X, dataformat.W]
|
|
351
|
+
if keys_height is None:
|
|
352
|
+
keys_height = [dataformat.Y, dataformat.H]
|
|
353
|
+
log.debug("Denormalize frame wise")
|
|
354
|
+
otdict = _denormalize_transformation(otdict, keys_width, keys_height, metadata)
|
|
355
|
+
return otdict
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# TODO: Type hint nested dict during refactoring
|
|
359
|
+
def _denormalize_transformation(
|
|
360
|
+
otdict: dict,
|
|
361
|
+
keys_width: list[str],
|
|
362
|
+
keys_height: list[str],
|
|
363
|
+
metadata: dict[str, dict] = {},
|
|
364
|
+
) -> dict:
|
|
365
|
+
"""Helper to do the actual denormalization.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
otdict (dict): dict of detections or tracks
|
|
369
|
+
keys_width (list[str]): list of keys describing horizontal position.
|
|
370
|
+
Defaults to ["x", "w"].
|
|
371
|
+
keys_height (list[str]): list of keys describing vertical position.
|
|
372
|
+
Defaults to ["y", "h"].
|
|
373
|
+
metadata (dict[str, dict]): dict of metadata per input file.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
dict: denormalized dict
|
|
377
|
+
"""
|
|
378
|
+
changed_files = set()
|
|
379
|
+
|
|
380
|
+
for frame in otdict[dataformat.DATA].values():
|
|
381
|
+
input_file = frame[INPUT_FILE_PATH]
|
|
382
|
+
metadate = metadata[input_file]
|
|
383
|
+
width = metadate[dataformat.VIDEO][dataformat.WIDTH]
|
|
384
|
+
height = metadate[dataformat.VIDEO][dataformat.HEIGHT]
|
|
385
|
+
is_normalized = metadate[dataformat.DETECTION][dataformat.NORMALIZED_BBOX]
|
|
386
|
+
if is_normalized:
|
|
387
|
+
changed_files.add(input_file)
|
|
388
|
+
for bbox in frame[dataformat.DETECTIONS]:
|
|
389
|
+
for key in bbox:
|
|
390
|
+
if key in keys_width:
|
|
391
|
+
bbox[key] = bbox[key] * width
|
|
392
|
+
elif key in keys_height:
|
|
393
|
+
bbox[key] = bbox[key] * height
|
|
394
|
+
|
|
395
|
+
for file in changed_files:
|
|
396
|
+
metadata[file][dataformat.DETECTION][dataformat.NORMALIZED_BBOX] = False
|
|
397
|
+
return otdict
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
# TODO: Type hint nested dict during refactoring
|
|
401
|
+
def normalize_bbox(
|
|
402
|
+
otdict: dict,
|
|
403
|
+
keys_width: Union[list[str], None] = None,
|
|
404
|
+
keys_height: Union[list[str], None] = None,
|
|
405
|
+
metadata: dict[str, dict] = {},
|
|
406
|
+
) -> dict:
|
|
407
|
+
"""Normalize all bbox references in detections or tracks dict from percent to px.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
otdict (dict): dict of detections or tracks
|
|
411
|
+
keys_width (list[str], optional): list of keys describing horizontal position.
|
|
412
|
+
Defaults to ["x", "w"].
|
|
413
|
+
keys_height (list[str], optional): list of keys describing vertical position.
|
|
414
|
+
Defaults to ["y", "h"].
|
|
415
|
+
metadata (dict[str, dict]): dict of metadata per input file.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
_type_: Normalized dict.
|
|
419
|
+
"""
|
|
420
|
+
if keys_width is None:
|
|
421
|
+
keys_width = [dataformat.X, dataformat.W]
|
|
422
|
+
if keys_height is None:
|
|
423
|
+
keys_height = [dataformat.Y, dataformat.H]
|
|
424
|
+
if not otdict[dataformat.METADATA][dataformat.NORMALIZED_BBOX]:
|
|
425
|
+
otdict = _normalize_transformation(
|
|
426
|
+
otdict,
|
|
427
|
+
keys_width,
|
|
428
|
+
keys_height,
|
|
429
|
+
metadata,
|
|
430
|
+
)
|
|
431
|
+
log.debug("Dict normalized")
|
|
432
|
+
else:
|
|
433
|
+
log.debug("Dict was already normalized")
|
|
434
|
+
return otdict
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# TODO: Type hint nested dict during refactoring
|
|
438
|
+
def _normalize_transformation(
|
|
439
|
+
otdict: dict,
|
|
440
|
+
keys_width: list[str],
|
|
441
|
+
keys_height: list[str],
|
|
442
|
+
metadata: dict[str, dict] = {},
|
|
443
|
+
) -> dict:
|
|
444
|
+
"""Helper to do the actual normalization.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
otdict (dict): dict of detections or tracks
|
|
448
|
+
keys_width (list[str]): list of keys describing horizontal position.
|
|
449
|
+
Defaults to ["x", "w"].
|
|
450
|
+
keys_height (list[str]): list of keys describing vertical position.
|
|
451
|
+
Defaults to ["y", "h"].
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
dict: Normalized dict
|
|
456
|
+
"""
|
|
457
|
+
changed_files = set()
|
|
458
|
+
|
|
459
|
+
for frame in otdict[dataformat.DATA].values():
|
|
460
|
+
input_file = frame[INPUT_FILE_PATH]
|
|
461
|
+
metadate = metadata[input_file]
|
|
462
|
+
width = metadate[dataformat.VIDEO][dataformat.WIDTH]
|
|
463
|
+
height = metadate[dataformat.VIDEO][dataformat.HEIGHT]
|
|
464
|
+
is_denormalized = not metadate[dataformat.NORMALIZED_BBOX]
|
|
465
|
+
if is_denormalized:
|
|
466
|
+
changed_files.add(input_file)
|
|
467
|
+
for bbox in frame[dataformat.DETECTIONS]:
|
|
468
|
+
for key in bbox:
|
|
469
|
+
if key in keys_width:
|
|
470
|
+
bbox[key] = bbox[key] / width
|
|
471
|
+
elif key in keys_height:
|
|
472
|
+
bbox[key] = bbox[key] / height
|
|
473
|
+
|
|
474
|
+
for file in changed_files:
|
|
475
|
+
metadata[file][dataformat.NORMALIZED_BBOX] = True
|
|
476
|
+
return otdict
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def has_filetype(file: Path, filetypes: list[str]) -> bool:
|
|
480
|
+
"""Checks if a file has a specified filetype.
|
|
481
|
+
|
|
482
|
+
The case of a filetype is ignored.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
file (Path): The path to the file
|
|
486
|
+
file_formats(list(str)): The valid filetypes
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
True if file is of filetype specified in filetypes.
|
|
490
|
+
Otherwise False.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
return file.suffix.lower() in [
|
|
494
|
+
filetype.lower() if filetype.startswith(".") else f".{filetype.lower()}"
|
|
495
|
+
for filetype in filetypes
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def is_video(file: Path) -> bool:
|
|
500
|
+
"""Checks if a file is a video according to its filetype
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
file (Path): file to check
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
bool: whether or not the file is a video
|
|
507
|
+
"""
|
|
508
|
+
return file.suffix.lower() in CONFIG["FILETYPES"]["VID"]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def is_image(file: Path) -> bool:
|
|
512
|
+
"""Checks if a file is an image according to its filetype
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
file (Path): file to check
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
bool: whether or not the file is an image
|
|
519
|
+
"""
|
|
520
|
+
return file.suffix.lower() in CONFIG["FILETYPES"]["IMG"]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def unzip(file: Path) -> Path:
|
|
524
|
+
"""Unpack a zip archive to a directory of same name.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
file (Path): zip to unpack
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Path: unzipped directory
|
|
531
|
+
"""
|
|
532
|
+
directory = file.with_suffix("")
|
|
533
|
+
shutil.unpack_archive(file, directory)
|
|
534
|
+
return directory
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class InproperFormattedFilename(Exception):
|
|
538
|
+
pass
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OTVision helpers to change formats and retrieve information
|
|
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
|
+
|
|
23
|
+
import datetime as dt
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_fps_from_filename(filename: str) -> int:
|
|
31
|
+
"""Get frame rate from file name using regex.
|
|
32
|
+
Returns None if frame rate is not found in file name.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
input_filename (str): file name
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
int or None: frame rate in frames per second or None
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
if match := re.search(r"_FR([\d]+)_", filename):
|
|
42
|
+
return int(match[1])
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Cannot read frame rate from file name {filename}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_datetime_from_filename(
|
|
48
|
+
filename: str, epoch_datetime: str = "1970-01-01_00-00-00"
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Get date and time from file name.
|
|
51
|
+
Searches for "_yyyy-mm-dd_hh-mm-ss".
|
|
52
|
+
Returns "yyyy-mm-dd_hh-mm-ss".
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
filename (str): filename with expression
|
|
56
|
+
epoch_datetime (str): Unix epoch (00:00:00 on 1 January 1970)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: datetime
|
|
60
|
+
"""
|
|
61
|
+
regex = "_([0-9]{4,4}-[0-9]{2,2}-[0-9]{2,2}_[0-9]{2,2}-[0-9]{2,2}-[0-9]{2,2})"
|
|
62
|
+
match = re.search(regex, filename)
|
|
63
|
+
if not match:
|
|
64
|
+
return epoch_datetime
|
|
65
|
+
|
|
66
|
+
# Assume that there is only one timestamp in the file name
|
|
67
|
+
datetime_str = match[1]
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
dt.datetime.strptime(datetime_str, "%Y-%m-%d_%H-%M-%S")
|
|
71
|
+
except ValueError:
|
|
72
|
+
return epoch_datetime
|
|
73
|
+
|
|
74
|
+
return datetime_str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# TODO: Type hint nested dict during refactoring
|
|
78
|
+
def _ottrk_detections_to_df(ottrk: list) -> pd.DataFrame:
|
|
79
|
+
"""Turns a list of tracked detections into a dataframe
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
ottrk (list): List of dicts, each dict is a tracked detection.
|
|
83
|
+
Comes from from .ottrk file.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
pd.DataFrame: DataFrame of tracks
|
|
87
|
+
"""
|
|
88
|
+
return pd.DataFrame(ottrk)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _get_time_from_frame_number(
|
|
92
|
+
frame_series: pd.Series,
|
|
93
|
+
start_datetime: str,
|
|
94
|
+
fps: int,
|
|
95
|
+
return_yyyymmdd_hhmmss: bool = True,
|
|
96
|
+
return_milliseconds: bool = True,
|
|
97
|
+
) -> pd.Series | tuple[pd.Series, pd.Series]:
|
|
98
|
+
"""Get datetime series of detections from series of frame numbers of video
|
|
99
|
+
the objects were detected using a start datetime of the video and
|
|
100
|
+
the video frame rate (fps).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
frame_series (pd.Series): Series of video frames of detections
|
|
104
|
+
start_datetime (str): Start datetime of video
|
|
105
|
+
fps (int): Video frame rate in frames per second
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
pd.Series: Datetime series of detections in "%Y-%m-%d_%H-%M-%S" format
|
|
109
|
+
pd.Series: Datetime series of detections in milliseconds
|
|
110
|
+
"""
|
|
111
|
+
datetime_yyyymmdd_hhmmss = pd.to_datetime(
|
|
112
|
+
start_datetime, format=r"%Y-%m-%d_%H-%M-%S"
|
|
113
|
+
) + pd.to_timedelta((frame_series.astype("int32") - 1) / fps, unit="s")
|
|
114
|
+
if return_milliseconds:
|
|
115
|
+
datetime_milliseconds = datetime_yyyymmdd_hhmmss.astype(np.int64) / int(1e6)
|
|
116
|
+
if return_yyyymmdd_hhmmss and return_milliseconds:
|
|
117
|
+
return datetime_yyyymmdd_hhmmss, datetime_milliseconds
|
|
118
|
+
elif not return_milliseconds:
|
|
119
|
+
return datetime_yyyymmdd_hhmmss
|
|
120
|
+
else:
|
|
121
|
+
return datetime_milliseconds
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_epsg_from_utm_zone(utm_zone: int, hemisphere: str) -> int:
|
|
125
|
+
"""Calculates the epsg number from utm zone and hemisphere.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
utm_zone (int): UTM zone (1-60)
|
|
129
|
+
hemisphere (str): Hemisphere ("N" or "S")
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
int: epsg number of UTM zone (e.g. 32632)
|
|
133
|
+
"""
|
|
134
|
+
identifier_digits = 32000
|
|
135
|
+
if hemisphere == "N":
|
|
136
|
+
hemisphere_digit = 600
|
|
137
|
+
elif hemisphere == "S":
|
|
138
|
+
hemisphere_digit = 700
|
|
139
|
+
return identifier_digits + hemisphere_digit + utm_zone
|