mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0__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 +287 -22
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +17 -8
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- 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 +182 -0
- mapillary_tools/geotag/utils.py +52 -16
- 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 +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +408 -416
- mapillary_tools/upload_api_v4.py +172 -174
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- 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.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
|
@@ -1,109 +1,160 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
4
|
+
import sys
|
|
3
5
|
import typing as T
|
|
4
6
|
import xml.etree.ElementTree as ET
|
|
5
|
-
from multiprocessing import Pool
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
if sys.version_info >= (3, 12):
|
|
10
|
+
from typing import override
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import override
|
|
9
13
|
|
|
10
|
-
from .. import exceptions, exiftool_read, types
|
|
11
|
-
from
|
|
12
|
-
from .
|
|
14
|
+
from .. import constants, exceptions, exiftool_read, types, utils
|
|
15
|
+
from ..exiftool_runner import ExiftoolRunner
|
|
16
|
+
from . import options
|
|
17
|
+
from .base import GeotagImagesFromGeneric
|
|
18
|
+
from .geotag_images_from_video import GeotagImagesFromVideo
|
|
19
|
+
from .geotag_videos_from_exiftool import GeotagVideosFromExifToolXML
|
|
20
|
+
from .image_extractors.exiftool import ImageExifToolExtractor
|
|
13
21
|
|
|
14
22
|
LOG = logging.getLogger(__name__)
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
class
|
|
25
|
+
class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
|
|
18
26
|
def __init__(
|
|
19
|
-
self,
|
|
20
|
-
image_paths: T.Sequence[Path],
|
|
21
|
-
xml_path: Path,
|
|
22
|
-
num_processes: T.Optional[int] = None,
|
|
27
|
+
self, source_path: options.SourcePathOption, num_processes: int | None = None
|
|
23
28
|
):
|
|
24
|
-
self.
|
|
25
|
-
|
|
26
|
-
self.num_processes = num_processes
|
|
27
|
-
super().__init__()
|
|
28
|
-
|
|
29
|
-
@staticmethod
|
|
30
|
-
def geotag_image(element: ET.Element) -> types.ImageMetadataOrError:
|
|
31
|
-
image_path = exiftool_read.find_rdf_description_path(element)
|
|
32
|
-
assert image_path is not None, "must find the path from the element"
|
|
29
|
+
self.source_path = source_path
|
|
30
|
+
super().__init__(num_processes=num_processes)
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# load the image bytes into memory to avoid reading it multiple times
|
|
40
|
-
with image_path.open("rb") as fp:
|
|
41
|
-
image_bytesio = io.BytesIO(fp.read())
|
|
42
|
-
image_bytesio.seek(0, io.SEEK_SET)
|
|
43
|
-
verify_image_exif_write(
|
|
44
|
-
image_metadata,
|
|
45
|
-
image_bytes=image_bytesio.read(),
|
|
46
|
-
)
|
|
47
|
-
except Exception as ex:
|
|
48
|
-
return types.describe_error_metadata(
|
|
49
|
-
ex, image_path, filetype=types.FileType.IMAGE
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
image_bytesio.seek(0, io.SEEK_SET)
|
|
53
|
-
image_metadata.update_md5sum(image_bytesio)
|
|
54
|
-
|
|
55
|
-
return image_metadata
|
|
56
|
-
|
|
57
|
-
def to_description(self) -> T.List[types.ImageMetadataOrError]:
|
|
58
|
-
rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
|
|
59
|
-
[self.xml_path]
|
|
60
|
-
)
|
|
32
|
+
@classmethod
|
|
33
|
+
def build_image_extractors(
|
|
34
|
+
cls, rdf_by_path: dict[str, ET.Element], image_paths: T.Iterable[Path]
|
|
35
|
+
) -> list[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
36
|
+
results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
|
|
61
37
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
if rdf_description is None:
|
|
69
|
-
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
70
|
-
f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
|
|
38
|
+
for path in image_paths:
|
|
39
|
+
rdf = rdf_by_path.get(exiftool_read.canonical_path(path))
|
|
40
|
+
if rdf is None:
|
|
41
|
+
ex = exceptions.MapillaryExifToolXMLNotFoundError(
|
|
42
|
+
"Cannot find the image in the ExifTool XML"
|
|
71
43
|
)
|
|
72
|
-
|
|
44
|
+
results.append(
|
|
73
45
|
types.describe_error_metadata(
|
|
74
|
-
|
|
46
|
+
ex, path, filetype=types.FileType.IMAGE
|
|
75
47
|
)
|
|
76
48
|
)
|
|
77
49
|
else:
|
|
78
|
-
|
|
50
|
+
results.append(ImageExifToolExtractor(path, rdf))
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
def _generate_image_extractors(
|
|
56
|
+
self, image_paths: T.Sequence[Path]
|
|
57
|
+
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
58
|
+
rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
|
|
59
|
+
self.source_path, image_paths
|
|
60
|
+
)
|
|
61
|
+
return self.build_image_extractors(rdf_by_path, image_paths)
|
|
62
|
+
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
65
|
+
@override
|
|
66
|
+
def _generate_image_extractors(
|
|
67
|
+
self, image_paths: T.Sequence[Path]
|
|
68
|
+
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
69
|
+
if constants.EXIFTOOL_PATH is None:
|
|
70
|
+
runner = ExiftoolRunner()
|
|
83
71
|
else:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
image_metadata_or_errors = list(
|
|
100
|
-
tqdm(
|
|
101
|
-
image_metadatas_iter,
|
|
102
|
-
desc="Extracting geotags from ExifTool XML",
|
|
103
|
-
unit="images",
|
|
104
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
105
|
-
total=len(self.image_paths),
|
|
72
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
73
|
+
|
|
74
|
+
LOG.debug(
|
|
75
|
+
"Extracting XML from %d images with ExifTool command: %s",
|
|
76
|
+
len(image_paths),
|
|
77
|
+
" ".join(runner._build_args_read_stdin()),
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
xml = runner.extract_xml(image_paths)
|
|
81
|
+
except FileNotFoundError as ex:
|
|
82
|
+
exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
|
|
83
|
+
return [
|
|
84
|
+
types.describe_error_metadata(
|
|
85
|
+
exiftool_ex, image_path, filetype=types.FileType.IMAGE
|
|
106
86
|
)
|
|
87
|
+
for image_path in image_paths
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
xml_element = ET.fromstring(xml)
|
|
92
|
+
except ET.ParseError as ex:
|
|
93
|
+
LOG.warning(
|
|
94
|
+
"Failed to parse ExifTool XML: %s",
|
|
95
|
+
str(ex),
|
|
96
|
+
exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
107
97
|
)
|
|
98
|
+
rdf_by_path = {}
|
|
99
|
+
else:
|
|
100
|
+
rdf_by_path = exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
101
|
+
xml_element
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return GeotagImagesFromExifToolXML.build_image_extractors(
|
|
105
|
+
rdf_by_path, image_paths
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
source_path: options.SourcePathOption,
|
|
113
|
+
offset_time: float = 0.0,
|
|
114
|
+
num_processes: int | None = None,
|
|
115
|
+
):
|
|
116
|
+
super().__init__(num_processes=num_processes)
|
|
117
|
+
self.source_path = source_path
|
|
118
|
+
self.offset_time = offset_time
|
|
119
|
+
|
|
120
|
+
def geotag_samples(
|
|
121
|
+
self, image_paths: T.Sequence[Path]
|
|
122
|
+
) -> list[types.ImageMetadataOrError]:
|
|
123
|
+
# Find all video paths in self.xml_path
|
|
124
|
+
rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
|
|
125
|
+
self.source_path, image_paths
|
|
126
|
+
)
|
|
127
|
+
video_paths = utils.find_videos(
|
|
128
|
+
[Path(canonical_path) for canonical_path in rdf_by_path.keys()],
|
|
129
|
+
skip_subfolders=True,
|
|
130
|
+
)
|
|
131
|
+
# Find all video paths that have sample images
|
|
132
|
+
samples_by_video = utils.find_all_image_samples(image_paths, video_paths)
|
|
133
|
+
|
|
134
|
+
video_metadata_or_errors = GeotagVideosFromExifToolXML(
|
|
135
|
+
self.source_path, num_processes=self.num_processes
|
|
136
|
+
).to_description(list(samples_by_video.keys()))
|
|
137
|
+
sample_paths = sum(samples_by_video.values(), [])
|
|
138
|
+
sample_metadata_or_errors = GeotagImagesFromVideo(
|
|
139
|
+
video_metadata_or_errors,
|
|
140
|
+
offset_time=self.offset_time,
|
|
141
|
+
num_processes=self.num_processes,
|
|
142
|
+
).to_description(sample_paths)
|
|
143
|
+
|
|
144
|
+
return sample_metadata_or_errors
|
|
145
|
+
|
|
146
|
+
@override
|
|
147
|
+
def to_description(
|
|
148
|
+
self, image_paths: T.Sequence[Path]
|
|
149
|
+
) -> list[types.ImageMetadataOrError]:
|
|
150
|
+
sample_metadata_or_errors = self.geotag_samples(image_paths)
|
|
151
|
+
|
|
152
|
+
sample_paths = set(metadata.filename for metadata in sample_metadata_or_errors)
|
|
153
|
+
|
|
154
|
+
non_sample_paths = [path for path in image_paths if path not in sample_paths]
|
|
155
|
+
|
|
156
|
+
non_sample_metadata_or_errors = GeotagImagesFromExifToolXML(
|
|
157
|
+
self.source_path, num_processes=self.num_processes
|
|
158
|
+
).to_description(non_sample_paths)
|
|
108
159
|
|
|
109
|
-
return
|
|
160
|
+
return sample_metadata_or_errors + non_sample_metadata_or_errors
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
2
4
|
import logging
|
|
5
|
+
import sys
|
|
3
6
|
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
9
|
+
if sys.version_info >= (3, 12):
|
|
10
|
+
from typing import override
|
|
11
|
+
else:
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
7
14
|
from .. import exceptions, geo, types
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
15
|
+
from ..serializer.description import build_capture_time
|
|
16
|
+
from .base import GeotagImagesFromGeneric
|
|
17
|
+
from .geotag_images_from_exif import ImageEXIFExtractor
|
|
10
18
|
|
|
11
19
|
|
|
12
20
|
LOG = logging.getLogger(__name__)
|
|
@@ -15,79 +23,50 @@ LOG = logging.getLogger(__name__)
|
|
|
15
23
|
class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
16
24
|
def __init__(
|
|
17
25
|
self,
|
|
18
|
-
image_paths: T.Sequence[Path],
|
|
19
26
|
points: T.Sequence[geo.Point],
|
|
20
27
|
use_gpx_start_time: bool = False,
|
|
21
28
|
use_image_start_time: bool = False,
|
|
22
29
|
offset_time: float = 0.0,
|
|
23
|
-
num_processes:
|
|
30
|
+
num_processes: int | None = None,
|
|
24
31
|
):
|
|
25
|
-
super().__init__()
|
|
26
|
-
self.image_paths = image_paths
|
|
32
|
+
super().__init__(num_processes=num_processes)
|
|
27
33
|
self.points = points
|
|
28
34
|
self.use_gpx_start_time = use_gpx_start_time
|
|
29
35
|
self.use_image_start_time = use_image_start_time
|
|
30
36
|
self.offset_time = offset_time
|
|
31
|
-
self.num_processes = num_processes
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def geotag_image(image_path: Path) -> types.ImageMetadataOrError:
|
|
35
|
-
return GeotagImagesFromEXIF.geotag_image(image_path, skip_lonlat_error=True)
|
|
36
|
-
|
|
37
|
-
def geotag_multiple_images(
|
|
38
|
-
self, image_paths: T.Sequence[Path]
|
|
39
|
-
) -> T.List[types.ImageMetadataOrError]:
|
|
40
|
-
if self.num_processes is None:
|
|
41
|
-
num_processes = self.num_processes
|
|
42
|
-
disable_multiprocessing = False
|
|
43
|
-
else:
|
|
44
|
-
num_processes = max(self.num_processes, 1)
|
|
45
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
46
|
-
|
|
47
|
-
if disable_multiprocessing:
|
|
48
|
-
return list(map(GeotagImagesFromGPX.geotag_image, image_paths))
|
|
49
|
-
else:
|
|
50
|
-
with Pool(processes=num_processes) as pool:
|
|
51
|
-
return pool.map(GeotagImagesFromGPX.geotag_image, image_paths)
|
|
52
37
|
|
|
53
38
|
def _interpolate_image_metadata_along(
|
|
54
39
|
self,
|
|
55
40
|
image_metadata: types.ImageMetadata,
|
|
56
41
|
sorted_points: T.Sequence[geo.Point],
|
|
57
|
-
) -> types.
|
|
42
|
+
) -> types.ImageMetadata:
|
|
58
43
|
assert sorted_points, "must have at least one point"
|
|
59
44
|
|
|
60
45
|
if image_metadata.time < sorted_points[0].time:
|
|
61
46
|
delta = sorted_points[0].time - image_metadata.time
|
|
62
|
-
gpx_start_time =
|
|
63
|
-
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)
|
|
64
49
|
# with the tolerance of 1ms
|
|
65
50
|
if 0.001 < delta:
|
|
66
|
-
|
|
51
|
+
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
67
52
|
f"The image date time is {round(delta, 3)} seconds behind the GPX start point",
|
|
68
|
-
image_time=
|
|
53
|
+
image_time=build_capture_time(image_metadata.time),
|
|
69
54
|
gpx_start_time=gpx_start_time,
|
|
70
55
|
gpx_end_time=gpx_end_time,
|
|
71
56
|
)
|
|
72
|
-
return types.describe_error_metadata(
|
|
73
|
-
exc, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
74
|
-
)
|
|
75
57
|
|
|
76
58
|
if sorted_points[-1].time < image_metadata.time:
|
|
77
59
|
delta = image_metadata.time - sorted_points[-1].time
|
|
78
|
-
gpx_start_time =
|
|
79
|
-
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)
|
|
80
62
|
# with the tolerance of 1ms
|
|
81
63
|
if 0.001 < delta:
|
|
82
|
-
|
|
64
|
+
raise exceptions.MapillaryOutsideGPXTrackError(
|
|
83
65
|
f"The image time is {round(delta, 3)} seconds beyond the GPX end point",
|
|
84
|
-
image_time=
|
|
66
|
+
image_time=build_capture_time(image_metadata.time),
|
|
85
67
|
gpx_start_time=gpx_start_time,
|
|
86
68
|
gpx_end_time=gpx_end_time,
|
|
87
69
|
)
|
|
88
|
-
return types.describe_error_metadata(
|
|
89
|
-
exc, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
90
|
-
)
|
|
91
70
|
|
|
92
71
|
interpolated = geo.interpolate(sorted_points, image_metadata.time)
|
|
93
72
|
|
|
@@ -100,34 +79,30 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
100
79
|
time=interpolated.time,
|
|
101
80
|
)
|
|
102
81
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
assert len(self.image_paths) == len(metadatas)
|
|
117
|
-
return metadatas
|
|
82
|
+
@override
|
|
83
|
+
def _generate_image_extractors(
|
|
84
|
+
self, image_paths: T.Sequence[Path]
|
|
85
|
+
) -> T.Sequence[ImageEXIFExtractor]:
|
|
86
|
+
return [
|
|
87
|
+
ImageEXIFExtractor(path, skip_lonlat_error=True) for path in image_paths
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
@override
|
|
91
|
+
def to_description(
|
|
92
|
+
self, image_paths: T.Sequence[Path]
|
|
93
|
+
) -> list[types.ImageMetadataOrError]:
|
|
94
|
+
final_metadatas: list[types.ImageMetadataOrError] = []
|
|
118
95
|
|
|
119
|
-
image_metadata_or_errors =
|
|
96
|
+
image_metadata_or_errors = super().to_description(image_paths)
|
|
120
97
|
|
|
121
|
-
image_metadatas =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
else:
|
|
126
|
-
image_metadatas.append(metadata_or_error)
|
|
98
|
+
image_metadatas, error_metadatas = types.separate_errors(
|
|
99
|
+
image_metadata_or_errors
|
|
100
|
+
)
|
|
101
|
+
final_metadatas.extend(error_metadatas)
|
|
127
102
|
|
|
128
103
|
if not image_metadatas:
|
|
129
|
-
assert len(
|
|
130
|
-
return
|
|
104
|
+
assert len(image_paths) == len(final_metadatas)
|
|
105
|
+
return final_metadatas
|
|
131
106
|
|
|
132
107
|
# Do not use point itself for comparison because point.angle or point.alt could be None
|
|
133
108
|
# when you compare nonnull value with None, it will throw
|
|
@@ -162,64 +137,25 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
|
|
|
162
137
|
LOG.debug("GPX start time delta: %s", time_delta)
|
|
163
138
|
image_time_offset += time_delta
|
|
164
139
|
|
|
165
|
-
|
|
140
|
+
if image_time_offset:
|
|
141
|
+
LOG.debug("Final time offset for interpolation: %s", image_time_offset)
|
|
142
|
+
for image_metadata in sorted_image_metadatas:
|
|
143
|
+
# TODO: this time modification seems to affect final capture times
|
|
144
|
+
image_metadata.time += image_time_offset
|
|
166
145
|
|
|
167
146
|
for image_metadata in sorted_image_metadatas:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert len(self.image_paths) == len(metadatas)
|
|
174
|
-
return metadatas
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class GeotagImagesFromGPXWithProgress(GeotagImagesFromGPX):
|
|
178
|
-
def __init__(
|
|
179
|
-
self,
|
|
180
|
-
image_paths: T.Sequence[Path],
|
|
181
|
-
points: T.Sequence[geo.Point],
|
|
182
|
-
use_gpx_start_time: bool = False,
|
|
183
|
-
use_image_start_time: bool = False,
|
|
184
|
-
offset_time: float = 0.0,
|
|
185
|
-
num_processes: T.Optional[int] = None,
|
|
186
|
-
progress_bar=None,
|
|
187
|
-
) -> None:
|
|
188
|
-
super().__init__(
|
|
189
|
-
image_paths,
|
|
190
|
-
points,
|
|
191
|
-
use_gpx_start_time=use_gpx_start_time,
|
|
192
|
-
use_image_start_time=use_image_start_time,
|
|
193
|
-
offset_time=offset_time,
|
|
194
|
-
num_processes=num_processes,
|
|
195
|
-
)
|
|
196
|
-
self._progress_bar = progress_bar
|
|
197
|
-
|
|
198
|
-
def geotag_multiple_images(
|
|
199
|
-
self, image_paths: T.Sequence[Path]
|
|
200
|
-
) -> T.List[types.ImageMetadataOrError]:
|
|
201
|
-
if self._progress_bar is None:
|
|
202
|
-
return super().geotag_multiple_images(image_paths)
|
|
203
|
-
|
|
204
|
-
if self.num_processes is None:
|
|
205
|
-
num_processes = self.num_processes
|
|
206
|
-
disable_multiprocessing = False
|
|
207
|
-
else:
|
|
208
|
-
num_processes = max(self.num_processes, 1)
|
|
209
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
210
|
-
|
|
211
|
-
output = []
|
|
212
|
-
with Pool(processes=num_processes) as pool:
|
|
213
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
214
|
-
if disable_multiprocessing:
|
|
215
|
-
image_metadatas_iter = map(
|
|
216
|
-
GeotagImagesFromGPX.geotag_image, image_paths
|
|
147
|
+
try:
|
|
148
|
+
final_metadatas.append(
|
|
149
|
+
self._interpolate_image_metadata_along(
|
|
150
|
+
image_metadata, sorted_points
|
|
151
|
+
)
|
|
217
152
|
)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
153
|
+
except exceptions.MapillaryOutsideGPXTrackError as ex:
|
|
154
|
+
error_metadata = types.describe_error_metadata(
|
|
155
|
+
ex, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
221
156
|
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
157
|
+
final_metadatas.append(error_metadata)
|
|
158
|
+
|
|
159
|
+
assert len(image_paths) == len(final_metadatas)
|
|
160
|
+
|
|
161
|
+
return final_metadatas
|
|
@@ -1,32 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
|
-
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from .. import exif_read, geo, types
|
|
11
|
-
from .geotag_from_generic import GeotagImagesFromGeneric
|
|
12
|
-
from .geotag_images_from_gpx import GeotagImagesFromGPXWithProgress
|
|
6
|
+
from . import utils
|
|
7
|
+
from .geotag_images_from_gpx import GeotagImagesFromGPX
|
|
13
8
|
|
|
14
9
|
|
|
15
10
|
LOG = logging.getLogger(__name__)
|
|
16
11
|
|
|
17
12
|
|
|
18
|
-
class GeotagImagesFromGPXFile(
|
|
13
|
+
class GeotagImagesFromGPXFile(GeotagImagesFromGPX):
|
|
19
14
|
def __init__(
|
|
20
15
|
self,
|
|
21
|
-
image_paths: T.Sequence[Path],
|
|
22
16
|
source_path: Path,
|
|
23
17
|
use_gpx_start_time: bool = False,
|
|
24
18
|
offset_time: float = 0.0,
|
|
25
|
-
num_processes:
|
|
19
|
+
num_processes: int | None = None,
|
|
26
20
|
):
|
|
27
|
-
super().__init__()
|
|
28
21
|
try:
|
|
29
|
-
tracks = parse_gpx(source_path)
|
|
22
|
+
tracks = utils.parse_gpx(source_path)
|
|
30
23
|
except Exception as ex:
|
|
31
24
|
raise RuntimeError(
|
|
32
25
|
f"Error parsing GPX {source_path}: {ex.__class__.__name__}: {ex}"
|
|
@@ -38,116 +31,10 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGeneric):
|
|
|
38
31
|
len(tracks),
|
|
39
32
|
source_path,
|
|
40
33
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@staticmethod
|
|
49
|
-
def _extract_image_metadata(
|
|
50
|
-
image_metadata: types.ImageMetadata,
|
|
51
|
-
) -> types.ImageMetadataOrError:
|
|
52
|
-
try:
|
|
53
|
-
exif = exif_read.ExifRead(image_metadata.filename)
|
|
54
|
-
orientation = exif.extract_orientation()
|
|
55
|
-
make = exif.extract_make()
|
|
56
|
-
model = exif.extract_model()
|
|
57
|
-
except Exception as ex:
|
|
58
|
-
return types.describe_error_metadata(
|
|
59
|
-
ex, image_metadata.filename, filetype=types.FileType.IMAGE
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return dataclasses.replace(
|
|
63
|
-
image_metadata,
|
|
64
|
-
MAPOrientation=orientation,
|
|
65
|
-
MAPDeviceMake=make,
|
|
66
|
-
MAPDeviceModel=model,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
def to_description(self) -> T.List[types.ImageMetadataOrError]:
|
|
70
|
-
with tqdm(
|
|
71
|
-
total=len(self.image_paths),
|
|
72
|
-
desc="Interpolating",
|
|
73
|
-
unit="images",
|
|
74
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
75
|
-
) as pbar:
|
|
76
|
-
geotag = GeotagImagesFromGPXWithProgress(
|
|
77
|
-
self.image_paths,
|
|
78
|
-
self.points,
|
|
79
|
-
use_gpx_start_time=self.use_gpx_start_time,
|
|
80
|
-
offset_time=self.offset_time,
|
|
81
|
-
progress_bar=pbar,
|
|
82
|
-
)
|
|
83
|
-
image_metadata_or_errors = geotag.to_description()
|
|
84
|
-
|
|
85
|
-
image_metadatas: T.List[types.ImageMetadata] = []
|
|
86
|
-
error_metadatas: T.List[types.ErrorMetadata] = []
|
|
87
|
-
for metadata in image_metadata_or_errors:
|
|
88
|
-
if isinstance(metadata, types.ErrorMetadata):
|
|
89
|
-
error_metadatas.append(metadata)
|
|
90
|
-
else:
|
|
91
|
-
image_metadatas.append(metadata)
|
|
92
|
-
|
|
93
|
-
if self.num_processes is None:
|
|
94
|
-
num_processes = self.num_processes
|
|
95
|
-
disable_multiprocessing = False
|
|
96
|
-
else:
|
|
97
|
-
num_processes = max(self.num_processes, 1)
|
|
98
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
99
|
-
|
|
100
|
-
with Pool(processes=num_processes) as pool:
|
|
101
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
102
|
-
if disable_multiprocessing:
|
|
103
|
-
image_metadatas_iter = map(
|
|
104
|
-
GeotagImagesFromGPXFile._extract_image_metadata, image_metadatas
|
|
105
|
-
)
|
|
106
|
-
else:
|
|
107
|
-
# Do not pass error metadatas where the error object can not be pickled for multiprocessing to work
|
|
108
|
-
# Otherwise we get:
|
|
109
|
-
# TypeError: __init__() missing 3 required positional arguments: 'image_time', 'gpx_start_time', and 'gpx_end_time'
|
|
110
|
-
# See https://stackoverflow.com/a/61432070
|
|
111
|
-
image_metadatas_iter = pool.imap(
|
|
112
|
-
GeotagImagesFromGPXFile._extract_image_metadata, image_metadatas
|
|
113
|
-
)
|
|
114
|
-
image_metadata_or_errors = list(
|
|
115
|
-
tqdm(
|
|
116
|
-
image_metadatas_iter,
|
|
117
|
-
desc="Processing",
|
|
118
|
-
unit="images",
|
|
119
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
120
|
-
)
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
T.cast(T.List[types.ImageMetadataOrError], error_metadatas)
|
|
125
|
-
+ image_metadata_or_errors
|
|
34
|
+
points = sum(tracks, [])
|
|
35
|
+
super().__init__(
|
|
36
|
+
points,
|
|
37
|
+
use_gpx_start_time=use_gpx_start_time,
|
|
38
|
+
offset_time=offset_time,
|
|
39
|
+
num_processes=num_processes,
|
|
126
40
|
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Track = T.List[geo.Point]
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def parse_gpx(gpx_file: Path) -> T.List[Track]:
|
|
133
|
-
with gpx_file.open("r") as f:
|
|
134
|
-
gpx = gpxpy.parse(f)
|
|
135
|
-
|
|
136
|
-
tracks: T.List[Track] = []
|
|
137
|
-
|
|
138
|
-
for track in gpx.tracks:
|
|
139
|
-
for segment in track.segments:
|
|
140
|
-
tracks.append([])
|
|
141
|
-
for point in segment.points:
|
|
142
|
-
if point.time is not None:
|
|
143
|
-
tracks[-1].append(
|
|
144
|
-
geo.Point(
|
|
145
|
-
time=geo.as_unix_time(point.time),
|
|
146
|
-
lat=point.latitude,
|
|
147
|
-
lon=point.longitude,
|
|
148
|
-
alt=point.elevation,
|
|
149
|
-
angle=None,
|
|
150
|
-
)
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
return tracks
|