mapillary-tools 0.14.0a1__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 +5 -4
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +41 -18
- mapillary_tools/constants.py +3 -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 +88 -49
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
- mapillary_tools/geotag/factory.py +105 -103
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
- mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
- 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 +51 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
- 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 +26 -3
- 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/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +135 -0
- mapillary_tools/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +8 -3
- 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 +21 -15
- mapillary_tools/process_sequence_properties.py +49 -49
- mapillary_tools/sample_video.py +15 -14
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +64 -635
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -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/gpx_parser.py +0 -108
- 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.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
mapillary_tools/geo.py
CHANGED
|
@@ -244,14 +244,14 @@ class Interpolator:
|
|
|
244
244
|
return interpolated
|
|
245
245
|
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
_T = T.TypeVar("_T")
|
|
248
248
|
|
|
249
249
|
|
|
250
250
|
def sample_points_by_distance(
|
|
251
|
-
samples: T.Iterable[
|
|
251
|
+
samples: T.Iterable[_T],
|
|
252
252
|
min_distance: float,
|
|
253
|
-
point_func: T.Callable[[
|
|
254
|
-
) -> T.Generator[
|
|
253
|
+
point_func: T.Callable[[_T], Point],
|
|
254
|
+
) -> T.Generator[_T, None, None]:
|
|
255
255
|
prevp: Point | None = None
|
|
256
256
|
for sample in samples:
|
|
257
257
|
if prevp is None:
|
|
@@ -281,23 +281,6 @@ def interpolate_directions_if_none(sequence: T.Sequence[PointLike]) -> None:
|
|
|
281
281
|
sequence[-1].angle = prev_angle
|
|
282
282
|
|
|
283
283
|
|
|
284
|
-
def extend_deduplicate_points(
|
|
285
|
-
sequence: T.Iterable[PointLike],
|
|
286
|
-
to_extend: list[PointLike] | None = None,
|
|
287
|
-
) -> list[PointLike]:
|
|
288
|
-
if to_extend is None:
|
|
289
|
-
to_extend = []
|
|
290
|
-
for point in sequence:
|
|
291
|
-
if to_extend:
|
|
292
|
-
prev = to_extend[-1].lon, to_extend[-1].lat
|
|
293
|
-
cur = (point.lon, point.lat)
|
|
294
|
-
if cur != prev:
|
|
295
|
-
to_extend.append(point)
|
|
296
|
-
else:
|
|
297
|
-
to_extend.append(point)
|
|
298
|
-
return to_extend
|
|
299
|
-
|
|
300
|
-
|
|
301
284
|
def _ecef_from_lla2(lat: float, lon: float) -> tuple[float, float, float]:
|
|
302
285
|
"""
|
|
303
286
|
Compute ECEF XYZ from latitude and longitude.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .. import geo # noqa: F401
|
|
@@ -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
|
|
|
@@ -62,12 +51,7 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
|
62
51
|
)
|
|
63
52
|
)
|
|
64
53
|
|
|
65
|
-
return results + error_metadatas
|
|
66
|
-
|
|
67
|
-
def _generate_image_extractors(
|
|
68
|
-
self,
|
|
69
|
-
) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
|
|
70
|
-
raise NotImplementedError
|
|
54
|
+
return T.cast(list[types.ImageMetadataOrError], results + error_metadatas)
|
|
71
55
|
|
|
72
56
|
# This method is passed to multiprocessing
|
|
73
57
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
@@ -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
|
|
|
@@ -137,12 +117,7 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
|
137
117
|
)
|
|
138
118
|
)
|
|
139
119
|
|
|
140
|
-
return results + error_metadatas
|
|
141
|
-
|
|
142
|
-
def _generate_video_extractors(
|
|
143
|
-
self,
|
|
144
|
-
) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
|
|
145
|
-
raise NotImplementedError
|
|
120
|
+
return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
|
|
146
121
|
|
|
147
122
|
# This method is passed to multiprocessing
|
|
148
123
|
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
@@ -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
|
|
@@ -6,16 +6,14 @@ 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,
|
|
13
12
|
geotag_images_from_exiftool,
|
|
14
|
-
geotag_images_from_exiftool_both_image_and_video,
|
|
15
13
|
geotag_images_from_gpx_file,
|
|
16
14
|
geotag_images_from_nmea_file,
|
|
17
15
|
geotag_images_from_video,
|
|
18
|
-
|
|
16
|
+
geotag_videos_from_exiftool,
|
|
19
17
|
geotag_videos_from_gpx,
|
|
20
18
|
geotag_videos_from_video,
|
|
21
19
|
)
|
|
@@ -71,8 +69,25 @@ def process(
|
|
|
71
69
|
for idx, option in enumerate(options):
|
|
72
70
|
LOG.debug("Processing %d files with %s", len(reprocessable_paths), option)
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
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 = []
|
|
76
91
|
|
|
77
92
|
more_option = idx < len(options) - 1
|
|
78
93
|
|
|
@@ -98,6 +113,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
|
|
|
98
113
|
(
|
|
99
114
|
exceptions.MapillaryGeoTaggingError,
|
|
100
115
|
exceptions.MapillaryVideoGPSNotFoundError,
|
|
116
|
+
exceptions.MapillaryExiftoolNotFoundError,
|
|
101
117
|
),
|
|
102
118
|
):
|
|
103
119
|
return True
|
|
@@ -106,7 +122,7 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
|
|
|
106
122
|
|
|
107
123
|
|
|
108
124
|
def _filter_images_and_videos(
|
|
109
|
-
|
|
125
|
+
paths: T.Iterable[Path],
|
|
110
126
|
filetypes: set[types.FileType] | None = None,
|
|
111
127
|
) -> tuple[list[Path], list[Path]]:
|
|
112
128
|
image_paths = []
|
|
@@ -121,7 +137,7 @@ def _filter_images_and_videos(
|
|
|
121
137
|
include_images = types.FileType.IMAGE in filetypes
|
|
122
138
|
include_videos = bool(filetypes & ALL_VIDEO_TYPES)
|
|
123
139
|
|
|
124
|
-
for path in
|
|
140
|
+
for path in paths:
|
|
125
141
|
if utils.is_image_file(path):
|
|
126
142
|
if include_images:
|
|
127
143
|
image_paths.append(path)
|
|
@@ -141,158 +157,144 @@ def _ensure_source_path(option: SourceOption) -> Path:
|
|
|
141
157
|
return option.source_path.source_path
|
|
142
158
|
|
|
143
159
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if not image_paths:
|
|
150
|
-
return []
|
|
151
|
-
|
|
160
|
+
def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
|
|
161
|
+
"""
|
|
162
|
+
Build a GeotagImagesFromGeneric object based on the provided option.
|
|
163
|
+
"""
|
|
152
164
|
if option.interpolation is None:
|
|
153
165
|
interpolation = InterpolationOption()
|
|
154
166
|
else:
|
|
155
167
|
interpolation = option.interpolation
|
|
156
168
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
161
|
-
image_paths, num_processes=option.num_processes
|
|
169
|
+
if option.source in [SourceType.EXIF, SourceType.NATIVE]:
|
|
170
|
+
return geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
171
|
+
num_processes=option.num_processes
|
|
162
172
|
)
|
|
163
|
-
return geotag.to_description()
|
|
164
173
|
|
|
165
174
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
|
|
176
|
+
num_processes=option.num_processes
|
|
168
177
|
)
|
|
169
|
-
try:
|
|
170
|
-
return geotag.to_description()
|
|
171
|
-
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
172
|
-
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
173
|
-
return []
|
|
174
178
|
|
|
175
179
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
176
180
|
# This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
|
|
177
181
|
# to work
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
181
188
|
num_processes=option.num_processes,
|
|
182
189
|
)
|
|
183
|
-
return geotag.to_description()
|
|
184
190
|
|
|
185
191
|
elif option.source is SourceType.GPX:
|
|
186
|
-
|
|
187
|
-
image_paths,
|
|
192
|
+
return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
|
|
188
193
|
source_path=_ensure_source_path(option),
|
|
189
194
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
190
195
|
offset_time=interpolation.offset_time,
|
|
191
196
|
num_processes=option.num_processes,
|
|
192
197
|
)
|
|
193
|
-
return geotag.to_description()
|
|
194
198
|
|
|
195
199
|
elif option.source is SourceType.NMEA:
|
|
196
|
-
|
|
197
|
-
image_paths,
|
|
200
|
+
return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
|
|
198
201
|
source_path=_ensure_source_path(option),
|
|
199
202
|
use_gpx_start_time=interpolation.use_gpx_start_time,
|
|
200
203
|
offset_time=interpolation.offset_time,
|
|
201
204
|
num_processes=option.num_processes,
|
|
202
205
|
)
|
|
203
206
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
|
|
208
|
-
image_paths, num_processes=option.num_processes
|
|
209
|
-
)
|
|
210
|
-
return geotag.to_description()
|
|
211
|
-
|
|
212
|
-
elif option.source in [
|
|
213
|
-
SourceType.GOPRO,
|
|
214
|
-
SourceType.BLACKVUE,
|
|
215
|
-
SourceType.CAMM,
|
|
216
|
-
]:
|
|
217
|
-
map_geotag_source_to_filetype: dict[SourceType, FileType] = {
|
|
218
|
-
SourceType.GOPRO: FileType.GOPRO,
|
|
219
|
-
SourceType.BLACKVUE: FileType.BLACKVUE,
|
|
220
|
-
SourceType.CAMM: FileType.CAMM,
|
|
221
|
-
}
|
|
222
|
-
video_paths = utils.find_videos([_ensure_source_path(option)])
|
|
223
|
-
image_samples_by_video_path = utils.find_all_image_samples(
|
|
224
|
-
image_paths, video_paths
|
|
225
|
-
)
|
|
226
|
-
video_paths_with_image_samples = list(image_samples_by_video_path.keys())
|
|
227
|
-
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
228
|
-
video_paths_with_image_samples,
|
|
229
|
-
filetypes={map_geotag_source_to_filetype[option.source]},
|
|
230
|
-
num_processes=option.num_processes,
|
|
231
|
-
).to_description()
|
|
232
|
-
geotag = geotag_images_from_video.GeotagImagesFromVideo(
|
|
233
|
-
image_paths,
|
|
234
|
-
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),
|
|
235
210
|
offset_time=interpolation.offset_time,
|
|
236
211
|
num_processes=option.num_processes,
|
|
237
212
|
)
|
|
238
|
-
return geotag.to_description()
|
|
239
213
|
|
|
240
214
|
else:
|
|
241
215
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
242
216
|
|
|
243
217
|
|
|
244
|
-
def
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
_, video_paths = _filter_images_and_videos(paths, option.filetypes)
|
|
248
|
-
|
|
249
|
-
if not video_paths:
|
|
250
|
-
return []
|
|
251
|
-
|
|
252
|
-
geotag: geotag_from_generic.GeotagVideosFromGeneric
|
|
218
|
+
def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
|
|
219
|
+
"""
|
|
220
|
+
Build a GeotagVideosFromGeneric object based on the provided option.
|
|
253
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
|
+
"""
|
|
254
263
|
if option.source is SourceType.NATIVE:
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
return geotag_videos_from_video.GeotagVideosFromVideo(
|
|
265
|
+
num_processes=option.num_processes, filetypes=option.filetypes
|
|
257
266
|
)
|
|
258
|
-
return geotag.to_description()
|
|
259
267
|
|
|
260
268
|
if option.source is SourceType.EXIFTOOL_RUNTIME:
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
|
|
270
|
+
num_processes=option.num_processes
|
|
263
271
|
)
|
|
264
|
-
try:
|
|
265
|
-
return geotag.to_description()
|
|
266
|
-
except exceptions.MapillaryExiftoolNotFoundError as ex:
|
|
267
|
-
LOG.warning('Skip "%s" because: %s', option.source.value, ex)
|
|
268
|
-
return []
|
|
269
272
|
|
|
270
273
|
elif option.source is SourceType.EXIFTOOL_XML:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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,
|
|
274
280
|
)
|
|
275
|
-
return geotag.to_description()
|
|
276
281
|
|
|
277
282
|
elif option.source is SourceType.GPX:
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
return geotag_videos_from_gpx.GeotagVideosFromGPX(
|
|
284
|
+
source_path=option.source_path, num_processes=option.num_processes
|
|
285
|
+
)
|
|
280
286
|
|
|
281
287
|
elif option.source is SourceType.NMEA:
|
|
282
288
|
# TODO: geotag videos from NMEA
|
|
283
|
-
return
|
|
289
|
+
return None
|
|
284
290
|
|
|
285
291
|
elif option.source is SourceType.EXIF:
|
|
286
292
|
# Legacy image-specific geotag types
|
|
287
|
-
return
|
|
293
|
+
return None
|
|
288
294
|
|
|
289
|
-
elif option.source in [
|
|
290
|
-
SourceType.GOPRO,
|
|
291
|
-
SourceType.BLACKVUE,
|
|
292
|
-
SourceType.CAMM,
|
|
293
|
-
]:
|
|
295
|
+
elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
|
|
294
296
|
# Legacy image-specific geotag types
|
|
295
|
-
return
|
|
297
|
+
return None
|
|
296
298
|
|
|
297
299
|
else:
|
|
298
300
|
raise ValueError(f"Invalid geotag source {option.source}")
|
|
@@ -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]
|