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.
Files changed (67) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +4 -4
  3. mapillary_tools/camm/camm_parser.py +5 -5
  4. mapillary_tools/commands/__main__.py +1 -2
  5. mapillary_tools/config.py +7 -5
  6. mapillary_tools/constants.py +1 -2
  7. mapillary_tools/exceptions.py +1 -1
  8. mapillary_tools/exif_read.py +65 -65
  9. mapillary_tools/exif_write.py +7 -7
  10. mapillary_tools/exiftool_read.py +23 -46
  11. mapillary_tools/exiftool_read_video.py +36 -34
  12. mapillary_tools/ffmpeg.py +24 -23
  13. mapillary_tools/geo.py +4 -21
  14. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +32 -48
  15. mapillary_tools/geotag/factory.py +27 -34
  16. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  17. mapillary_tools/geotag/geotag_images_from_exiftool.py +107 -59
  18. mapillary_tools/geotag/geotag_images_from_gpx.py +20 -10
  19. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  20. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  21. mapillary_tools/geotag/geotag_images_from_video.py +16 -14
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +14 -115
  24. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  25. mapillary_tools/geotag/image_extractors/base.py +18 -0
  26. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  27. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  28. mapillary_tools/geotag/options.py +1 -0
  29. mapillary_tools/geotag/utils.py +62 -0
  30. mapillary_tools/geotag/video_extractors/base.py +18 -0
  31. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  32. mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
  33. mapillary_tools/geotag/video_extractors/native.py +157 -0
  34. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  35. mapillary_tools/gpmf/gps_filter.py +5 -3
  36. mapillary_tools/history.py +4 -2
  37. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  38. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  39. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  40. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  41. mapillary_tools/process_geotag_properties.py +5 -7
  42. mapillary_tools/process_sequence_properties.py +40 -38
  43. mapillary_tools/sample_video.py +8 -8
  44. mapillary_tools/telemetry.py +6 -5
  45. mapillary_tools/types.py +33 -38
  46. mapillary_tools/utils.py +16 -18
  47. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +1 -1
  48. mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
  49. mapillary_tools/geotag/__init__.py +0 -1
  50. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  51. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  52. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  53. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  54. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  55. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  56. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  57. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  58. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  59. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  60. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  61. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  62. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  63. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  64. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +0 -0
  65. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
  66. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/licenses/LICENSE +0 -0
  67. {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
- 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)
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(self) -> list[types.ImageMetadataOrError]:
43
- extractor_or_errors = self._generate_image_extractors()
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(self.image_paths)
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
- 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:
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=GenericVideoExtractor)
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(self) -> list[types.VideoMetadataOrError]:
118
- extractor_or_errors = self._generate_video_extractors()
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(self.video_paths)
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
- geotag_from_generic,
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
- geotag_videos_from_exiftool_video,
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
- file_paths: T.Iterable[Path],
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 file_paths:
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: geotag_from_generic.GeotagImagesFromGeneric
156
+ geotag: base.GeotagImagesFromGeneric
158
157
 
159
158
  if option.source is SourceType.NATIVE:
160
159
  geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
161
- image_paths, num_processes=option.num_processes
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
- image_paths, num_processes=option.num_processes
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 = geotag_images_from_exiftool_both_image_and_video.GeotagImagesFromExifToolBothImageAndVideo(
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
- image_paths, num_processes=option.num_processes
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: geotag_from_generic.GeotagVideosFromGeneric
246
+ geotag: base.GeotagVideosFromGeneric
253
247
 
254
248
  if option.source is SourceType.NATIVE:
255
249
  geotag = geotag_videos_from_video.GeotagVideosFromVideo(
256
- video_paths, num_processes=option.num_processes, filetypes=option.filetypes
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 = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolRunner(
262
- video_paths, num_processes=option.num_processes
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 = geotag_videos_from_exiftool_video.GeotagVideosFromExifToolVideo(
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(video_paths)
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 contextlib
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
- from .. import exceptions, geo, types, utils
7
- from ..exif_read import ExifRead, ExifReadABC
8
- from .geotag_from_generic import GenericImageExtractor, GeotagImagesFromGeneric
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
- @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)
13
+ from .base import GeotagImagesFromGeneric
14
+ from .image_extractors.exif import ImageEXIFExtractor
22
15
 
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:
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
- def _generate_image_extractors(self) -> T.Sequence[ImageEXIFExtractor]:
60
- return [ImageEXIFExtractor(path) for path in self.image_paths]
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
- from .. import constants, exceptions, exiftool_read, types
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 .geotag_from_generic import GeotagImagesFromGeneric
12
- from .geotag_images_from_exif import ImageEXIFExtractor
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 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
-
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__(image_paths=image_paths, num_processes=num_processes)
36
-
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
- )
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 self.image_paths:
47
- rdf_description = rdf_description_by_path.get(
48
- exiftool_read.canonical_path(path)
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"
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
- exc, path, filetype=types.FileType.IMAGE
50
+ ex, path, filetype=types.FileType.IMAGE
57
51
  )
58
52
  )
59
53
  else:
60
- results.append(ImageExifToolExtractor(path, rdf_description))
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 exiftool command: %s",
73
- len(self.image_paths),
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(self.image_paths)
79
+ xml = runner.extract_xml(image_paths)
78
80
  except FileNotFoundError as ex:
79
81
  raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
80
82
 
81
- rdf_description_by_path = (
82
- exiftool_read.index_rdf_description_by_path_from_xml_element(
83
- ET.fromstring(xml)
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
- results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
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
- return results
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