mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.1__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 +66 -262
- mapillary_tools/authenticate.py +54 -46
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools/commands/upload.py +33 -4
- mapillary_tools/config.py +38 -17
- mapillary_tools/constants.py +127 -43
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +2 -1
- mapillary_tools/exif_write.py +3 -1
- mapillary_tools/exiftool_read_video.py +52 -15
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +406 -232
- mapillary_tools/geo.py +16 -0
- mapillary_tools/geotag/__init__.py +0 -0
- mapillary_tools/geotag/base.py +8 -4
- mapillary_tools/geotag/factory.py +106 -89
- mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
- mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
- mapillary_tools/geotag/geotag_images_from_video.py +35 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
- mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
- mapillary_tools/geotag/options.py +25 -3
- mapillary_tools/geotag/utils.py +9 -12
- mapillary_tools/geotag/video_extractors/base.py +1 -1
- mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
- mapillary_tools/geotag/video_extractors/gpx.py +61 -70
- mapillary_tools/geotag/video_extractors/native.py +34 -31
- mapillary_tools/history.py +128 -8
- mapillary_tools/http.py +211 -0
- mapillary_tools/mp4/construct_mp4_parser.py +8 -2
- mapillary_tools/process_geotag_properties.py +47 -35
- mapillary_tools/process_sequence_properties.py +340 -325
- mapillary_tools/sample_video.py +8 -8
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/types.py +44 -610
- mapillary_tools/upload.py +327 -352
- mapillary_tools/upload_api_v4.py +125 -72
- mapillary_tools/uploader.py +797 -216
- mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
- mapillary_tools-0.14.1.dist-info/RECORD +76 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
- mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
mapillary_tools/geo.py
CHANGED
|
@@ -51,6 +51,22 @@ def gps_distance(latlon_1: tuple[float, float], latlon_2: tuple[float, float]) -
|
|
|
51
51
|
return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def avg_speed(sequence: T.Sequence[PointLike]) -> float:
|
|
55
|
+
total_distance = 0.0
|
|
56
|
+
for cur, nxt in pairwise(sequence):
|
|
57
|
+
total_distance += gps_distance((cur.lat, cur.lon), (nxt.lat, nxt.lon))
|
|
58
|
+
|
|
59
|
+
if sequence:
|
|
60
|
+
time_diff = sequence[-1].time - sequence[0].time
|
|
61
|
+
else:
|
|
62
|
+
time_diff = 0.0
|
|
63
|
+
|
|
64
|
+
if time_diff == 0.0:
|
|
65
|
+
return float("inf")
|
|
66
|
+
|
|
67
|
+
return total_distance / time_diff
|
|
68
|
+
|
|
69
|
+
|
|
54
70
|
def compute_bearing(
|
|
55
71
|
latlon_1: tuple[float, float],
|
|
56
72
|
latlon_2: tuple[float, float],
|
|
File without changes
|
mapillary_tools/geotag/base.py
CHANGED
|
@@ -46,12 +46,12 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
46
46
|
map_results,
|
|
47
47
|
desc="Extracting images",
|
|
48
48
|
unit="images",
|
|
49
|
-
disable=LOG.
|
|
49
|
+
disable=LOG.isEnabledFor(logging.DEBUG),
|
|
50
50
|
total=len(extractors),
|
|
51
51
|
)
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
return results + error_metadatas
|
|
54
|
+
return T.cast(list[types.ImageMetadataOrError], results + error_metadatas)
|
|
55
55
|
|
|
56
56
|
# This method is passed to multiprocessing
|
|
57
57
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
@@ -62,6 +62,8 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
62
62
|
try:
|
|
63
63
|
return extractor.extract()
|
|
64
64
|
except exceptions.MapillaryDescriptionError as ex:
|
|
65
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
66
|
+
LOG.error(f"{cls.__name__}({image_path.name}): {ex}")
|
|
65
67
|
return types.describe_error_metadata(
|
|
66
68
|
ex, image_path, filetype=types.FileType.IMAGE
|
|
67
69
|
)
|
|
@@ -112,12 +114,12 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
112
114
|
map_results,
|
|
113
115
|
desc="Extracting videos",
|
|
114
116
|
unit="videos",
|
|
115
|
-
disable=LOG.
|
|
117
|
+
disable=LOG.isEnabledFor(logging.DEBUG),
|
|
116
118
|
total=len(extractors),
|
|
117
119
|
)
|
|
118
120
|
)
|
|
119
121
|
|
|
120
|
-
return results + error_metadatas
|
|
122
|
+
return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
|
|
121
123
|
|
|
122
124
|
# This method is passed to multiprocessing
|
|
123
125
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
@@ -128,6 +130,8 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
128
130
|
try:
|
|
129
131
|
return extractor.extract()
|
|
130
132
|
except exceptions.MapillaryDescriptionError as ex:
|
|
133
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
134
|
+
LOG.error(f"{cls.__name__}({video_path.name}): {ex}")
|
|
131
135
|
return types.describe_error_metadata(
|
|
132
136
|
ex, video_path, filetype=types.FileType.VIDEO
|
|
133
137
|
)
|
|
@@ -6,7 +6,6 @@ import typing as T
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from .. import exceptions, types, utils
|
|
9
|
-
from ..types import FileType
|
|
10
9
|
from . import (
|
|
11
10
|
base,
|
|
12
11
|
geotag_images_from_exif,
|
|
@@ -68,10 +67,34 @@ def process(
|
|
|
68
67
|
reprocessable_paths = set(paths)
|
|
69
68
|
|
|
70
69
|
for idx, option in enumerate(options):
|
|
71
|
-
LOG.
|
|
70
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
71
|
+
LOG.info(
|
|
72
|
+
f"==> Processing {len(reprocessable_paths)} files with source {option}..."
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
LOG.info(
|
|
76
|
+
f"==> Processing {len(reprocessable_paths)} files with source {option.source.value}..."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
image_videos, video_paths = _filter_images_and_videos(
|
|
80
|
+
reprocessable_paths, option.filetypes
|
|
81
|
+
)
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
if image_videos:
|
|
84
|
+
image_geotag = _build_image_geotag(option)
|
|
85
|
+
image_metadata_or_errors = (
|
|
86
|
+
image_geotag.to_description(image_videos) if image_geotag else []
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
image_metadata_or_errors = []
|
|
90
|
+
|
|
91
|
+
if video_paths:
|
|
92
|
+
video_geotag = _build_video_geotag(option)
|
|
93
|
+
video_metadata_or_errors = (
|
|
94
|
+
video_geotag.to_description(video_paths) if video_geotag else []
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
video_metadata_or_errors = []
|
|
75
98
|
|
|
76
99
|
more_option = idx < len(options) - 1
|
|
77
100
|
|
|
@@ -97,6 +120,8 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
|
|
|
97
120
|
(
|
|
98
121
|
exceptions.MapillaryGeoTaggingError,
|
|
99
122
|
exceptions.MapillaryVideoGPSNotFoundError,
|
|
123
|
+
exceptions.MapillaryExiftoolNotFoundError,
|
|
124
|
+
exceptions.MapillaryExifToolXMLNotFoundError,
|
|
100
125
|
),
|
|
101
126
|
):
|
|
102
127
|
return True
|
|
@@ -140,152 +165,144 @@ def _ensure_source_path(option: SourceOption) -> Path:
|
|
|
140
165
|
return option.source_path.source_path
|
|
141
166
|
|
|
142
167
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if not image_paths:
|
|
149
|
-
return []
|
|
150
|
-
|
|
168
|
+
def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
|
|
169
|
+
"""
|
|
170
|
+
Build a GeotagImagesFromGeneric object based on the provided option.
|
|
171
|
+
"""
|
|
151
172
|
if option.interpolation is None:
|
|
152
173
|
interpolation = InterpolationOption()
|
|
153
174
|
else:
|
|
154
175
|
interpolation = option.interpolation
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if option.source is SourceType.NATIVE:
|
|
159
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
177
|
+
if option.source in [SourceType.EXIF, SourceType.NATIVE]:
|
|
178
|
+
return geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
160
179
|
num_processes=option.num_processes
|
|
161
180
|
)
|
|
162
|
-
return geotag.to_description(image_paths)
|
|
163
181
|
|
|
164
182
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
165
|
-
|
|
183
|
+
return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
|
|
166
184
|
num_processes=option.num_processes
|
|
167
185
|
)
|
|
168
|
-
try:
|
|
169
|
-
return geotag.to_description(image_paths)
|
|
170
|
-
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
171
|
-
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
172
|
-
return []
|
|
173
186
|
|
|
174
187
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
175
188
|
# This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
|
|
176
189
|
# to work
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
if option.source_path is None:
|
|
191
|
+
raise exceptions.MapillaryBadParameterError(
|
|
192
|
+
"source_path must be provided for EXIFTOOL_XML source"
|
|
193
|
+
)
|
|
194
|
+
return geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
|
|
195
|
+
source_path=option.source_path,
|
|
179
196
|
num_processes=option.num_processes,
|
|
180
197
|
)
|
|
181
|
-
return geotag.to_description(image_paths)
|
|
182
198
|
|
|
183
199
|
elif option.source is SourceType.GPX:
|
|
184
|
-
|
|
200
|
+
return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
|
|
185
201
|
source_path=_ensure_source_path(option),
|
|
186
202
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
187
203
|
offset_time=interpolation.offset_time,
|
|
188
204
|
num_processes=option.num_processes,
|
|
189
205
|
)
|
|
190
|
-
return geotag.to_description(image_paths)
|
|
191
206
|
|
|
192
207
|
elif option.source is SourceType.NMEA:
|
|
193
|
-
|
|
208
|
+
return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
|
|
194
209
|
source_path=_ensure_source_path(option),
|
|
195
210
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
196
211
|
offset_time=interpolation.offset_time,
|
|
197
212
|
num_processes=option.num_processes,
|
|
198
213
|
)
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
204
|
-
num_processes=option.num_processes
|
|
205
|
-
)
|
|
206
|
-
return geotag.to_description(image_paths)
|
|
207
|
-
|
|
208
|
-
elif option.source in [
|
|
209
|
-
SourceType.GOPRO,
|
|
210
|
-
SourceType.BLACKVUE,
|
|
211
|
-
SourceType.CAMM,
|
|
212
|
-
]:
|
|
213
|
-
map_geotag_source_to_filetype: dict[SourceType, FileType] = {
|
|
214
|
-
SourceType.GOPRO: FileType.GOPRO,
|
|
215
|
-
SourceType.BLACKVUE: FileType.BLACKVUE,
|
|
216
|
-
SourceType.CAMM: FileType.CAMM,
|
|
217
|
-
}
|
|
218
|
-
video_paths = utils.find_videos([_ensure_source_path(option)])
|
|
219
|
-
image_samples_by_video_path = utils.find_all_image_samples(
|
|
220
|
-
image_paths, video_paths
|
|
221
|
-
)
|
|
222
|
-
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
223
|
-
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
224
|
-
filetypes={map_geotag_source_to_filetype[option.source]},
|
|
225
|
-
num_processes=option.num_processes,
|
|
226
|
-
).to_description(video_paths_with_image_samples)
|
|
227
|
-
geotag = geotag_images_from_video.GeotagImagesFromVideo(
|
|
228
|
-
video_metadatas,
|
|
215
|
+
elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
|
|
216
|
+
return geotag_images_from_video.GeotagImageSamplesFromVideo(
|
|
217
|
+
_ensure_source_path(option),
|
|
229
218
|
offset_time=interpolation.offset_time,
|
|
230
219
|
num_processes=option.num_processes,
|
|
231
220
|
)
|
|
232
|
-
return geotag.to_description(image_paths)
|
|
233
221
|
|
|
234
222
|
else:
|
|
235
223
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
236
224
|
|
|
237
225
|
|
|
238
|
-
def
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
_, video_paths = _filter_images_and_videos(paths, option.filetypes)
|
|
242
|
-
|
|
243
|
-
if not video_paths:
|
|
244
|
-
return []
|
|
245
|
-
|
|
246
|
-
geotag: base.GeotagVideosFromGeneric
|
|
226
|
+
def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
|
|
227
|
+
"""
|
|
228
|
+
Build a GeotagVideosFromGeneric object based on the provided option.
|
|
247
229
|
|
|
230
|
+
Examples:
|
|
231
|
+
>>> from pathlib import Path
|
|
232
|
+
>>> from mapillary_tools.geotag.options import SourceOption, SourceType
|
|
233
|
+
>>> opt = SourceOption(SourceType.NATIVE)
|
|
234
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
235
|
+
>>> geotagger.__class__.__name__
|
|
236
|
+
'GeotagVideosFromVideo'
|
|
237
|
+
|
|
238
|
+
>>> opt = SourceOption(SourceType.EXIFTOOL_RUNTIME)
|
|
239
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
240
|
+
>>> geotagger.__class__.__name__
|
|
241
|
+
'GeotagVideosFromExifToolRunner'
|
|
242
|
+
|
|
243
|
+
>>> opt = SourceOption(SourceType.EXIFTOOL_XML, source_path=Path("/tmp/test.xml"))
|
|
244
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
245
|
+
>>> geotagger.__class__.__name__
|
|
246
|
+
'GeotagVideosFromExifToolXML'
|
|
247
|
+
|
|
248
|
+
>>> opt = SourceOption(SourceType.GPX, source_path=Path("/tmp/test.gpx"))
|
|
249
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
250
|
+
>>> geotagger.__class__.__name__
|
|
251
|
+
'GeotagVideosFromGPX'
|
|
252
|
+
|
|
253
|
+
>>> opt = SourceOption(SourceType.NMEA, source_path=Path("/tmp/test.nmea"))
|
|
254
|
+
>>> _build_video_geotag(opt) is None
|
|
255
|
+
True
|
|
256
|
+
|
|
257
|
+
>>> opt = SourceOption(SourceType.EXIF)
|
|
258
|
+
>>> _build_video_geotag(opt) is None
|
|
259
|
+
True
|
|
260
|
+
|
|
261
|
+
>>> opt = SourceOption(SourceType.GOPRO)
|
|
262
|
+
>>> _build_video_geotag(opt) is None
|
|
263
|
+
True
|
|
264
|
+
|
|
265
|
+
>>> try:
|
|
266
|
+
... _build_video_geotag(SourceOption("invalid"))
|
|
267
|
+
... except ValueError as e:
|
|
268
|
+
... "Invalid geotag source" in str(e)
|
|
269
|
+
True
|
|
270
|
+
"""
|
|
248
271
|
if option.source is SourceType.NATIVE:
|
|
249
|
-
|
|
272
|
+
return geotag_videos_from_video.GeotagVideosFromVideo(
|
|
250
273
|
num_processes=option.num_processes, filetypes=option.filetypes
|
|
251
274
|
)
|
|
252
|
-
return geotag.to_description(video_paths)
|
|
253
275
|
|
|
254
276
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
255
|
-
|
|
277
|
+
return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
|
|
256
278
|
num_processes=option.num_processes
|
|
257
279
|
)
|
|
258
|
-
try:
|
|
259
|
-
return geotag.to_description(video_paths)
|
|
260
|
-
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
261
|
-
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
262
|
-
return []
|
|
263
280
|
|
|
264
281
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
265
|
-
|
|
266
|
-
|
|
282
|
+
if option.source_path is None:
|
|
283
|
+
raise exceptions.MapillaryBadParameterError(
|
|
284
|
+
"source_path must be provided for EXIFTOOL_XML source"
|
|
285
|
+
)
|
|
286
|
+
return geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
|
|
287
|
+
source_path=option.source_path,
|
|
267
288
|
)
|
|
268
|
-
return geotag.to_description(video_paths)
|
|
269
289
|
|
|
270
290
|
elif option.source is SourceType.GPX:
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
return geotag_videos_from_gpx.GeotagVideosFromGPX(
|
|
292
|
+
source_path=option.source_path, num_processes=option.num_processes
|
|
293
|
+
)
|
|
273
294
|
|
|
274
295
|
elif option.source is SourceType.NMEA:
|
|
275
296
|
# TODO: geotag videos from NMEA
|
|
276
|
-
return
|
|
297
|
+
return None
|
|
277
298
|
|
|
278
299
|
elif option.source is SourceType.EXIF:
|
|
279
300
|
# Legacy image-specific geotag types
|
|
280
|
-
return
|
|
301
|
+
return None
|
|
281
302
|
|
|
282
|
-
elif option.source in [
|
|
283
|
-
SourceType.GOPRO,
|
|
284
|
-
SourceType.BLACKVUE,
|
|
285
|
-
SourceType.CAMM,
|
|
286
|
-
]:
|
|
303
|
+
elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
|
|
287
304
|
# Legacy image-specific geotag types
|
|
288
|
-
return
|
|
305
|
+
return None
|
|
289
306
|
|
|
290
307
|
else:
|
|
291
308
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
@@ -13,29 +13,25 @@ else:
|
|
|
13
13
|
|
|
14
14
|
from .. import constants, exceptions, exiftool_read, types, utils
|
|
15
15
|
from ..exiftool_runner import ExiftoolRunner
|
|
16
|
+
from . import options
|
|
16
17
|
from .base import GeotagImagesFromGeneric
|
|
17
18
|
from .geotag_images_from_video import GeotagImagesFromVideo
|
|
18
19
|
from .geotag_videos_from_exiftool import GeotagVideosFromExifToolXML
|
|
19
20
|
from .image_extractors.exiftool import ImageExifToolExtractor
|
|
20
|
-
from .utils import index_rdf_description_by_path
|
|
21
21
|
|
|
22
22
|
LOG = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
|
|
26
26
|
def __init__(
|
|
27
|
-
self,
|
|
28
|
-
xml_path: Path,
|
|
29
|
-
num_processes: int | None = None,
|
|
27
|
+
self, source_path: options.SourcePathOption, num_processes: int | None = None
|
|
30
28
|
):
|
|
31
|
-
self.
|
|
29
|
+
self.source_path = source_path
|
|
32
30
|
super().__init__(num_processes=num_processes)
|
|
33
31
|
|
|
34
32
|
@classmethod
|
|
35
33
|
def build_image_extractors(
|
|
36
|
-
cls,
|
|
37
|
-
rdf_by_path: dict[str, ET.Element],
|
|
38
|
-
image_paths: T.Iterable[Path],
|
|
34
|
+
cls, rdf_by_path: dict[str, ET.Element], image_paths: T.Iterable[Path]
|
|
39
35
|
) -> list[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
40
36
|
results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
|
|
41
37
|
|
|
@@ -59,7 +55,9 @@ class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
|
|
|
59
55
|
def _generate_image_extractors(
|
|
60
56
|
self, image_paths: T.Sequence[Path]
|
|
61
57
|
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
62
|
-
rdf_by_path =
|
|
58
|
+
rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
|
|
59
|
+
self.source_path, image_paths
|
|
60
|
+
)
|
|
63
61
|
return self.build_image_extractors(rdf_by_path, image_paths)
|
|
64
62
|
|
|
65
63
|
|
|
@@ -68,7 +66,10 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
|
68
66
|
def _generate_image_extractors(
|
|
69
67
|
self, image_paths: T.Sequence[Path]
|
|
70
68
|
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
71
|
-
|
|
69
|
+
if constants.EXIFTOOL_PATH is None:
|
|
70
|
+
runner = ExiftoolRunner()
|
|
71
|
+
else:
|
|
72
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
72
73
|
|
|
73
74
|
LOG.debug(
|
|
74
75
|
"Extracting XML from %d images with ExifTool command: %s",
|
|
@@ -78,7 +79,13 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
|
78
79
|
try:
|
|
79
80
|
xml = runner.extract_xml(image_paths)
|
|
80
81
|
except FileNotFoundError as ex:
|
|
81
|
-
|
|
82
|
+
exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
|
|
83
|
+
return [
|
|
84
|
+
types.describe_error_metadata(
|
|
85
|
+
exiftool_ex, image_path, filetype=types.FileType.IMAGE
|
|
86
|
+
)
|
|
87
|
+
for image_path in image_paths
|
|
88
|
+
]
|
|
82
89
|
|
|
83
90
|
try:
|
|
84
91
|
xml_element = ET.fromstring(xml)
|
|
@@ -86,7 +93,7 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
|
86
93
|
LOG.warning(
|
|
87
94
|
"Failed to parse ExifTool XML: %s",
|
|
88
95
|
str(ex),
|
|
89
|
-
exc_info=LOG.
|
|
96
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
90
97
|
)
|
|
91
98
|
rdf_by_path = {}
|
|
92
99
|
else:
|
|
@@ -102,29 +109,30 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
|
102
109
|
class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
|
|
103
110
|
def __init__(
|
|
104
111
|
self,
|
|
105
|
-
|
|
112
|
+
source_path: options.SourcePathOption,
|
|
106
113
|
offset_time: float = 0.0,
|
|
107
114
|
num_processes: int | None = None,
|
|
108
115
|
):
|
|
109
116
|
super().__init__(num_processes=num_processes)
|
|
110
|
-
self.
|
|
117
|
+
self.source_path = source_path
|
|
111
118
|
self.offset_time = offset_time
|
|
112
119
|
|
|
113
120
|
def geotag_samples(
|
|
114
121
|
self, image_paths: T.Sequence[Path]
|
|
115
122
|
) -> list[types.ImageMetadataOrError]:
|
|
116
123
|
# Find all video paths in self.xml_path
|
|
117
|
-
rdf_by_path =
|
|
124
|
+
rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
|
|
125
|
+
self.source_path, image_paths
|
|
126
|
+
)
|
|
118
127
|
video_paths = utils.find_videos(
|
|
119
|
-
[Path(
|
|
128
|
+
[Path(canonical_path) for canonical_path in rdf_by_path.keys()],
|
|
120
129
|
skip_subfolders=True,
|
|
121
130
|
)
|
|
122
131
|
# Find all video paths that have sample images
|
|
123
132
|
samples_by_video = utils.find_all_image_samples(image_paths, video_paths)
|
|
124
133
|
|
|
125
134
|
video_metadata_or_errors = GeotagVideosFromExifToolXML(
|
|
126
|
-
self.
|
|
127
|
-
num_processes=self.num_processes,
|
|
135
|
+
self.source_path, num_processes=self.num_processes
|
|
128
136
|
).to_description(list(samples_by_video.keys()))
|
|
129
137
|
sample_paths = sum(samples_by_video.values(), [])
|
|
130
138
|
sample_metadata_or_errors = GeotagImagesFromVideo(
|
|
@@ -146,8 +154,7 @@ class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
|
|
|
146
154
|
non_sample_paths = [path for path in image_paths if path not in sample_paths]
|
|
147
155
|
|
|
148
156
|
non_sample_metadata_or_errors = GeotagImagesFromExifToolXML(
|
|
149
|
-
self.
|
|
150
|
-
num_processes=self.num_processes,
|
|
157
|
+
self.source_path, num_processes=self.num_processes
|
|
151
158
|
).to_description(non_sample_paths)
|
|
152
159
|
|
|
153
160
|
return sample_metadata_or_errors + non_sample_metadata_or_errors
|
|
@@ -12,6 +12,7 @@ else:
|
|
|
12
12
|
from typing_extensions import override
|
|
13
13
|
|
|
14
14
|
from .. import exceptions, geo, types
|
|
15
|
+
from ..serializer.description import build_capture_time
|
|
15
16
|
from .base import GeotagImagesFromGeneric
|
|
16
17
|
from .geotag_images_from_exif import ImageEXIFExtractor
|
|
17
18
|
|
|
@@ -43,26 +44,26 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
43
44
|
|
|
44
45
|
if image_metadata.time < sorted_points[0].time:
|
|
45
46
|
delta = sorted_points[0].time - image_metadata.time
|
|
46
|
-
gpx_start_time =
|
|
47
|
-
gpx_end_time =
|
|
47
|
+
gpx_start_time = build_capture_time(sorted_points[0].time)
|
|
48
|
+
gpx_end_time = build_capture_time(sorted_points[-1].time)
|
|
48
49
|
# with the tolerance of 1ms
|
|
49
50
|
if 0.001 < delta:
|
|
50
51
|
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
51
52
|
f"The image date time is {round(delta, 3)} seconds behind the GPX start point",
|
|
52
|
-
image_time=
|
|
53
|
+
image_time=build_capture_time(image_metadata.time),
|
|
53
54
|
gpx_start_time=gpx_start_time,
|
|
54
55
|
gpx_end_time=gpx_end_time,
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
if sorted_points[-1].time < image_metadata.time:
|
|
58
59
|
delta = image_metadata.time - sorted_points[-1].time
|
|
59
|
-
gpx_start_time =
|
|
60
|
-
gpx_end_time =
|
|
60
|
+
gpx_start_time = build_capture_time(sorted_points[0].time)
|
|
61
|
+
gpx_end_time = build_capture_time(sorted_points[-1].time)
|
|
61
62
|
# with the tolerance of 1ms
|
|
62
63
|
if 0.001 < delta:
|
|
63
64
|
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
64
65
|
f"The image time is {round(delta, 3)} seconds beyond the GPX end point",
|
|
65
|
-
image_time=
|
|
66
|
+
image_time=build_capture_time(image_metadata.time),
|
|
66
67
|
gpx_start_time=gpx_start_time,
|
|
67
68
|
gpx_end_time=gpx_end_time,
|
|
68
69
|
)
|
|
@@ -13,6 +13,7 @@ else:
|
|
|
13
13
|
from .. import types, utils
|
|
14
14
|
from .base import GeotagImagesFromGeneric
|
|
15
15
|
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
16
|
+
from .geotag_videos_from_video import GeotagVideosFromVideo
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
LOG = logging.getLogger(__name__)
|
|
@@ -90,3 +91,37 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
|
|
|
90
91
|
assert len(final_image_metadatas) <= len(image_paths)
|
|
91
92
|
|
|
92
93
|
return final_image_metadatas
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GeotagImageSamplesFromVideo(GeotagImagesFromGeneric):
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
source_path: Path,
|
|
100
|
+
filetypes: set[types.FileType] | None = None,
|
|
101
|
+
offset_time: float = 0.0,
|
|
102
|
+
num_processes: int | None = None,
|
|
103
|
+
):
|
|
104
|
+
super().__init__(num_processes=num_processes)
|
|
105
|
+
self.source_path = source_path
|
|
106
|
+
self.filetypes = filetypes
|
|
107
|
+
self.offset_time = offset_time
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def to_description(
|
|
111
|
+
self, image_paths: T.Sequence[Path]
|
|
112
|
+
) -> list[types.ImageMetadataOrError]:
|
|
113
|
+
video_paths = utils.find_videos([self.source_path])
|
|
114
|
+
image_samples_by_video_path = utils.find_all_image_samples(
|
|
115
|
+
image_paths, video_paths
|
|
116
|
+
)
|
|
117
|
+
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
118
|
+
video_metadatas = GeotagVideosFromVideo(
|
|
119
|
+
filetypes=self.filetypes,
|
|
120
|
+
num_processes=self.num_processes,
|
|
121
|
+
).to_description(video_paths_with_image_samples)
|
|
122
|
+
geotag = GeotagImagesFromVideo(
|
|
123
|
+
video_metadatas,
|
|
124
|
+
offset_time=self.offset_time,
|
|
125
|
+
num_processes=self.num_processes,
|
|
126
|
+
)
|
|
127
|
+
return geotag.to_description(image_paths)
|