mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.0b1__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 +1 -0
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/config.py +38 -17
- mapillary_tools/constants.py +2 -0
- mapillary_tools/exiftool_read_video.py +52 -15
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +406 -232
- mapillary_tools/geotag/__init__.py +0 -0
- mapillary_tools/geotag/base.py +2 -2
- mapillary_tools/geotag/factory.py +97 -88
- mapillary_tools/geotag/geotag_images_from_exiftool.py +26 -19
- mapillary_tools/geotag/geotag_images_from_gpx.py +13 -6
- mapillary_tools/geotag/geotag_images_from_video.py +35 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +39 -13
- mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
- mapillary_tools/geotag/options.py +25 -3
- 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 +60 -70
- mapillary_tools/geotag/video_extractors/native.py +9 -31
- mapillary_tools/history.py +4 -1
- mapillary_tools/process_geotag_properties.py +16 -8
- mapillary_tools/process_sequence_properties.py +9 -11
- mapillary_tools/sample_video.py +7 -6
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/types.py +44 -610
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/RECORD +38 -35
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
|
File without changes
|
mapillary_tools/geotag/base.py
CHANGED
|
@@ -51,7 +51,7 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
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
|
|
@@ -117,7 +117,7 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
117
117
|
)
|
|
118
118
|
)
|
|
119
119
|
|
|
120
|
-
return results + error_metadatas
|
|
120
|
+
return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
|
|
121
121
|
|
|
122
122
|
# This method is passed to multiprocessing
|
|
123
123
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
@@ -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,
|
|
@@ -70,8 +69,25 @@ def process(
|
|
|
70
69
|
for idx, option in enumerate(options):
|
|
71
70
|
LOG.debug("Processing %d files with %s", len(reprocessable_paths), option)
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
image_videos, video_paths = _filter_images_and_videos(
|
|
73
|
+
reprocessable_paths, option.filetypes
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if image_videos:
|
|
77
|
+
image_geotag = _build_image_geotag(option)
|
|
78
|
+
image_metadata_or_errors = (
|
|
79
|
+
image_geotag.to_description(image_videos) if image_geotag else []
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
image_metadata_or_errors = []
|
|
83
|
+
|
|
84
|
+
if video_paths:
|
|
85
|
+
video_geotag = _build_video_geotag(option)
|
|
86
|
+
video_metadata_or_errors = (
|
|
87
|
+
video_geotag.to_description(video_paths) if video_geotag else []
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
video_metadata_or_errors = []
|
|
75
91
|
|
|
76
92
|
more_option = idx < len(options) - 1
|
|
77
93
|
|
|
@@ -97,6 +113,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
|
|
|
97
113
|
(
|
|
98
114
|
exceptions.MapillaryGeoTaggingError,
|
|
99
115
|
exceptions.MapillaryVideoGPSNotFoundError,
|
|
116
|
+
exceptions.MapillaryExiftoolNotFoundError,
|
|
100
117
|
),
|
|
101
118
|
):
|
|
102
119
|
return True
|
|
@@ -140,152 +157,144 @@ def _ensure_source_path(option: SourceOption) -> Path:
|
|
|
140
157
|
return option.source_path.source_path
|
|
141
158
|
|
|
142
159
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if not image_paths:
|
|
149
|
-
return []
|
|
150
|
-
|
|
160
|
+
def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
|
|
161
|
+
"""
|
|
162
|
+
Build a GeotagImagesFromGeneric object based on the provided option.
|
|
163
|
+
"""
|
|
151
164
|
if option.interpolation is None:
|
|
152
165
|
interpolation = InterpolationOption()
|
|
153
166
|
else:
|
|
154
167
|
interpolation = option.interpolation
|
|
155
168
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if option.source is SourceType.NATIVE:
|
|
159
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
169
|
+
if option.source in [SourceType.EXIF, SourceType.NATIVE]:
|
|
170
|
+
return geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
160
171
|
num_processes=option.num_processes
|
|
161
172
|
)
|
|
162
|
-
return geotag.to_description(image_paths)
|
|
163
173
|
|
|
164
174
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
165
|
-
|
|
175
|
+
return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
|
|
166
176
|
num_processes=option.num_processes
|
|
167
177
|
)
|
|
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
178
|
|
|
174
179
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
175
180
|
# This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
|
|
176
181
|
# to work
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
if option.source_path is None:
|
|
183
|
+
raise exceptions.MapillaryBadParameterError(
|
|
184
|
+
"source_path must be provided for EXIFTOOL_XML source"
|
|
185
|
+
)
|
|
186
|
+
return geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
|
|
187
|
+
source_path=option.source_path,
|
|
179
188
|
num_processes=option.num_processes,
|
|
180
189
|
)
|
|
181
|
-
return geotag.to_description(image_paths)
|
|
182
190
|
|
|
183
191
|
elif option.source is SourceType.GPX:
|
|
184
|
-
|
|
192
|
+
return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
|
|
185
193
|
source_path=_ensure_source_path(option),
|
|
186
194
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
187
195
|
offset_time=interpolation.offset_time,
|
|
188
196
|
num_processes=option.num_processes,
|
|
189
197
|
)
|
|
190
|
-
return geotag.to_description(image_paths)
|
|
191
198
|
|
|
192
199
|
elif option.source is SourceType.NMEA:
|
|
193
|
-
|
|
200
|
+
return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
|
|
194
201
|
source_path=_ensure_source_path(option),
|
|
195
202
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
196
203
|
offset_time=interpolation.offset_time,
|
|
197
204
|
num_processes=option.num_processes,
|
|
198
205
|
)
|
|
199
206
|
|
|
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,
|
|
207
|
+
elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
|
|
208
|
+
return geotag_images_from_video.GeotagImageSamplesFromVideo(
|
|
209
|
+
_ensure_source_path(option),
|
|
229
210
|
offset_time=interpolation.offset_time,
|
|
230
211
|
num_processes=option.num_processes,
|
|
231
212
|
)
|
|
232
|
-
return geotag.to_description(image_paths)
|
|
233
213
|
|
|
234
214
|
else:
|
|
235
215
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
236
216
|
|
|
237
217
|
|
|
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
|
|
218
|
+
def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
|
|
219
|
+
"""
|
|
220
|
+
Build a GeotagVideosFromGeneric object based on the provided option.
|
|
247
221
|
|
|
222
|
+
Examples:
|
|
223
|
+
>>> from pathlib import Path
|
|
224
|
+
>>> from mapillary_tools.geotag.options import SourceOption, SourceType
|
|
225
|
+
>>> opt = SourceOption(SourceType.NATIVE)
|
|
226
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
227
|
+
>>> geotagger.__class__.__name__
|
|
228
|
+
'GeotagVideosFromVideo'
|
|
229
|
+
|
|
230
|
+
>>> opt = SourceOption(SourceType.EXIFTOOL_RUNTIME)
|
|
231
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
232
|
+
>>> geotagger.__class__.__name__
|
|
233
|
+
'GeotagVideosFromExifToolRunner'
|
|
234
|
+
|
|
235
|
+
>>> opt = SourceOption(SourceType.EXIFTOOL_XML, source_path=Path("/tmp/test.xml"))
|
|
236
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
237
|
+
>>> geotagger.__class__.__name__
|
|
238
|
+
'GeotagVideosFromExifToolXML'
|
|
239
|
+
|
|
240
|
+
>>> opt = SourceOption(SourceType.GPX, source_path=Path("/tmp/test.gpx"))
|
|
241
|
+
>>> geotagger = _build_video_geotag(opt)
|
|
242
|
+
>>> geotagger.__class__.__name__
|
|
243
|
+
'GeotagVideosFromGPX'
|
|
244
|
+
|
|
245
|
+
>>> opt = SourceOption(SourceType.NMEA, source_path=Path("/tmp/test.nmea"))
|
|
246
|
+
>>> _build_video_geotag(opt) is None
|
|
247
|
+
True
|
|
248
|
+
|
|
249
|
+
>>> opt = SourceOption(SourceType.EXIF)
|
|
250
|
+
>>> _build_video_geotag(opt) is None
|
|
251
|
+
True
|
|
252
|
+
|
|
253
|
+
>>> opt = SourceOption(SourceType.GOPRO)
|
|
254
|
+
>>> _build_video_geotag(opt) is None
|
|
255
|
+
True
|
|
256
|
+
|
|
257
|
+
>>> try:
|
|
258
|
+
... _build_video_geotag(SourceOption("invalid"))
|
|
259
|
+
... except ValueError as e:
|
|
260
|
+
... "Invalid geotag source" in str(e)
|
|
261
|
+
True
|
|
262
|
+
"""
|
|
248
263
|
if option.source is SourceType.NATIVE:
|
|
249
|
-
|
|
264
|
+
return geotag_videos_from_video.GeotagVideosFromVideo(
|
|
250
265
|
num_processes=option.num_processes, filetypes=option.filetypes
|
|
251
266
|
)
|
|
252
|
-
return geotag.to_description(video_paths)
|
|
253
267
|
|
|
254
268
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
255
|
-
|
|
269
|
+
return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
|
|
256
270
|
num_processes=option.num_processes
|
|
257
271
|
)
|
|
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
272
|
|
|
264
273
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
if option.source_path is None:
|
|
275
|
+
raise exceptions.MapillaryBadParameterError(
|
|
276
|
+
"source_path must be provided for EXIFTOOL_XML source"
|
|
277
|
+
)
|
|
278
|
+
return geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
|
|
279
|
+
source_path=option.source_path,
|
|
267
280
|
)
|
|
268
|
-
return geotag.to_description(video_paths)
|
|
269
281
|
|
|
270
282
|
elif option.source is SourceType.GPX:
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
return geotag_videos_from_gpx.GeotagVideosFromGPX(
|
|
284
|
+
source_path=option.source_path, num_processes=option.num_processes
|
|
285
|
+
)
|
|
273
286
|
|
|
274
287
|
elif option.source is SourceType.NMEA:
|
|
275
288
|
# TODO: geotag videos from NMEA
|
|
276
|
-
return
|
|
289
|
+
return None
|
|
277
290
|
|
|
278
291
|
elif option.source is SourceType.EXIF:
|
|
279
292
|
# Legacy image-specific geotag types
|
|
280
|
-
return
|
|
293
|
+
return None
|
|
281
294
|
|
|
282
|
-
elif option.source in [
|
|
283
|
-
SourceType.GOPRO,
|
|
284
|
-
SourceType.BLACKVUE,
|
|
285
|
-
SourceType.CAMM,
|
|
286
|
-
]:
|
|
295
|
+
elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
|
|
287
296
|
# Legacy image-specific geotag types
|
|
288
|
-
return
|
|
297
|
+
return None
|
|
289
298
|
|
|
290
299
|
else:
|
|
291
300
|
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)
|
|
@@ -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
|
|
|
@@ -19,6 +20,12 @@ from .geotag_images_from_exif import ImageEXIFExtractor
|
|
|
19
20
|
LOG = logging.getLogger(__name__)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
class SyncMode:
|
|
24
|
+
SYNC = "sync"
|
|
25
|
+
STRICT_SYNC = "strict_sync"
|
|
26
|
+
RESET = "reset"
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
23
30
|
def __init__(
|
|
24
31
|
self,
|
|
@@ -43,26 +50,26 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
43
50
|
|
|
44
51
|
if image_metadata.time < sorted_points[0].time:
|
|
45
52
|
delta = sorted_points[0].time - image_metadata.time
|
|
46
|
-
gpx_start_time =
|
|
47
|
-
gpx_end_time =
|
|
53
|
+
gpx_start_time = build_capture_time(sorted_points[0].time)
|
|
54
|
+
gpx_end_time = build_capture_time(sorted_points[-1].time)
|
|
48
55
|
# with the tolerance of 1ms
|
|
49
56
|
if 0.001 < delta:
|
|
50
57
|
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
51
58
|
f"The image date time is {round(delta, 3)} seconds behind the GPX start point",
|
|
52
|
-
image_time=
|
|
59
|
+
image_time=build_capture_time(image_metadata.time),
|
|
53
60
|
gpx_start_time=gpx_start_time,
|
|
54
61
|
gpx_end_time=gpx_end_time,
|
|
55
62
|
)
|
|
56
63
|
|
|
57
64
|
if sorted_points[-1].time < image_metadata.time:
|
|
58
65
|
delta = image_metadata.time - sorted_points[-1].time
|
|
59
|
-
gpx_start_time =
|
|
60
|
-
gpx_end_time =
|
|
66
|
+
gpx_start_time = build_capture_time(sorted_points[0].time)
|
|
67
|
+
gpx_end_time = build_capture_time(sorted_points[-1].time)
|
|
61
68
|
# with the tolerance of 1ms
|
|
62
69
|
if 0.001 < delta:
|
|
63
70
|
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
64
71
|
f"The image time is {round(delta, 3)} seconds beyond the GPX end point",
|
|
65
|
-
image_time=
|
|
72
|
+
image_time=build_capture_time(image_metadata.time),
|
|
66
73
|
gpx_start_time=gpx_start_time,
|
|
67
74
|
gpx_end_time=gpx_end_time,
|
|
68
75
|
)
|
|
@@ -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)
|
|
@@ -13,6 +13,7 @@ else:
|
|
|
13
13
|
|
|
14
14
|
from .. import constants, exceptions, exiftool_read, types
|
|
15
15
|
from ..exiftool_runner import ExiftoolRunner
|
|
16
|
+
from . import options
|
|
16
17
|
from .base import GeotagVideosFromGeneric
|
|
17
18
|
from .utils import index_rdf_description_by_path
|
|
18
19
|
from .video_extractors.exiftool import VideoExifToolExtractor
|
|
@@ -22,18 +23,14 @@ LOG = logging.getLogger(__name__)
|
|
|
22
23
|
|
|
23
24
|
class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
24
25
|
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
xml_path: Path,
|
|
27
|
-
num_processes: int | None = None,
|
|
26
|
+
self, source_path: options.SourcePathOption, num_processes: int | None = None
|
|
28
27
|
):
|
|
29
28
|
super().__init__(num_processes=num_processes)
|
|
30
|
-
self.
|
|
29
|
+
self.source_path = source_path
|
|
31
30
|
|
|
32
31
|
@classmethod
|
|
33
|
-
def
|
|
34
|
-
cls,
|
|
35
|
-
rdf_by_path: dict[str, ET.Element],
|
|
36
|
-
video_paths: T.Iterable[Path],
|
|
32
|
+
def build_video_extractors_from_etree(
|
|
33
|
+
cls, rdf_by_path: dict[str, ET.Element], video_paths: T.Iterable[Path]
|
|
37
34
|
) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
38
35
|
results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
|
|
39
36
|
|
|
@@ -57,8 +54,28 @@ class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
|
57
54
|
def _generate_video_extractors(
|
|
58
55
|
self, video_paths: T.Sequence[Path]
|
|
59
56
|
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
60
|
-
rdf_by_path =
|
|
61
|
-
return self.
|
|
57
|
+
rdf_by_path = self.find_rdf_by_path(self.source_path, video_paths)
|
|
58
|
+
return self.build_video_extractors_from_etree(rdf_by_path, video_paths)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def find_rdf_by_path(
|
|
62
|
+
cls, option: options.SourcePathOption, paths: T.Iterable[Path]
|
|
63
|
+
) -> dict[str, ET.Element]:
|
|
64
|
+
if option.source_path is not None:
|
|
65
|
+
return index_rdf_description_by_path([option.source_path])
|
|
66
|
+
|
|
67
|
+
elif option.pattern is not None:
|
|
68
|
+
rdf_by_path = {}
|
|
69
|
+
for path in paths:
|
|
70
|
+
source_path = option.resolve(path)
|
|
71
|
+
r = index_rdf_description_by_path([source_path])
|
|
72
|
+
rdfs = list(r.values())
|
|
73
|
+
if rdfs:
|
|
74
|
+
rdf_by_path[exiftool_read.canonical_path(path)] = rdfs[0]
|
|
75
|
+
return rdf_by_path
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
assert False, "Either source_path or pattern must be provided"
|
|
62
79
|
|
|
63
80
|
|
|
64
81
|
class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
@@ -66,7 +83,10 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
66
83
|
def _generate_video_extractors(
|
|
67
84
|
self, video_paths: T.Sequence[Path]
|
|
68
85
|
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
69
|
-
|
|
86
|
+
if constants.EXIFTOOL_PATH is None:
|
|
87
|
+
runner = ExiftoolRunner()
|
|
88
|
+
else:
|
|
89
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
70
90
|
|
|
71
91
|
LOG.debug(
|
|
72
92
|
"Extracting XML from %d videos with ExifTool command: %s",
|
|
@@ -76,7 +96,13 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
76
96
|
try:
|
|
77
97
|
xml = runner.extract_xml(video_paths)
|
|
78
98
|
except FileNotFoundError as ex:
|
|
79
|
-
|
|
99
|
+
exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
|
|
100
|
+
return [
|
|
101
|
+
types.describe_error_metadata(
|
|
102
|
+
exiftool_ex, video_path, filetype=types.FileType.VIDEO
|
|
103
|
+
)
|
|
104
|
+
for video_path in video_paths
|
|
105
|
+
]
|
|
80
106
|
|
|
81
107
|
try:
|
|
82
108
|
xml_element = ET.fromstring(xml)
|
|
@@ -92,6 +118,6 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
92
118
|
xml_element
|
|
93
119
|
)
|
|
94
120
|
|
|
95
|
-
return GeotagVideosFromExifToolXML.
|
|
121
|
+
return GeotagVideosFromExifToolXML.build_video_extractors_from_etree(
|
|
96
122
|
rdf_by_path, video_paths
|
|
97
123
|
)
|
|
@@ -10,6 +10,7 @@ if sys.version_info >= (3, 12):
|
|
|
10
10
|
else:
|
|
11
11
|
from typing_extensions import override
|
|
12
12
|
|
|
13
|
+
from .. import exceptions, types
|
|
13
14
|
from . import options
|
|
14
15
|
from .base import GeotagVideosFromGeneric
|
|
15
16
|
from .video_extractors.gpx import GPXVideoExtractor
|
|
@@ -21,19 +22,31 @@ LOG = logging.getLogger(__name__)
|
|
|
21
22
|
class GeotagVideosFromGPX(GeotagVideosFromGeneric):
|
|
22
23
|
def __init__(
|
|
23
24
|
self,
|
|
24
|
-
|
|
25
|
+
source_path: options.SourcePathOption | None = None,
|
|
25
26
|
num_processes: int | None = None,
|
|
26
27
|
):
|
|
27
28
|
super().__init__(num_processes=num_processes)
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
self.
|
|
29
|
+
if source_path is None:
|
|
30
|
+
source_path = options.SourcePathOption(pattern="%g.gpx")
|
|
31
|
+
self.source_path = source_path
|
|
31
32
|
|
|
32
33
|
@override
|
|
33
34
|
def _generate_video_extractors(
|
|
34
35
|
self, video_paths: T.Sequence[Path]
|
|
35
|
-
) -> T.Sequence[GPXVideoExtractor]:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
) -> T.Sequence[GPXVideoExtractor | types.ErrorMetadata]:
|
|
37
|
+
results: list[GPXVideoExtractor | types.ErrorMetadata] = []
|
|
38
|
+
for video_path in video_paths:
|
|
39
|
+
source_path = self.source_path.resolve(video_path)
|
|
40
|
+
if source_path.is_file():
|
|
41
|
+
results.append(GPXVideoExtractor(video_path, source_path))
|
|
42
|
+
else:
|
|
43
|
+
results.append(
|
|
44
|
+
types.describe_error_metadata(
|
|
45
|
+
exceptions.MapillaryVideoGPSNotFoundError(
|
|
46
|
+
"GPX file not found for video"
|
|
47
|
+
),
|
|
48
|
+
filename=video_path,
|
|
49
|
+
filetype=types.FileType.VIDEO,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
return results
|