mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a1__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 +235 -14
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +11 -4
- 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 +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,22 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import abc
|
|
4
|
+
import logging
|
|
2
5
|
import typing as T
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
|
|
10
|
+
from .. import exceptions, types, utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenericImageExtractor(abc.ABC):
|
|
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)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
|
|
32
|
+
"""
|
|
33
|
+
Extracts metadata from a list of image files with multiprocessing.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, image_paths: T.Sequence[Path], num_processes: int | None = None
|
|
38
|
+
) -> None:
|
|
39
|
+
self.image_paths = image_paths
|
|
40
|
+
self.num_processes = num_processes
|
|
41
|
+
|
|
42
|
+
def to_description(self) -> list[types.ImageMetadataOrError]:
|
|
43
|
+
extractor_or_errors = self._generate_image_extractors()
|
|
44
|
+
|
|
45
|
+
assert len(extractor_or_errors) == len(self.image_paths)
|
|
46
|
+
|
|
47
|
+
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
3
48
|
|
|
4
|
-
|
|
49
|
+
map_results = utils.mp_map_maybe(
|
|
50
|
+
self.run_extraction,
|
|
51
|
+
extractors,
|
|
52
|
+
num_processes=self.num_processes,
|
|
53
|
+
)
|
|
5
54
|
|
|
55
|
+
results = list(
|
|
56
|
+
tqdm(
|
|
57
|
+
map_results,
|
|
58
|
+
desc="Extracting images",
|
|
59
|
+
unit="images",
|
|
60
|
+
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
61
|
+
total=len(extractors),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
6
64
|
|
|
7
|
-
|
|
8
|
-
def __init__(self) -> None:
|
|
9
|
-
pass
|
|
65
|
+
return results + error_metadatas
|
|
10
66
|
|
|
11
|
-
|
|
12
|
-
|
|
67
|
+
def _generate_image_extractors(
|
|
68
|
+
self,
|
|
69
|
+
) -> T.Sequence[TImageExtractor | types.ErrorMetadata]:
|
|
13
70
|
raise NotImplementedError
|
|
14
71
|
|
|
72
|
+
# This method is passed to multiprocessing
|
|
73
|
+
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
74
|
+
@classmethod
|
|
75
|
+
def run_extraction(cls, extractor: TImageExtractor) -> types.ImageMetadataOrError:
|
|
76
|
+
image_path = extractor.image_path
|
|
15
77
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
78
|
+
try:
|
|
79
|
+
return extractor.extract()
|
|
80
|
+
except exceptions.MapillaryDescriptionError as ex:
|
|
81
|
+
return types.describe_error_metadata(
|
|
82
|
+
ex, image_path, filetype=types.FileType.IMAGE
|
|
83
|
+
)
|
|
84
|
+
except Exception as ex:
|
|
85
|
+
LOG.exception("Unexpected error extracting metadata from %s", image_path)
|
|
86
|
+
return types.describe_error_metadata(
|
|
87
|
+
ex, image_path, filetype=types.FileType.IMAGE
|
|
88
|
+
)
|
|
19
89
|
|
|
20
|
-
|
|
21
|
-
|
|
90
|
+
|
|
91
|
+
class GenericVideoExtractor(abc.ABC):
|
|
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:
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
TVideoExtractor = T.TypeVar("TVideoExtractor", bound=GenericVideoExtractor)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
|
|
107
|
+
"""
|
|
108
|
+
Extracts metadata from a list of video files with multiprocessing.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self, video_paths: T.Sequence[Path], num_processes: int | None = None
|
|
113
|
+
) -> None:
|
|
114
|
+
self.video_paths = video_paths
|
|
115
|
+
self.num_processes = num_processes
|
|
116
|
+
|
|
117
|
+
def to_description(self) -> list[types.VideoMetadataOrError]:
|
|
118
|
+
extractor_or_errors = self._generate_video_extractors()
|
|
119
|
+
|
|
120
|
+
assert len(extractor_or_errors) == len(self.video_paths)
|
|
121
|
+
|
|
122
|
+
extractors, error_metadatas = types.separate_errors(extractor_or_errors)
|
|
123
|
+
|
|
124
|
+
map_results = utils.mp_map_maybe(
|
|
125
|
+
self.run_extraction,
|
|
126
|
+
extractors,
|
|
127
|
+
num_processes=self.num_processes,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
results = list(
|
|
131
|
+
tqdm(
|
|
132
|
+
map_results,
|
|
133
|
+
desc="Extracting videos",
|
|
134
|
+
unit="videos",
|
|
135
|
+
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
136
|
+
total=len(extractors),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return results + error_metadatas
|
|
141
|
+
|
|
142
|
+
def _generate_video_extractors(
|
|
143
|
+
self,
|
|
144
|
+
) -> T.Sequence[TVideoExtractor | types.ErrorMetadata]:
|
|
22
145
|
raise NotImplementedError
|
|
146
|
+
|
|
147
|
+
# This method is passed to multiprocessing
|
|
148
|
+
# so it has to be classmethod or staticmethod to avoid pickling the instance
|
|
149
|
+
@classmethod
|
|
150
|
+
def run_extraction(cls, extractor: TVideoExtractor) -> types.VideoMetadataOrError:
|
|
151
|
+
video_path = extractor.video_path
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
return extractor.extract()
|
|
155
|
+
except exceptions.MapillaryDescriptionError as ex:
|
|
156
|
+
return types.describe_error_metadata(
|
|
157
|
+
ex, video_path, filetype=types.FileType.VIDEO
|
|
158
|
+
)
|
|
159
|
+
except Exception as ex:
|
|
160
|
+
LOG.exception("Unexpected error extracting metadata from %s", video_path)
|
|
161
|
+
return types.describe_error_metadata(
|
|
162
|
+
ex, video_path, filetype=types.FileType.VIDEO
|
|
163
|
+
)
|
|
@@ -1,141 +1,60 @@
|
|
|
1
|
-
import
|
|
1
|
+
import contextlib
|
|
2
2
|
import logging
|
|
3
3
|
import typing as T
|
|
4
|
-
from multiprocessing import Pool
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from .. import exceptions, exif_write, geo, types, utils
|
|
6
|
+
from .. import exceptions, geo, types, utils
|
|
10
7
|
from ..exif_read import ExifRead, ExifReadABC
|
|
11
|
-
from .geotag_from_generic import GeotagImagesFromGeneric
|
|
8
|
+
from .geotag_from_generic import GenericImageExtractor, GeotagImagesFromGeneric
|
|
12
9
|
|
|
13
10
|
LOG = logging.getLogger(__name__)
|
|
14
11
|
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self, image_paths: T.Sequence[Path], num_processes: T.Optional[int] = None
|
|
40
|
-
):
|
|
41
|
-
self.image_paths = image_paths
|
|
42
|
-
self.num_processes = num_processes
|
|
43
|
-
super().__init__()
|
|
44
|
-
|
|
45
|
-
@staticmethod
|
|
46
|
-
def build_image_metadata(
|
|
47
|
-
image_path: Path, exif: ExifReadABC, skip_lonlat_error: bool = False
|
|
48
|
-
) -> types.ImageMetadata:
|
|
49
|
-
lonlat = exif.extract_lon_lat()
|
|
50
|
-
if lonlat is None:
|
|
51
|
-
if not skip_lonlat_error:
|
|
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
|
|
17
|
+
|
|
18
|
+
@contextlib.contextmanager
|
|
19
|
+
def _exif_context(self) -> T.Generator[ExifReadABC, None, None]:
|
|
20
|
+
with self.image_path.open("rb") as fp:
|
|
21
|
+
yield ExifRead(fp)
|
|
22
|
+
|
|
23
|
+
def extract(self) -> types.ImageMetadata:
|
|
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:
|
|
52
36
|
raise exceptions.MapillaryGeoTaggingError(
|
|
53
|
-
"Unable to extract
|
|
37
|
+
"Unable to extract timestamp from the image"
|
|
54
38
|
)
|
|
55
|
-
lonlat = (0.0, 0.0)
|
|
56
|
-
lon, lat = lonlat
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(),
|
|
62
53
|
)
|
|
63
54
|
|
|
64
|
-
image_metadata = types.ImageMetadata(
|
|
65
|
-
filename=image_path,
|
|
66
|
-
md5sum=None,
|
|
67
|
-
filesize=utils.get_file_size(image_path),
|
|
68
|
-
time=geo.as_unix_time(capture_time),
|
|
69
|
-
lat=lat,
|
|
70
|
-
lon=lon,
|
|
71
|
-
alt=exif.extract_altitude(),
|
|
72
|
-
angle=exif.extract_direction(),
|
|
73
|
-
width=exif.extract_width(),
|
|
74
|
-
height=exif.extract_height(),
|
|
75
|
-
MAPOrientation=exif.extract_orientation(),
|
|
76
|
-
MAPDeviceMake=exif.extract_make(),
|
|
77
|
-
MAPDeviceModel=exif.extract_model(),
|
|
78
|
-
)
|
|
79
|
-
|
|
80
55
|
return image_metadata
|
|
81
56
|
|
|
82
|
-
@staticmethod
|
|
83
|
-
def geotag_image(
|
|
84
|
-
image_path: Path, skip_lonlat_error: bool = False
|
|
85
|
-
) -> types.ImageMetadataOrError:
|
|
86
|
-
try:
|
|
87
|
-
# load the image bytes into memory to avoid reading it multiple times
|
|
88
|
-
with image_path.open("rb") as fp:
|
|
89
|
-
image_bytesio = io.BytesIO(fp.read())
|
|
90
|
-
|
|
91
|
-
image_bytesio.seek(0, io.SEEK_SET)
|
|
92
|
-
exif = ExifRead(image_bytesio)
|
|
93
57
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
image_bytesio.seek(0, io.SEEK_SET)
|
|
99
|
-
verify_image_exif_write(
|
|
100
|
-
image_metadata,
|
|
101
|
-
image_bytes=image_bytesio.read(),
|
|
102
|
-
)
|
|
103
|
-
except Exception as ex:
|
|
104
|
-
return types.describe_error_metadata(
|
|
105
|
-
ex, image_path, filetype=types.FileType.IMAGE
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
image_bytesio.seek(0, io.SEEK_SET)
|
|
109
|
-
image_metadata.update_md5sum(image_bytesio)
|
|
110
|
-
|
|
111
|
-
return image_metadata
|
|
112
|
-
|
|
113
|
-
def to_description(self) -> T.List[types.ImageMetadataOrError]:
|
|
114
|
-
if self.num_processes is None:
|
|
115
|
-
num_processes = self.num_processes
|
|
116
|
-
disable_multiprocessing = False
|
|
117
|
-
else:
|
|
118
|
-
num_processes = max(self.num_processes, 1)
|
|
119
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
120
|
-
|
|
121
|
-
with Pool(processes=num_processes) as pool:
|
|
122
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
123
|
-
if disable_multiprocessing:
|
|
124
|
-
image_metadatas_iter = map(
|
|
125
|
-
GeotagImagesFromEXIF.geotag_image,
|
|
126
|
-
self.image_paths,
|
|
127
|
-
)
|
|
128
|
-
else:
|
|
129
|
-
image_metadatas_iter = pool.imap(
|
|
130
|
-
GeotagImagesFromEXIF.geotag_image,
|
|
131
|
-
self.image_paths,
|
|
132
|
-
)
|
|
133
|
-
return list(
|
|
134
|
-
tqdm(
|
|
135
|
-
image_metadatas_iter,
|
|
136
|
-
desc="Extracting geotags from images",
|
|
137
|
-
unit="images",
|
|
138
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
139
|
-
total=len(self.image_paths),
|
|
140
|
-
)
|
|
141
|
-
)
|
|
58
|
+
class GeotagImagesFromEXIF(GeotagImagesFromGeneric):
|
|
59
|
+
def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
|
|
60
|
+
return [ImageEXIFExtractor(path) for path in self.image_paths]
|
|
@@ -1,66 +1,91 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
2
4
|
import logging
|
|
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
|
-
from
|
|
9
|
-
|
|
10
|
-
from .. import exceptions, exiftool_read, types
|
|
9
|
+
from .. import constants, exceptions, exiftool_read, types
|
|
10
|
+
from ..exiftool_runner import ExiftoolRunner
|
|
11
11
|
from .geotag_from_generic import GeotagImagesFromGeneric
|
|
12
|
-
from .geotag_images_from_exif import
|
|
12
|
+
from .geotag_images_from_exif import ImageEXIFExtractor
|
|
13
13
|
|
|
14
14
|
LOG = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class ImageExifToolExtractor(ImageEXIFExtractor):
|
|
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
|
+
|
|
17
27
|
class GeotagImagesFromExifTool(GeotagImagesFromGeneric):
|
|
18
28
|
def __init__(
|
|
19
29
|
self,
|
|
20
30
|
image_paths: T.Sequence[Path],
|
|
21
31
|
xml_path: Path,
|
|
22
|
-
num_processes:
|
|
32
|
+
num_processes: int | None = None,
|
|
23
33
|
):
|
|
24
|
-
self.image_paths = image_paths
|
|
25
34
|
self.xml_path = xml_path
|
|
26
|
-
|
|
27
|
-
super().__init__()
|
|
35
|
+
super().__init__(image_paths=image_paths, num_processes=num_processes)
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
def _generate_image_extractors(
|
|
38
|
+
self,
|
|
39
|
+
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
40
|
+
rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
|
|
41
|
+
[self.xml_path]
|
|
42
|
+
)
|
|
33
43
|
|
|
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
|
|
44
|
+
results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
|
|
45
|
+
|
|
46
|
+
for path in self.image_paths:
|
|
47
|
+
rdf_description = rdf_description_by_path.get(
|
|
48
|
+
exiftool_read.canonical_path(path)
|
|
50
49
|
)
|
|
50
|
+
if rdf_description is None:
|
|
51
|
+
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
52
|
+
f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
|
|
53
|
+
)
|
|
54
|
+
results.append(
|
|
55
|
+
types.describe_error_metadata(
|
|
56
|
+
exc, path, filetype=types.FileType.IMAGE
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
results.append(ImageExifToolExtractor(path, rdf_description))
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
image_metadata.update_md5sum(image_bytesio)
|
|
62
|
+
return results
|
|
54
63
|
|
|
55
|
-
return image_metadata
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
|
|
66
|
+
def _generate_image_extractors(
|
|
67
|
+
self,
|
|
68
|
+
) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
|
|
69
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
70
|
+
|
|
71
|
+
LOG.debug(
|
|
72
|
+
"Extracting XML from %d images with exiftool command: %s",
|
|
73
|
+
len(self.image_paths),
|
|
74
|
+
" ".join(runner._build_args_read_stdin()),
|
|
60
75
|
)
|
|
76
|
+
try:
|
|
77
|
+
xml = runner.extract_xml(self.image_paths)
|
|
78
|
+
except FileNotFoundError as ex:
|
|
79
|
+
raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
|
|
80
|
+
|
|
81
|
+
rdf_description_by_path = (
|
|
82
|
+
exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
83
|
+
ET.fromstring(xml)
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
|
|
61
88
|
|
|
62
|
-
error_metadatas: T.List[types.ErrorMetadata] = []
|
|
63
|
-
rdf_descriptions: T.List[ET.Element] = []
|
|
64
89
|
for path in self.image_paths:
|
|
65
90
|
rdf_description = rdf_description_by_path.get(
|
|
66
91
|
exiftool_read.canonical_path(path)
|
|
@@ -69,41 +94,12 @@ class GeotagImagesFromExifTool(GeotagImagesFromGeneric):
|
|
|
69
94
|
exc = exceptions.MapillaryEXIFNotFoundError(
|
|
70
95
|
f"The {exiftool_read._DESCRIPTION_TAG} XML element for the image not found"
|
|
71
96
|
)
|
|
72
|
-
|
|
97
|
+
results.append(
|
|
73
98
|
types.describe_error_metadata(
|
|
74
99
|
exc, path, filetype=types.FileType.IMAGE
|
|
75
100
|
)
|
|
76
101
|
)
|
|
77
102
|
else:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if self.num_processes is None:
|
|
81
|
-
num_processes = self.num_processes
|
|
82
|
-
disable_multiprocessing = False
|
|
83
|
-
else:
|
|
84
|
-
num_processes = max(self.num_processes, 1)
|
|
85
|
-
disable_multiprocessing = self.num_processes <= 0
|
|
86
|
-
|
|
87
|
-
with Pool(processes=num_processes) as pool:
|
|
88
|
-
image_metadatas_iter: T.Iterator[types.ImageMetadataOrError]
|
|
89
|
-
if disable_multiprocessing:
|
|
90
|
-
image_metadatas_iter = map(
|
|
91
|
-
GeotagImagesFromExifTool.geotag_image,
|
|
92
|
-
rdf_descriptions,
|
|
93
|
-
)
|
|
94
|
-
else:
|
|
95
|
-
image_metadatas_iter = pool.imap(
|
|
96
|
-
GeotagImagesFromExifTool.geotag_image,
|
|
97
|
-
rdf_descriptions,
|
|
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),
|
|
106
|
-
)
|
|
107
|
-
)
|
|
103
|
+
results.append(ImageExifToolExtractor(path, rdf_description))
|
|
108
104
|
|
|
109
|
-
return
|
|
105
|
+
return results
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import typing as T
|
|
3
5
|
from pathlib import Path
|
|
@@ -20,28 +22,14 @@ class GeotagImagesFromExifToolBothImageAndVideo(GeotagImagesFromGeneric):
|
|
|
20
22
|
image_paths: T.Sequence[Path],
|
|
21
23
|
xml_path: Path,
|
|
22
24
|
offset_time: float = 0.0,
|
|
23
|
-
num_processes:
|
|
25
|
+
num_processes: int | None = None,
|
|
24
26
|
):
|
|
25
|
-
|
|
27
|
+
super().__init__(image_paths, num_processes=num_processes)
|
|
26
28
|
self.xml_path = xml_path
|
|
27
29
|
self.offset_time = offset_time
|
|
28
|
-
self.num_processes = num_processes
|
|
29
|
-
super().__init__()
|
|
30
|
-
|
|
31
|
-
def to_description(self) -> T.List[types.ImageMetadataOrError]:
|
|
32
|
-
# will return this list
|
|
33
|
-
final_image_metadatas: T.List[types.ImageMetadataOrError] = []
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
geotag_images_from_exiftool.GeotagImagesFromExifTool(
|
|
38
|
-
self.image_paths,
|
|
39
|
-
self.xml_path,
|
|
40
|
-
num_processes=self.num_processes,
|
|
41
|
-
).to_description()
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
# find all video paths in self.xml_path
|
|
31
|
+
def geotag_samples(self) -> list[types.ImageMetadataOrError]:
|
|
32
|
+
# Find all video paths in self.xml_path
|
|
45
33
|
rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
|
|
46
34
|
[self.xml_path]
|
|
47
35
|
)
|
|
@@ -49,45 +37,41 @@ class GeotagImagesFromExifToolBothImageAndVideo(GeotagImagesFromGeneric):
|
|
|
49
37
|
[Path(pathstr) for pathstr in rdf_description_by_path.keys()],
|
|
50
38
|
skip_subfolders=True,
|
|
51
39
|
)
|
|
40
|
+
# Find all video paths that have sample images
|
|
41
|
+
samples_by_video = utils.find_all_image_samples(self.image_paths, video_paths)
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
error_metadata_by_image_path = {}
|
|
55
|
-
for image_metadata in image_metadatas_from_exiftool:
|
|
56
|
-
if isinstance(image_metadata, types.ErrorMetadata):
|
|
57
|
-
error_metadata_by_image_path[image_metadata.filename] = image_metadata
|
|
58
|
-
else:
|
|
59
|
-
final_image_metadatas.append(image_metadata)
|
|
60
|
-
|
|
61
|
-
maybe_image_samples = list(error_metadata_by_image_path.keys())
|
|
62
|
-
|
|
63
|
-
# find all video paths that have sample images
|
|
64
|
-
video_paths_with_image_samples = list(
|
|
65
|
-
utils.find_all_image_samples(maybe_image_samples, video_paths).keys()
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
video_metadatas = (
|
|
43
|
+
video_metadata_or_errors = (
|
|
69
44
|
geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
|
|
70
|
-
|
|
45
|
+
list(samples_by_video.keys()),
|
|
71
46
|
self.xml_path,
|
|
72
47
|
num_processes=self.num_processes,
|
|
73
48
|
).to_description()
|
|
74
49
|
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
sample_paths = sum(samples_by_video.values(), [])
|
|
51
|
+
sample_metadata_or_errors = geotag_images_from_video.GeotagImagesFromVideo(
|
|
52
|
+
sample_paths,
|
|
53
|
+
video_metadata_or_errors,
|
|
79
54
|
offset_time=self.offset_time,
|
|
80
55
|
num_processes=self.num_processes,
|
|
81
56
|
).to_description()
|
|
82
|
-
final_image_metadatas.extend(image_metadatas_from_video)
|
|
83
57
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
return sample_metadata_or_errors
|
|
59
|
+
|
|
60
|
+
def to_description(self) -> list[types.ImageMetadataOrError]:
|
|
61
|
+
sample_metadata_or_errors = self.geotag_samples()
|
|
62
|
+
|
|
63
|
+
sample_paths = set(metadata.filename for metadata in sample_metadata_or_errors)
|
|
64
|
+
|
|
65
|
+
non_sample_paths = [
|
|
66
|
+
path for path in self.image_paths if path not in sample_paths
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
non_sample_metadata_or_errors = (
|
|
70
|
+
geotag_images_from_exiftool.GeotagImagesFromExifTool(
|
|
71
|
+
non_sample_paths,
|
|
72
|
+
self.xml_path,
|
|
73
|
+
num_processes=self.num_processes,
|
|
74
|
+
).to_description()
|
|
87
75
|
)
|
|
88
|
-
for path, error_metadata in error_metadata_by_image_path.items():
|
|
89
|
-
if path not in actual_image_sample_paths:
|
|
90
|
-
final_image_metadatas.append(error_metadata)
|
|
91
76
|
|
|
92
|
-
|
|
93
|
-
return final_image_metadatas
|
|
77
|
+
return sample_metadata_or_errors + non_sample_metadata_or_errors
|