mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0a1__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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +106 -7
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +2 -0
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +311 -261
- mapillary_tools/upload_api_v4.py +55 -95
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,174 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import collections
|
|
2
4
|
import datetime
|
|
3
|
-
import itertools
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
6
7
|
import typing as T
|
|
7
|
-
from multiprocessing import Pool
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
from tqdm import tqdm
|
|
11
11
|
|
|
12
|
-
from . import constants, exceptions, exif_write,
|
|
13
|
-
from .geotag import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
geotag_images_from_video,
|
|
20
|
-
geotag_videos_from_exiftool_video,
|
|
21
|
-
geotag_videos_from_video,
|
|
12
|
+
from . import constants, exceptions, exif_write, types, utils
|
|
13
|
+
from .geotag.factory import parse_source_option, process
|
|
14
|
+
from .geotag.options import (
|
|
15
|
+
InterpolationOption,
|
|
16
|
+
SourceOption,
|
|
17
|
+
SourcePathOption,
|
|
18
|
+
SourceType,
|
|
22
19
|
)
|
|
23
|
-
from .types import FileType, VideoMetadataOrError
|
|
24
|
-
|
|
25
|
-
from .video_data_extraction.cli_options import CliOptions, CliParserOptions
|
|
26
|
-
from .video_data_extraction.extract_video_data import VideoDataExtractor
|
|
27
|
-
|
|
28
20
|
|
|
29
21
|
LOG = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"gopro_videos", "blackvue_videos", "camm", "exif", "gpx", "nmea", "exiftool"
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
VideoGeotagSource = T.Literal[
|
|
37
|
-
"video",
|
|
38
|
-
"camm",
|
|
39
|
-
"gopro",
|
|
40
|
-
"blackvue",
|
|
41
|
-
"gpx",
|
|
42
|
-
"nmea",
|
|
43
|
-
"exiftool_xml",
|
|
44
|
-
"exiftool_runtime",
|
|
22
|
+
DEFAULT_GEOTAG_SOURCE_OPTIONS = [
|
|
23
|
+
SourceType.NATIVE.value,
|
|
24
|
+
SourceType.EXIFTOOL_RUNTIME.value,
|
|
45
25
|
]
|
|
46
26
|
|
|
47
27
|
|
|
48
|
-
def _process_images(
|
|
49
|
-
image_paths: T.Sequence[Path],
|
|
50
|
-
geotag_source: GeotagSource,
|
|
51
|
-
geotag_source_path: T.Optional[Path] = None,
|
|
52
|
-
video_import_path: T.Optional[Path] = None,
|
|
53
|
-
interpolation_use_gpx_start_time: bool = False,
|
|
54
|
-
interpolation_offset_time: float = 0.0,
|
|
55
|
-
num_processes: T.Optional[int] = None,
|
|
56
|
-
skip_subfolders=False,
|
|
57
|
-
) -> T.Sequence[types.ImageMetadataOrError]:
|
|
58
|
-
geotag: geotag_from_generic.GeotagImagesFromGeneric
|
|
59
|
-
|
|
60
|
-
if video_import_path is not None:
|
|
61
|
-
# commands that trigger this branch:
|
|
62
|
-
# video_process video_import_path image_paths --geotag_source gpx --geotag_source_path <gpx_file> --skip_subfolders
|
|
63
|
-
image_paths = list(
|
|
64
|
-
utils.filter_video_samples(
|
|
65
|
-
image_paths, video_import_path, skip_subfolders=skip_subfolders
|
|
66
|
-
)
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if geotag_source == "exif":
|
|
70
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
71
|
-
image_paths, num_processes=num_processes
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
else:
|
|
75
|
-
if geotag_source_path is None:
|
|
76
|
-
geotag_source_path = video_import_path
|
|
77
|
-
if geotag_source_path is None:
|
|
78
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
79
|
-
"Geotag source path (--geotag_source_path) is required"
|
|
80
|
-
)
|
|
81
|
-
if geotag_source == "exiftool":
|
|
82
|
-
if not geotag_source_path.exists():
|
|
83
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
84
|
-
f"Geotag source file not found: {geotag_source_path}"
|
|
85
|
-
)
|
|
86
|
-
else:
|
|
87
|
-
if not geotag_source_path.is_file():
|
|
88
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
89
|
-
f"Geotag source file not found: {geotag_source_path}"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
if geotag_source == "gpx":
|
|
93
|
-
geotag = geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
|
|
94
|
-
image_paths,
|
|
95
|
-
geotag_source_path,
|
|
96
|
-
use_gpx_start_time=interpolation_use_gpx_start_time,
|
|
97
|
-
offset_time=interpolation_offset_time,
|
|
98
|
-
num_processes=num_processes,
|
|
99
|
-
)
|
|
100
|
-
elif geotag_source == "nmea":
|
|
101
|
-
geotag = geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
|
|
102
|
-
image_paths,
|
|
103
|
-
geotag_source_path,
|
|
104
|
-
use_gpx_start_time=interpolation_use_gpx_start_time,
|
|
105
|
-
offset_time=interpolation_offset_time,
|
|
106
|
-
num_processes=num_processes,
|
|
107
|
-
)
|
|
108
|
-
elif geotag_source in ["gopro_videos", "blackvue_videos", "camm"]:
|
|
109
|
-
map_geotag_source_to_filetype: T.Dict[GeotagSource, FileType] = {
|
|
110
|
-
"gopro_videos": FileType.GOPRO,
|
|
111
|
-
"blackvue_videos": FileType.BLACKVUE,
|
|
112
|
-
"camm": FileType.CAMM,
|
|
113
|
-
}
|
|
114
|
-
video_paths = utils.find_videos([geotag_source_path])
|
|
115
|
-
image_samples_by_video_path = utils.find_all_image_samples(
|
|
116
|
-
image_paths, video_paths
|
|
117
|
-
)
|
|
118
|
-
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
119
|
-
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
120
|
-
video_paths_with_image_samples,
|
|
121
|
-
filetypes={map_geotag_source_to_filetype[geotag_source]},
|
|
122
|
-
num_processes=num_processes,
|
|
123
|
-
).to_description()
|
|
124
|
-
geotag = geotag_images_from_video.GeotagImagesFromVideo(
|
|
125
|
-
image_paths,
|
|
126
|
-
video_metadatas,
|
|
127
|
-
offset_time=interpolation_offset_time,
|
|
128
|
-
num_processes=num_processes,
|
|
129
|
-
)
|
|
130
|
-
elif geotag_source == "exiftool":
|
|
131
|
-
geotag = geotag_images_from_exiftool_both_image_and_video.GeotagImagesFromExifToolBothImageAndVideo(
|
|
132
|
-
image_paths,
|
|
133
|
-
geotag_source_path,
|
|
134
|
-
)
|
|
135
|
-
else:
|
|
136
|
-
raise RuntimeError(f"Invalid geotag source {geotag_source}")
|
|
137
|
-
|
|
138
|
-
return geotag.to_description()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _process_videos(
|
|
142
|
-
geotag_source: str,
|
|
143
|
-
geotag_source_path: T.Optional[Path],
|
|
144
|
-
video_paths: T.Sequence[Path],
|
|
145
|
-
num_processes: T.Optional[int],
|
|
146
|
-
filetypes: T.Optional[T.Set[FileType]],
|
|
147
|
-
) -> T.Sequence[VideoMetadataOrError]:
|
|
148
|
-
geotag: geotag_from_generic.GeotagVideosFromGeneric
|
|
149
|
-
if geotag_source == "exiftool":
|
|
150
|
-
if geotag_source_path is None:
|
|
151
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
152
|
-
"Geotag source path (--geotag_source_path) is required"
|
|
153
|
-
)
|
|
154
|
-
if not geotag_source_path.exists():
|
|
155
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
156
|
-
f"Geotag source file not found: {geotag_source_path}"
|
|
157
|
-
)
|
|
158
|
-
geotag = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
|
|
159
|
-
video_paths,
|
|
160
|
-
geotag_source_path,
|
|
161
|
-
num_processes=num_processes,
|
|
162
|
-
)
|
|
163
|
-
else:
|
|
164
|
-
geotag = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
165
|
-
video_paths,
|
|
166
|
-
filetypes=filetypes,
|
|
167
|
-
num_processes=num_processes,
|
|
168
|
-
)
|
|
169
|
-
return geotag.to_description()
|
|
170
|
-
|
|
171
|
-
|
|
172
28
|
def _normalize_import_paths(
|
|
173
29
|
import_path: T.Union[Path, T.Sequence[Path]],
|
|
174
30
|
) -> T.Sequence[Path]:
|
|
@@ -181,20 +37,57 @@ def _normalize_import_paths(
|
|
|
181
37
|
return import_paths
|
|
182
38
|
|
|
183
39
|
|
|
40
|
+
def _parse_source_options(
|
|
41
|
+
geotag_source: list[str],
|
|
42
|
+
video_geotag_source: list[str],
|
|
43
|
+
geotag_source_path: Path | None,
|
|
44
|
+
) -> list[SourceOption]:
|
|
45
|
+
parsed_options: list[SourceOption] = []
|
|
46
|
+
|
|
47
|
+
for s in geotag_source:
|
|
48
|
+
parsed_options.extend(parse_source_option(s))
|
|
49
|
+
|
|
50
|
+
for s in video_geotag_source:
|
|
51
|
+
for video_option in parse_source_option(s):
|
|
52
|
+
video_option.filetypes = types.combine_filetype_filters(
|
|
53
|
+
video_option.filetypes, {types.FileType.VIDEO}
|
|
54
|
+
)
|
|
55
|
+
parsed_options.append(video_option)
|
|
56
|
+
|
|
57
|
+
if geotag_source_path is not None:
|
|
58
|
+
for parsed_option in parsed_options:
|
|
59
|
+
if parsed_option.source_path is None:
|
|
60
|
+
parsed_option.source_path = SourcePathOption(
|
|
61
|
+
source_path=Path(geotag_source_path)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
source_path_option = parsed_option.source_path
|
|
65
|
+
if source_path_option.source_path is None:
|
|
66
|
+
source_path_option.source_path = Path(geotag_source_path)
|
|
67
|
+
else:
|
|
68
|
+
LOG.warning(
|
|
69
|
+
"The option --geotag_source_path is ignored for source %s",
|
|
70
|
+
parsed_option,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return parsed_options
|
|
74
|
+
|
|
75
|
+
|
|
184
76
|
def process_geotag_properties(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
geotag_source:
|
|
189
|
-
geotag_source_path:
|
|
77
|
+
import_path: Path | T.Sequence[Path],
|
|
78
|
+
filetypes: set[types.FileType] | None,
|
|
79
|
+
# Geotag options
|
|
80
|
+
geotag_source: list[str],
|
|
81
|
+
geotag_source_path: Path | None,
|
|
82
|
+
video_geotag_source: list[str],
|
|
83
|
+
# Global options
|
|
190
84
|
# video_import_path comes from the command video_process
|
|
191
|
-
video_import_path:
|
|
85
|
+
video_import_path: Path | None = None,
|
|
192
86
|
interpolation_use_gpx_start_time: bool = False,
|
|
193
87
|
interpolation_offset_time: float = 0.0,
|
|
88
|
+
num_processes: int | None = None,
|
|
194
89
|
skip_subfolders=False,
|
|
195
|
-
|
|
196
|
-
) -> T.List[types.MetadataOrError]:
|
|
197
|
-
filetypes = set(FileType(f) for f in filetypes)
|
|
90
|
+
) -> list[types.MetadataOrError]:
|
|
198
91
|
import_paths = _normalize_import_paths(import_path)
|
|
199
92
|
|
|
200
93
|
# Check and fail early
|
|
@@ -204,84 +97,34 @@ def process_geotag_properties(
|
|
|
204
97
|
f"Import file or directory not found: {path}"
|
|
205
98
|
)
|
|
206
99
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if FileType.IMAGE in filetypes:
|
|
210
|
-
image_paths = utils.find_images(import_paths, skip_subfolders=skip_subfolders)
|
|
211
|
-
if image_paths:
|
|
212
|
-
image_metadatas = _process_images(
|
|
213
|
-
image_paths,
|
|
214
|
-
geotag_source=geotag_source,
|
|
215
|
-
geotag_source_path=geotag_source_path,
|
|
216
|
-
video_import_path=video_import_path,
|
|
217
|
-
interpolation_use_gpx_start_time=interpolation_use_gpx_start_time,
|
|
218
|
-
interpolation_offset_time=interpolation_offset_time,
|
|
219
|
-
num_processes=num_processes,
|
|
220
|
-
skip_subfolders=skip_subfolders,
|
|
221
|
-
)
|
|
222
|
-
metadatas.extend(image_metadatas)
|
|
100
|
+
if geotag_source_path is None:
|
|
101
|
+
geotag_source_path = video_import_path
|
|
223
102
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
metadatas.extend(_process_videos_beta(vars_args))
|
|
227
|
-
else:
|
|
228
|
-
if (
|
|
229
|
-
FileType.CAMM in filetypes
|
|
230
|
-
or FileType.GOPRO in filetypes
|
|
231
|
-
or FileType.BLACKVUE in filetypes
|
|
232
|
-
or FileType.VIDEO in filetypes
|
|
233
|
-
):
|
|
234
|
-
video_paths = utils.find_videos(
|
|
235
|
-
import_paths, skip_subfolders=skip_subfolders
|
|
236
|
-
)
|
|
237
|
-
if video_paths:
|
|
238
|
-
video_metadata = _process_videos(
|
|
239
|
-
geotag_source,
|
|
240
|
-
geotag_source_path,
|
|
241
|
-
video_paths,
|
|
242
|
-
num_processes,
|
|
243
|
-
filetypes,
|
|
244
|
-
)
|
|
245
|
-
metadatas.extend(video_metadata)
|
|
103
|
+
if not geotag_source and not video_geotag_source:
|
|
104
|
+
geotag_source = [*DEFAULT_GEOTAG_SOURCE_OPTIONS]
|
|
246
105
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
106
|
+
options = _parse_source_options(
|
|
107
|
+
geotag_source=geotag_source or [],
|
|
108
|
+
video_geotag_source=video_geotag_source or [],
|
|
109
|
+
geotag_source_path=geotag_source_path,
|
|
250
110
|
)
|
|
251
111
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
parsed_opts = json.loads(source)
|
|
262
|
-
except ValueError:
|
|
263
|
-
if source not in T.get_args(VideoGeotagSource):
|
|
264
|
-
raise exceptions.MapillaryBadParameterError(
|
|
265
|
-
"Unknown beta source %s or invalid JSON", source
|
|
266
|
-
)
|
|
267
|
-
parsed_opts = {"source": source}
|
|
112
|
+
for option in options:
|
|
113
|
+
option.filetypes = types.combine_filetype_filters(option.filetypes, filetypes)
|
|
114
|
+
option.num_processes = num_processes
|
|
115
|
+
if option.interpolation is None:
|
|
116
|
+
option.interpolation = InterpolationOption(
|
|
117
|
+
offset_time=interpolation_offset_time,
|
|
118
|
+
use_gpx_start_time=interpolation_use_gpx_start_time,
|
|
119
|
+
)
|
|
268
120
|
|
|
269
|
-
|
|
270
|
-
|
|
121
|
+
# TODO: can find both in one pass
|
|
122
|
+
image_paths = utils.find_images(import_paths, skip_subfolders=skip_subfolders)
|
|
123
|
+
video_paths = utils.find_videos(import_paths, skip_subfolders=skip_subfolders)
|
|
271
124
|
|
|
272
|
-
|
|
125
|
+
metadata_or_errors = process(image_paths + video_paths, options)
|
|
273
126
|
|
|
274
|
-
|
|
275
|
-
"paths": vars_args["import_path"],
|
|
276
|
-
"recursive": vars_args["skip_subfolders"] is False,
|
|
277
|
-
"geotag_sources_options": geotag_sources_opts,
|
|
278
|
-
"geotag_source_path": vars_args["geotag_source_path"],
|
|
279
|
-
"num_processes": vars_args["num_processes"],
|
|
280
|
-
"device_make": vars_args["device_make"],
|
|
281
|
-
"device_model": vars_args["device_model"],
|
|
282
|
-
}
|
|
283
|
-
extractor = VideoDataExtractor(options)
|
|
284
|
-
return extractor.process()
|
|
127
|
+
return metadata_or_errors
|
|
285
128
|
|
|
286
129
|
|
|
287
130
|
def _apply_offsets(
|
|
@@ -324,7 +167,7 @@ def _overwrite_exif_tags(
|
|
|
324
167
|
unit="images",
|
|
325
168
|
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
326
169
|
):
|
|
327
|
-
dt = datetime.datetime.
|
|
170
|
+
dt = datetime.datetime.fromtimestamp(metadata.time, datetime.timezone.utc)
|
|
328
171
|
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
329
172
|
|
|
330
173
|
try:
|
|
@@ -368,9 +211,7 @@ def _write_metadatas(
|
|
|
368
211
|
LOG.info("Check the description file for details: %s", desc_path)
|
|
369
212
|
|
|
370
213
|
|
|
371
|
-
def _is_error_skipped(
|
|
372
|
-
error_type: str, skipped_process_errors: T.Set[T.Type[Exception]]
|
|
373
|
-
):
|
|
214
|
+
def _is_error_skipped(error_type: str, skipped_process_errors: set[T.Type[Exception]]):
|
|
374
215
|
skipped_process_error_names = set(err.__name__ for err in skipped_process_errors)
|
|
375
216
|
skip_all = Exception in skipped_process_errors
|
|
376
217
|
return skip_all or error_type in skipped_process_error_names
|
|
@@ -380,15 +221,13 @@ def _show_stats(
|
|
|
380
221
|
metadatas: T.Sequence[types.MetadataOrError],
|
|
381
222
|
skipped_process_errors: T.Set[T.Type[Exception]],
|
|
382
223
|
) -> None:
|
|
383
|
-
metadatas_by_filetype:
|
|
224
|
+
metadatas_by_filetype: dict[types.FileType, list[types.MetadataOrError]] = {}
|
|
384
225
|
for metadata in metadatas:
|
|
385
|
-
filetype: T.Optional[FileType]
|
|
386
226
|
if isinstance(metadata, types.ImageMetadata):
|
|
387
|
-
filetype = FileType.IMAGE
|
|
227
|
+
filetype = types.FileType.IMAGE
|
|
388
228
|
else:
|
|
389
229
|
filetype = metadata.filetype
|
|
390
|
-
|
|
391
|
-
metadatas_by_filetype.setdefault(FileType(filetype), []).append(metadata)
|
|
230
|
+
metadatas_by_filetype.setdefault(filetype, []).append(metadata)
|
|
392
231
|
|
|
393
232
|
for filetype, group in metadatas_by_filetype.items():
|
|
394
233
|
_show_stats_per_filetype(group, filetype, skipped_process_errors)
|
|
@@ -408,19 +247,16 @@ def _show_stats(
|
|
|
408
247
|
|
|
409
248
|
|
|
410
249
|
def _show_stats_per_filetype(
|
|
411
|
-
metadatas: T.
|
|
412
|
-
filetype: FileType,
|
|
250
|
+
metadatas: T.Collection[types.MetadataOrError],
|
|
251
|
+
filetype: types.FileType,
|
|
413
252
|
skipped_process_errors: T.Set[T.Type[Exception]],
|
|
414
253
|
):
|
|
415
|
-
good_metadatas:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if
|
|
420
|
-
|
|
421
|
-
else:
|
|
422
|
-
good_metadatas.append(metadata)
|
|
423
|
-
filesize_to_upload += metadata.filesize or 0
|
|
254
|
+
good_metadatas: list[types.Metadata]
|
|
255
|
+
good_metadatas, error_metadatas = types.separate_errors(metadatas)
|
|
256
|
+
|
|
257
|
+
filesize_to_upload = sum(
|
|
258
|
+
[0 if m.filesize is None else m.filesize for m in good_metadatas]
|
|
259
|
+
)
|
|
424
260
|
|
|
425
261
|
LOG.info("%8d %s(s) read in total", len(metadatas), filetype.value)
|
|
426
262
|
if good_metadatas:
|
|
@@ -446,119 +282,43 @@ def _show_stats_per_filetype(
|
|
|
446
282
|
)
|
|
447
283
|
|
|
448
284
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
it: T.Iterable[_IT], sep: T.Callable[[_IT], bool]
|
|
454
|
-
) -> T.Tuple[T.List[_IT], T.List[_IT]]:
|
|
455
|
-
yes, no = [], []
|
|
456
|
-
for e in it:
|
|
457
|
-
if sep(e):
|
|
458
|
-
yes.append(e)
|
|
459
|
-
else:
|
|
460
|
-
no.append(e)
|
|
461
|
-
return yes, no
|
|
285
|
+
def _validate_metadatas(
|
|
286
|
+
metadatas: T.Collection[types.MetadataOrError], num_processes: int | None
|
|
287
|
+
) -> list[types.MetadataOrError]:
|
|
288
|
+
LOG.debug("Validating %d metadatas", len(metadatas))
|
|
462
289
|
|
|
290
|
+
# validating metadatas is slow, hence multiprocessing
|
|
463
291
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
292
|
+
# Do not pass error metadatas where the error object can not be pickled for multiprocessing to work
|
|
293
|
+
# Otherwise we get:
|
|
294
|
+
# TypeError: __init__() missing 3 required positional arguments: 'image_time', 'gpx_start_time', and 'gpx_end_time'
|
|
295
|
+
# See https://stackoverflow.com/a/61432070
|
|
296
|
+
good_metadatas, error_metadatas = types.separate_errors(metadatas)
|
|
297
|
+
map_results = utils.mp_map_maybe(
|
|
298
|
+
types.validate_and_fail_metadata,
|
|
299
|
+
T.cast(T.Iterable[types.Metadata], good_metadatas),
|
|
300
|
+
num_processes=num_processes,
|
|
473
301
|
)
|
|
474
|
-
uploaded_sequence_uuids = set()
|
|
475
|
-
for sequence_uuid, group in groups.items():
|
|
476
|
-
for m in group:
|
|
477
|
-
m.update_md5sum()
|
|
478
|
-
sequence_md5sum = types.sequence_md5sum(group)
|
|
479
|
-
if history.is_uploaded(sequence_md5sum):
|
|
480
|
-
uploaded_sequence_uuids.add(sequence_uuid)
|
|
481
|
-
|
|
482
|
-
output: T.List[types.MetadataOrError] = []
|
|
483
|
-
for metadata in metadatas:
|
|
484
|
-
if isinstance(metadata, types.ImageMetadata):
|
|
485
|
-
if metadata.MAPSequenceUUID in uploaded_sequence_uuids:
|
|
486
|
-
output.append(
|
|
487
|
-
types.describe_error_metadata(
|
|
488
|
-
exceptions.MapillaryUploadedAlreadyError(
|
|
489
|
-
"The image was already uploaded",
|
|
490
|
-
types.as_desc(metadata),
|
|
491
|
-
),
|
|
492
|
-
filename=metadata.filename,
|
|
493
|
-
filetype=types.FileType.IMAGE,
|
|
494
|
-
)
|
|
495
|
-
)
|
|
496
|
-
else:
|
|
497
|
-
output.append(metadata)
|
|
498
|
-
elif isinstance(metadata, types.VideoMetadata):
|
|
499
|
-
metadata.update_md5sum()
|
|
500
|
-
assert isinstance(metadata.md5sum, str)
|
|
501
|
-
if history.is_uploaded(metadata.md5sum):
|
|
502
|
-
output.append(
|
|
503
|
-
types.describe_error_metadata(
|
|
504
|
-
exceptions.MapillaryUploadedAlreadyError(
|
|
505
|
-
"The video was already uploaded",
|
|
506
|
-
types.as_desc(metadata),
|
|
507
|
-
),
|
|
508
|
-
filename=metadata.filename,
|
|
509
|
-
filetype=metadata.filetype,
|
|
510
|
-
)
|
|
511
|
-
)
|
|
512
|
-
else:
|
|
513
|
-
output.append(metadata)
|
|
514
|
-
else:
|
|
515
|
-
output.append(metadata)
|
|
516
|
-
assert len(output) == len(metadatas), "length mismatch"
|
|
517
|
-
return output
|
|
518
302
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
disable_multiprocessing = False
|
|
527
|
-
else:
|
|
528
|
-
pool_num_processes = max(num_processes, 1)
|
|
529
|
-
disable_multiprocessing = num_processes <= 0
|
|
530
|
-
with Pool(processes=pool_num_processes) as pool:
|
|
531
|
-
validated_metadatas_iter: T.Iterator[types.MetadataOrError]
|
|
532
|
-
if disable_multiprocessing:
|
|
533
|
-
validated_metadatas_iter = map(types.validate_and_fail_metadata, metadatas)
|
|
534
|
-
else:
|
|
535
|
-
# Do not pass error metadatas where the error object can not be pickled for multiprocessing to work
|
|
536
|
-
# Otherwise we get:
|
|
537
|
-
# TypeError: __init__() missing 3 required positional arguments: 'image_time', 'gpx_start_time', and 'gpx_end_time'
|
|
538
|
-
# See https://stackoverflow.com/a/61432070
|
|
539
|
-
yes, no = split_if(metadatas, lambda m: isinstance(m, types.ErrorMetadata))
|
|
540
|
-
no_iter = pool.imap(
|
|
541
|
-
types.validate_and_fail_metadata,
|
|
542
|
-
no,
|
|
543
|
-
)
|
|
544
|
-
validated_metadatas_iter = itertools.chain(yes, no_iter)
|
|
545
|
-
return list(
|
|
546
|
-
tqdm(
|
|
547
|
-
validated_metadatas_iter,
|
|
548
|
-
desc="Validating metadatas",
|
|
549
|
-
unit="metadata",
|
|
550
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
551
|
-
total=len(metadatas),
|
|
552
|
-
)
|
|
303
|
+
validated_metadatas = list(
|
|
304
|
+
tqdm(
|
|
305
|
+
map_results,
|
|
306
|
+
desc="Validating metadatas",
|
|
307
|
+
unit="metadata",
|
|
308
|
+
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
309
|
+
total=len(good_metadatas),
|
|
553
310
|
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return validated_metadatas + error_metadatas
|
|
554
314
|
|
|
555
315
|
|
|
556
316
|
def process_finalize(
|
|
557
317
|
import_path: T.Union[T.Sequence[Path], Path],
|
|
558
|
-
metadatas:
|
|
318
|
+
metadatas: list[types.MetadataOrError],
|
|
559
319
|
skip_process_errors: bool = False,
|
|
560
|
-
device_make:
|
|
561
|
-
device_model:
|
|
320
|
+
device_make: str | None = None,
|
|
321
|
+
device_model: str | None = None,
|
|
562
322
|
overwrite_all_EXIF_tags: bool = False,
|
|
563
323
|
overwrite_EXIF_time_tag: bool = False,
|
|
564
324
|
overwrite_EXIF_gps_tag: bool = False,
|
|
@@ -566,40 +326,48 @@ def process_finalize(
|
|
|
566
326
|
overwrite_EXIF_orientation_tag: bool = False,
|
|
567
327
|
offset_time: float = 0.0,
|
|
568
328
|
offset_angle: float = 0.0,
|
|
569
|
-
desc_path:
|
|
570
|
-
num_processes:
|
|
571
|
-
) ->
|
|
329
|
+
desc_path: str | None = None,
|
|
330
|
+
num_processes: int | None = None,
|
|
331
|
+
) -> list[types.MetadataOrError]:
|
|
332
|
+
image_metadatas: list[types.ImageMetadata] = []
|
|
333
|
+
video_metadatas: list[types.VideoMetadata] = []
|
|
334
|
+
|
|
572
335
|
for metadata in metadatas:
|
|
573
336
|
if isinstance(metadata, types.VideoMetadata):
|
|
574
|
-
|
|
575
|
-
metadata.make = device_make
|
|
576
|
-
if device_model is not None:
|
|
577
|
-
metadata.model = device_model
|
|
337
|
+
video_metadatas.append(metadata)
|
|
578
338
|
elif isinstance(metadata, types.ImageMetadata):
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
339
|
+
image_metadatas.append(metadata)
|
|
340
|
+
|
|
341
|
+
for metadata in video_metadatas:
|
|
342
|
+
if device_make is not None:
|
|
343
|
+
metadata.make = device_make
|
|
344
|
+
if device_model is not None:
|
|
345
|
+
metadata.model = device_model
|
|
346
|
+
|
|
347
|
+
for metadata in image_metadatas:
|
|
348
|
+
if device_make is not None:
|
|
349
|
+
metadata.MAPDeviceMake = device_make
|
|
350
|
+
if device_model is not None:
|
|
351
|
+
metadata.MAPDeviceModel = device_model
|
|
352
|
+
# Add the basename
|
|
353
|
+
metadata.MAPFilename = metadata.filename.name
|
|
583
354
|
|
|
584
355
|
# modified in place
|
|
585
356
|
_apply_offsets(
|
|
586
|
-
|
|
587
|
-
metadata
|
|
588
|
-
for metadata in metadatas
|
|
589
|
-
if isinstance(metadata, types.ImageMetadata)
|
|
590
|
-
],
|
|
357
|
+
image_metadatas,
|
|
591
358
|
offset_time=offset_time,
|
|
592
359
|
offset_angle=offset_angle,
|
|
593
360
|
)
|
|
594
361
|
|
|
595
|
-
|
|
596
|
-
metadatas = _validate_metadatas(metadatas, num_processes)
|
|
362
|
+
metadatas = _validate_metadatas(metadatas, num_processes=num_processes)
|
|
597
363
|
|
|
598
|
-
|
|
599
|
-
|
|
364
|
+
# image_metadatas and video_metadatas get stale after the validation,
|
|
365
|
+
# hence delete them to avoid confusion
|
|
366
|
+
del image_metadatas
|
|
367
|
+
del video_metadatas
|
|
600
368
|
|
|
601
369
|
_overwrite_exif_tags(
|
|
602
|
-
#
|
|
370
|
+
# Search image metadatas again because some of them might have been failed
|
|
603
371
|
[
|
|
604
372
|
metadata
|
|
605
373
|
for metadata in metadatas
|
|
@@ -637,16 +405,13 @@ def process_finalize(
|
|
|
637
405
|
# write descs first because _show_stats() may raise an exception
|
|
638
406
|
_write_metadatas(metadatas, desc_path)
|
|
639
407
|
|
|
640
|
-
#
|
|
408
|
+
# Show stats
|
|
641
409
|
skipped_process_errors: T.Set[T.Type[Exception]]
|
|
642
410
|
if skip_process_errors:
|
|
643
|
-
#
|
|
411
|
+
# Skip all exceptions
|
|
644
412
|
skipped_process_errors = {Exception}
|
|
645
413
|
else:
|
|
646
|
-
skipped_process_errors = {
|
|
647
|
-
exceptions.MapillaryDuplicationError,
|
|
648
|
-
exceptions.MapillaryUploadedAlreadyError,
|
|
649
|
-
}
|
|
414
|
+
skipped_process_errors = {exceptions.MapillaryDuplicationError}
|
|
650
415
|
_show_stats(metadatas, skipped_process_errors=skipped_process_errors)
|
|
651
416
|
|
|
652
417
|
return metadatas
|