mapillary-tools 0.14.0a1__py3-none-any.whl → 0.14.0a2__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 +4 -4
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +7 -5
- mapillary_tools/constants.py +1 -2
- mapillary_tools/exceptions.py +1 -1
- mapillary_tools/exif_read.py +65 -65
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +23 -46
- mapillary_tools/exiftool_read_video.py +36 -34
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +32 -48
- mapillary_tools/geotag/factory.py +27 -34
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +107 -59
- mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
- mapillary_tools/geotag/geotag_images_from_video.py +16 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +14 -115
- mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +1 -0
- mapillary_tools/geotag/utils.py +62 -0
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
- mapillary_tools/geotag/video_extractors/native.py +157 -0
- mapillary_tools/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +4 -2
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/mp4_sample_parser.py +27 -27
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -12
- mapillary_tools/process_geotag_properties.py +5 -7
- mapillary_tools/process_sequence_properties.py +40 -38
- mapillary_tools/sample_video.py +8 -8
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +33 -38
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +1 -1
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
|
@@ -8,24 +8,14 @@ from pathlib import Path
|
|
|
8
8
|
from tqdm import tqdm
|
|
9
9
|
|
|
10
10
|
from .. import exceptions, types, utils
|
|
11
|
+
from .image_extractors.base import BaseImageExtractor
|
|
12
|
+
from .video_extractors.base import BaseVideoExtractor
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
LOG = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
Extracts metadata from an image file.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self, image_path: Path):
|
|
22
|
-
self.image_path = image_path
|
|
23
|
-
|
|
24
|
-
def extract(self) -> types.ImageMetadataOrError:
|
|
25
|
-
raise NotImplementedError
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
TImageExtractor = T.TypeVar("TImageExtractor", bound=GenericImageExtractor)
|
|
18
|
+
TImageExtractor = T.TypeVar("TImageExtractor", bound=BaseImageExtractor)
|
|
29
19
|
|
|
30
20
|
|
|
31
21
|
class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
@@ -33,16 +23,15 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
33
23
|
Extracts metadata from a list of image files with multiprocessing.
|
|
34
24
|
"""
|
|
35
25
|
|
|
36
|
-
def __init__(
|
|
37
|
-
self, image_paths: T.Sequence[Path], num_processes: int | None = None
|
|
38
|
-
) -> None:
|
|
39
|
-
self.image_paths = image_paths
|
|
26
|
+
def __init__(self, num_processes: int | None = None) -> None:
|
|
40
27
|
self.num_processes = num_processes
|
|
41
28
|
|
|
42
|
-
def to_description(
|
|
43
|
-
|
|
29
|
+
def to_description(
|
|
30
|
+
self, image_paths: T.Sequence[Path]
|
|
31
|
+
) -> list[types.ImageMetadataOrError]:
|
|
32
|
+
extractor_or_errors = self._generate_image_extractors(image_paths)
|
|
44
33
|
|
|
45
|
-
assert len(extractor_or_errors) == len(
|
|
34
|
+
assert len(extractor_or_errors) == len(image_paths)
|
|
46
35
|
|
|
47
36
|
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
48
37
|
|
|
@@ -64,11 +53,6 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
64
53
|
|
|
65
54
|
return results + error_metadatas
|
|
66
55
|
|
|
67
|
-
def _generate_image_extractors(
|
|
68
|
-
self,
|
|
69
|
-
) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
|
|
70
|
-
raise NotImplementedError
|
|
71
|
-
|
|
72
56
|
# This method is passed to multiprocessing
|
|
73
57
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
74
58
|
@classmethod
|
|
@@ -81,26 +65,23 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
81
65
|
return types.describe_error_metadata(
|
|
82
66
|
ex, image_path, filetype=types.FileType.IMAGE
|
|
83
67
|
)
|
|
68
|
+
except exceptions.MapillaryUserError as ex:
|
|
69
|
+
# Considered as fatal error if not MapillaryDescriptionError
|
|
70
|
+
raise ex
|
|
84
71
|
except Exception as ex:
|
|
72
|
+
# TODO: hide details if not verbose mode
|
|
85
73
|
LOG.exception("Unexpected error extracting metadata from %s", image_path)
|
|
86
74
|
return types.describe_error_metadata(
|
|
87
75
|
ex, image_path, filetype=types.FileType.IMAGE
|
|
88
76
|
)
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
Extracts metadata from a video file.
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
def __init__(self, video_path: Path):
|
|
97
|
-
self.video_path = video_path
|
|
98
|
-
|
|
99
|
-
def extract(self) -> types.VideoMetadataOrError:
|
|
78
|
+
def _generate_image_extractors(
|
|
79
|
+
self, image_paths: T.Sequence[Path]
|
|
80
|
+
) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
|
|
100
81
|
raise NotImplementedError
|
|
101
82
|
|
|
102
83
|
|
|
103
|
-
TVideoExtractor = T.TypeVar("TVideoExtractor", bound=
|
|
84
|
+
TVideoExtractor = T.TypeVar("TVideoExtractor", bound=BaseVideoExtractor)
|
|
104
85
|
|
|
105
86
|
|
|
106
87
|
class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
@@ -108,16 +89,15 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
108
89
|
Extracts metadata from a list of video files with multiprocessing.
|
|
109
90
|
"""
|
|
110
91
|
|
|
111
|
-
def __init__(
|
|
112
|
-
self, video_paths: T.Sequence[Path], num_processes: int | None = None
|
|
113
|
-
) -> None:
|
|
114
|
-
self.video_paths = video_paths
|
|
92
|
+
def __init__(self, num_processes: int | None = None) -> None:
|
|
115
93
|
self.num_processes = num_processes
|
|
116
94
|
|
|
117
|
-
def to_description(
|
|
118
|
-
|
|
95
|
+
def to_description(
|
|
96
|
+
self, video_paths: T.Sequence[Path]
|
|
97
|
+
) -> list[types.VideoMetadataOrError]:
|
|
98
|
+
extractor_or_errors = self._generate_video_extractors(video_paths)
|
|
119
99
|
|
|
120
|
-
assert len(extractor_or_errors) == len(
|
|
100
|
+
assert len(extractor_or_errors) == len(video_paths)
|
|
121
101
|
|
|
122
102
|
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
123
103
|
|
|
@@ -139,11 +119,6 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
139
119
|
|
|
140
120
|
return results + error_metadatas
|
|
141
121
|
|
|
142
|
-
def _generate_video_extractors(
|
|
143
|
-
self,
|
|
144
|
-
) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
|
|
145
|
-
raise NotImplementedError
|
|
146
|
-
|
|
147
122
|
# This method is passed to multiprocessing
|
|
148
123
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
149
124
|
@classmethod
|
|
@@ -156,8 +131,17 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
156
131
|
return types.describe_error_metadata(
|
|
157
132
|
ex, video_path, filetype=types.FileType.VIDEO
|
|
158
133
|
)
|
|
134
|
+
except exceptions.MapillaryUserError as ex:
|
|
135
|
+
# Considered as fatal error if not MapillaryDescriptionError
|
|
136
|
+
raise ex
|
|
159
137
|
except Exception as ex:
|
|
138
|
+
# TODO: hide details if not verbose mode
|
|
160
139
|
LOG.exception("Unexpected error extracting metadata from %s", video_path)
|
|
161
140
|
return types.describe_error_metadata(
|
|
162
141
|
ex, video_path, filetype=types.FileType.VIDEO
|
|
163
142
|
)
|
|
143
|
+
|
|
144
|
+
def _generate_video_extractors(
|
|
145
|
+
self, video_paths: T.Sequence[Path]
|
|
146
|
+
) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
|
|
147
|
+
raise NotImplementedError
|
|
@@ -8,14 +8,13 @@ from pathlib import Path
|
|
|
8
8
|
from .. import exceptions, types, utils
|
|
9
9
|
from ..types import FileType
|
|
10
10
|
from . import (
|
|
11
|
-
|
|
11
|
+
base,
|
|
12
12
|
geotag_images_from_exif,
|
|
13
13
|
geotag_images_from_exiftool,
|
|
14
|
-
geotag_images_from_exiftool_both_image_and_video,
|
|
15
14
|
geotag_images_from_gpx_file,
|
|
16
15
|
geotag_images_from_nmea_file,
|
|
17
16
|
geotag_images_from_video,
|
|
18
|
-
|
|
17
|
+
geotag_videos_from_exiftool,
|
|
19
18
|
geotag_videos_from_gpx,
|
|
20
19
|
geotag_videos_from_video,
|
|
21
20
|
)
|
|
@@ -106,7 +105,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
|
|
|
106
105
|
|
|
107
106
|
|
|
108
107
|
def _filter_images_and_videos(
|
|
109
|
-
|
|
108
|
+
paths: T.Iterable[Path],
|
|
110
109
|
filetypes: set[types.FileType] | None = None,
|
|
111
110
|
) -> tuple[list[Path], list[Path]]:
|
|
112
111
|
image_paths = []
|
|
@@ -121,7 +120,7 @@ def _filter_images_and_videos(
|
|
|
121
120
|
include_images = types.FileType.IMAGE in filetypes
|
|
122
121
|
include_videos = bool(filetypes & ALL_VIDEO_TYPES)
|
|
123
122
|
|
|
124
|
-
for path in
|
|
123
|
+
for path in paths:
|
|
125
124
|
if utils.is_image_file(path):
|
|
126
125
|
if include_images:
|
|
127
126
|
image_paths.append(path)
|
|
@@ -154,20 +153,20 @@ def _geotag_images(
|
|
|
154
153
|
else:
|
|
155
154
|
interpolation = option.interpolation
|
|
156
155
|
|
|
157
|
-
geotag:
|
|
156
|
+
geotag: base.GeotagImagesFromGeneric
|
|
158
157
|
|
|
159
158
|
if option.source is SourceType.NATIVE:
|
|
160
159
|
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
161
|
-
|
|
160
|
+
num_processes=option.num_processes
|
|
162
161
|
)
|
|
163
|
-
return geotag.to_description()
|
|
162
|
+
return geotag.to_description(image_paths)
|
|
164
163
|
|
|
165
164
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
166
165
|
geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
|
|
167
|
-
|
|
166
|
+
num_processes=option.num_processes
|
|
168
167
|
)
|
|
169
168
|
try:
|
|
170
|
-
return geotag.to_description()
|
|
169
|
+
return geotag.to_description(image_paths)
|
|
171
170
|
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
172
171
|
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
173
172
|
return []
|
|
@@ -175,39 +174,36 @@ def _geotag_images(
|
|
|
175
174
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
176
175
|
# This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
|
|
177
176
|
# to work
|
|
178
|
-
geotag =
|
|
179
|
-
image_paths,
|
|
177
|
+
geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
|
|
180
178
|
xml_path=_ensure_source_path(option),
|
|
181
179
|
num_processes=option.num_processes,
|
|
182
180
|
)
|
|
183
|
-
return geotag.to_description()
|
|
181
|
+
return geotag.to_description(image_paths)
|
|
184
182
|
|
|
185
183
|
elif option.source is SourceType.GPX:
|
|
186
184
|
geotag = geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
|
|
187
|
-
image_paths,
|
|
188
185
|
source_path=_ensure_source_path(option),
|
|
189
186
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
190
187
|
offset_time=interpolation.offset_time,
|
|
191
188
|
num_processes=option.num_processes,
|
|
192
189
|
)
|
|
193
|
-
return geotag.to_description()
|
|
190
|
+
return geotag.to_description(image_paths)
|
|
194
191
|
|
|
195
192
|
elif option.source is SourceType.NMEA:
|
|
196
193
|
geotag = geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
|
|
197
|
-
image_paths,
|
|
198
194
|
source_path=_ensure_source_path(option),
|
|
199
195
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
200
196
|
offset_time=interpolation.offset_time,
|
|
201
197
|
num_processes=option.num_processes,
|
|
202
198
|
)
|
|
203
199
|
|
|
204
|
-
return geotag.to_description()
|
|
200
|
+
return geotag.to_description(image_paths)
|
|
205
201
|
|
|
206
202
|
elif option.source is SourceType.EXIF:
|
|
207
203
|
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
208
|
-
|
|
204
|
+
num_processes=option.num_processes
|
|
209
205
|
)
|
|
210
|
-
return geotag.to_description()
|
|
206
|
+
return geotag.to_description(image_paths)
|
|
211
207
|
|
|
212
208
|
elif option.source in [
|
|
213
209
|
SourceType.GOPRO,
|
|
@@ -225,17 +221,15 @@ def _geotag_images(
|
|
|
225
221
|
)
|
|
226
222
|
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
227
223
|
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
228
|
-
video_paths_with_image_samples,
|
|
229
224
|
filetypes={map_geotag_source_to_filetype[option.source]},
|
|
230
225
|
num_processes=option.num_processes,
|
|
231
|
-
).to_description()
|
|
226
|
+
).to_description(video_paths_with_image_samples)
|
|
232
227
|
geotag = geotag_images_from_video.GeotagImagesFromVideo(
|
|
233
|
-
image_paths,
|
|
234
228
|
video_metadatas,
|
|
235
229
|
offset_time=interpolation.offset_time,
|
|
236
230
|
num_processes=option.num_processes,
|
|
237
231
|
)
|
|
238
|
-
return geotag.to_description()
|
|
232
|
+
return geotag.to_description(image_paths)
|
|
239
233
|
|
|
240
234
|
else:
|
|
241
235
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
@@ -249,34 +243,33 @@ def _geotag_videos(
|
|
|
249
243
|
if not video_paths:
|
|
250
244
|
return []
|
|
251
245
|
|
|
252
|
-
geotag:
|
|
246
|
+
geotag: base.GeotagVideosFromGeneric
|
|
253
247
|
|
|
254
248
|
if option.source is SourceType.NATIVE:
|
|
255
249
|
geotag = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
256
|
-
|
|
250
|
+
num_processes=option.num_processes, filetypes=option.filetypes
|
|
257
251
|
)
|
|
258
|
-
return geotag.to_description()
|
|
252
|
+
return geotag.to_description(video_paths)
|
|
259
253
|
|
|
260
254
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
261
|
-
geotag =
|
|
262
|
-
|
|
255
|
+
geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
|
|
256
|
+
num_processes=option.num_processes
|
|
263
257
|
)
|
|
264
258
|
try:
|
|
265
|
-
return geotag.to_description()
|
|
259
|
+
return geotag.to_description(video_paths)
|
|
266
260
|
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
267
261
|
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
268
262
|
return []
|
|
269
263
|
|
|
270
264
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
271
|
-
geotag =
|
|
272
|
-
video_paths,
|
|
265
|
+
geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
|
|
273
266
|
xml_path=_ensure_source_path(option),
|
|
274
267
|
)
|
|
275
|
-
return geotag.to_description()
|
|
268
|
+
return geotag.to_description(video_paths)
|
|
276
269
|
|
|
277
270
|
elif option.source is SourceType.GPX:
|
|
278
|
-
geotag = geotag_videos_from_gpx.GeotagVideosFromGPX(
|
|
279
|
-
return geotag.to_description()
|
|
271
|
+
geotag = geotag_videos_from_gpx.GeotagVideosFromGPX()
|
|
272
|
+
return geotag.to_description(video_paths)
|
|
280
273
|
|
|
281
274
|
elif option.source is SourceType.NMEA:
|
|
282
275
|
# TODO: geotag videos from NMEA
|
|
@@ -1,60 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
4
|
+
import sys
|
|
3
5
|
import typing as T
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
LOG = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ImageEXIFExtractor(GenericImageExtractor):
|
|
14
|
-
def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
|
|
15
|
-
super().__init__(image_path)
|
|
16
|
-
self.skip_lonlat_error = skip_lonlat_error
|
|
8
|
+
if sys.version_info >= (3, 12):
|
|
9
|
+
from typing import override
|
|
10
|
+
else:
|
|
11
|
+
from typing_extensions import override
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
with self.image_path.open("rb") as fp:
|
|
21
|
-
yield ExifRead(fp)
|
|
13
|
+
from .base import GeotagImagesFromGeneric
|
|
14
|
+
from .image_extractors.exif import ImageEXIFExtractor
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
with self._exif_context() as exif:
|
|
25
|
-
lonlat = exif.extract_lon_lat()
|
|
26
|
-
if lonlat is None:
|
|
27
|
-
if not self.skip_lonlat_error:
|
|
28
|
-
raise exceptions.MapillaryGeoTaggingError(
|
|
29
|
-
"Unable to extract GPS Longitude or GPS Latitude from the image"
|
|
30
|
-
)
|
|
31
|
-
lonlat = (0.0, 0.0)
|
|
32
|
-
lon, lat = lonlat
|
|
33
|
-
|
|
34
|
-
capture_time = exif.extract_capture_time()
|
|
35
|
-
if capture_time is None:
|
|
36
|
-
raise exceptions.MapillaryGeoTaggingError(
|
|
37
|
-
"Unable to extract timestamp from the image"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
image_metadata = types.ImageMetadata(
|
|
41
|
-
filename=self.image_path,
|
|
42
|
-
filesize=utils.get_file_size(self.image_path),
|
|
43
|
-
time=geo.as_unix_time(capture_time),
|
|
44
|
-
lat=lat,
|
|
45
|
-
lon=lon,
|
|
46
|
-
alt=exif.extract_altitude(),
|
|
47
|
-
angle=exif.extract_direction(),
|
|
48
|
-
width=exif.extract_width(),
|
|
49
|
-
height=exif.extract_height(),
|
|
50
|
-
MAPOrientation=exif.extract_orientation(),
|
|
51
|
-
MAPDeviceMake=exif.extract_make(),
|
|
52
|
-
MAPDeviceModel=exif.extract_model(),
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
return image_metadata
|
|
16
|
+
LOG = logging.getLogger(__name__)
|
|
56
17
|
|
|
57
18
|
|
|
58
19
|
class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
|
|
59
|
-
|
|
60
|
-
|
|
20
|
+
@override
|
|
21
|
+
def _generate_image_extractors(
|
|
22
|
+
self, image_paths: T.Sequence[Path]
|
|
23
|
+
) -> T.Sequence[ImageEXIFExtractor]:
|
|
24
|
+
return [ImageEXIFExtractor(path) for path in image_paths]
|
|
@@ -1,105 +1,153 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
3
|
import logging
|
|
4
|
+
import sys
|
|
5
5
|
import typing as T
|
|
6
6
|
import xml.etree.ElementTree as ET
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
if sys.version_info >= (3, 12):
|
|
10
|
+
from typing import override
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
from .. import constants, exceptions, exiftool_read, types, utils
|
|
10
15
|
from ..exiftool_runner import ExiftoolRunner
|
|
11
|
-
from .
|
|
12
|
-
from .
|
|
16
|
+
from .base import GeotagImagesFromGeneric
|
|
17
|
+
from .geotag_images_from_video import GeotagImagesFromVideo
|
|
18
|
+
from .geotag_videos_from_exiftool import GeotagVideosFromExifToolXML
|
|
19
|
+
from .image_extractors.exiftool import ImageExifToolExtractor
|
|
20
|
+
from .utils import index_rdf_description_by_path
|
|
13
21
|
|
|
14
22
|
LOG = logging.getLogger(__name__)
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
class
|
|
18
|
-
def __init__(self, image_path: Path, element: ET.Element):
|
|
19
|
-
super().__init__(image_path)
|
|
20
|
-
self.element = element
|
|
21
|
-
|
|
22
|
-
@contextlib.contextmanager
|
|
23
|
-
def _exif_context(self):
|
|
24
|
-
yield exiftool_read.ExifToolRead(ET.ElementTree(self.element))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class GeotagImagesFromExifTool(GeotagImagesFromGeneric):
|
|
25
|
+
class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
|
|
28
26
|
def __init__(
|
|
29
27
|
self,
|
|
30
|
-
image_paths: T.Sequence[Path],
|
|
31
28
|
xml_path: Path,
|
|
32
29
|
num_processes: int | None = None,
|
|
33
30
|
):
|
|
34
31
|
self.xml_path = xml_path
|
|
35
|
-
super().__init__(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
super().__init__(num_processes=num_processes)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def build_image_extractors(
|
|
36
|
+
cls,
|
|
37
|
+
rdf_by_path: dict[str, ET.Element],
|
|
38
|
+
image_paths: T.Iterable[Path],
|
|
39
|
+
) -> list[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
44
40
|
results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
|
|
45
41
|
|
|
46
|
-
for path in
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
52
|
-
f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
|
|
42
|
+
for path in image_paths:
|
|
43
|
+
rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
|
|
44
|
+
if rdf is None:
|
|
45
|
+
ex = exceptions.MapillaryExifToolXMLNotFoundError(
|
|
46
|
+
"Cannot find the image in the ExifTool XML"
|
|
53
47
|
)
|
|
54
48
|
results.append(
|
|
55
49
|
types.describe_error_metadata(
|
|
56
|
-
|
|
50
|
+
ex, path, filetype=types.FileType.IMAGE
|
|
57
51
|
)
|
|
58
52
|
)
|
|
59
53
|
else:
|
|
60
|
-
results.append(ImageExifToolExtractor(path,
|
|
54
|
+
results.append(ImageExifToolExtractor(path, rdf))
|
|
61
55
|
|
|
62
56
|
return results
|
|
63
57
|
|
|
58
|
+
@override
|
|
59
|
+
def _generate_image_extractors(
|
|
60
|
+
self, image_paths: T.Sequence[Path]
|
|
61
|
+
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
62
|
+
rdf_by_path = index_rdf_description_by_path([self.xml_path])
|
|
63
|
+
return self.build_image_extractors(rdf_by_path, image_paths)
|
|
64
|
+
|
|
64
65
|
|
|
65
66
|
class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
67
|
+
@override
|
|
66
68
|
def _generate_image_extractors(
|
|
67
|
-
self,
|
|
69
|
+
self, image_paths: T.Sequence[Path]
|
|
68
70
|
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
69
71
|
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
70
72
|
|
|
71
73
|
LOG.debug(
|
|
72
|
-
"Extracting XML from %d images with
|
|
73
|
-
len(
|
|
74
|
+
"Extracting XML from %d images with ExifTool command: %s",
|
|
75
|
+
len(image_paths),
|
|
74
76
|
" ".join(runner._build_args_read_stdin()),
|
|
75
77
|
)
|
|
76
78
|
try:
|
|
77
|
-
xml = runner.extract_xml(
|
|
79
|
+
xml = runner.extract_xml(image_paths)
|
|
78
80
|
except FileNotFoundError as ex:
|
|
79
81
|
raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
try:
|
|
84
|
+
xml_element = ET.fromstring(xml)
|
|
85
|
+
except ET.ParseError as ex:
|
|
86
|
+
LOG.warning(
|
|
87
|
+
"Failed to parse ExifTool XML: %s",
|
|
88
|
+
str(ex),
|
|
89
|
+
exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
90
|
+
)
|
|
91
|
+
rdf_by_path = {}
|
|
92
|
+
else:
|
|
93
|
+
rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
94
|
+
xml_element
|
|
84
95
|
)
|
|
85
|
-
)
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
return GeotagImagesFromExifToolXML.build_image_extractors(
|
|
98
|
+
rdf_by_path, image_paths
|
|
99
|
+
)
|
|
88
100
|
|
|
89
|
-
for path in self.image_paths:
|
|
90
|
-
rdf_description = rdf_description_by_path.get(
|
|
91
|
-
exiftool_read.canonical_path(path)
|
|
92
|
-
)
|
|
93
|
-
if rdf_description is None:
|
|
94
|
-
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
95
|
-
f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
|
|
96
|
-
)
|
|
97
|
-
results.append(
|
|
98
|
-
types.describe_error_metadata(
|
|
99
|
-
exc, path, filetype=types.FileType.IMAGE
|
|
100
|
-
)
|
|
101
|
-
)
|
|
102
|
-
else:
|
|
103
|
-
results.append(ImageExifToolExtractor(path, rdf_description))
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
xml_path: Path,
|
|
106
|
+
offset_time: float = 0.0,
|
|
107
|
+
num_processes: int | None = None,
|
|
108
|
+
):
|
|
109
|
+
super().__init__(num_processes=num_processes)
|
|
110
|
+
self.xml_path = xml_path
|
|
111
|
+
self.offset_time = offset_time
|
|
112
|
+
|
|
113
|
+
def geotag_samples(
|
|
114
|
+
self, image_paths: T.Sequence[Path]
|
|
115
|
+
) -> list[types.ImageMetadataOrError]:
|
|
116
|
+
# Find all video paths in self.xml_path
|
|
117
|
+
rdf_by_path = index_rdf_description_by_path([self.xml_path])
|
|
118
|
+
video_paths = utils.find_videos(
|
|
119
|
+
[Path(pathstr) for pathstr in rdf_by_path.keys()],
|
|
120
|
+
skip_subfolders=True,
|
|
121
|
+
)
|
|
122
|
+
# Find all video paths that have sample images
|
|
123
|
+
samples_by_video = utils.find_all_image_samples(image_paths, video_paths)
|
|
124
|
+
|
|
125
|
+
video_metadata_or_errors = GeotagVideosFromExifToolXML(
|
|
126
|
+
self.xml_path,
|
|
127
|
+
num_processes=self.num_processes,
|
|
128
|
+
).to_description(list(samples_by_video.keys()))
|
|
129
|
+
sample_paths = sum(samples_by_video.values(), [])
|
|
130
|
+
sample_metadata_or_errors = GeotagImagesFromVideo(
|
|
131
|
+
video_metadata_or_errors,
|
|
132
|
+
offset_time=self.offset_time,
|
|
133
|
+
num_processes=self.num_processes,
|
|
134
|
+
).to_description(sample_paths)
|
|
135
|
+
|
|
136
|
+
return sample_metadata_or_errors
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
def to_description(
|
|
140
|
+
self, image_paths: T.Sequence[Path]
|
|
141
|
+
) -> list[types.ImageMetadataOrError]:
|
|
142
|
+
sample_metadata_or_errors = self.geotag_samples(image_paths)
|
|
143
|
+
|
|
144
|
+
sample_paths = set(metadata.filename for metadata in sample_metadata_or_errors)
|
|
145
|
+
|
|
146
|
+
non_sample_paths = [path for path in image_paths if path not in sample_paths]
|
|
147
|
+
|
|
148
|
+
non_sample_metadata_or_errors = GeotagImagesFromExifToolXML(
|
|
149
|
+
self.xml_path,
|
|
150
|
+
num_processes=self.num_processes,
|
|
151
|
+
).to_description(non_sample_paths)
|
|
152
|
+
|
|
153
|
+
return sample_metadata_or_errors + non_sample_metadata_or_errors
|