mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.1__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 (49) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +66 -262
  3. mapillary_tools/authenticate.py +54 -46
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/commands/__main__.py +15 -16
  6. mapillary_tools/commands/upload.py +33 -4
  7. mapillary_tools/config.py +38 -17
  8. mapillary_tools/constants.py +127 -43
  9. mapillary_tools/exceptions.py +4 -0
  10. mapillary_tools/exif_read.py +2 -1
  11. mapillary_tools/exif_write.py +3 -1
  12. mapillary_tools/exiftool_read_video.py +52 -15
  13. mapillary_tools/exiftool_runner.py +4 -24
  14. mapillary_tools/ffmpeg.py +406 -232
  15. mapillary_tools/geo.py +16 -0
  16. mapillary_tools/geotag/__init__.py +0 -0
  17. mapillary_tools/geotag/base.py +8 -4
  18. mapillary_tools/geotag/factory.py +106 -89
  19. mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
  20. mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
  21. mapillary_tools/geotag/geotag_images_from_video.py +35 -0
  22. mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
  23. mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
  24. mapillary_tools/geotag/options.py +25 -3
  25. mapillary_tools/geotag/utils.py +9 -12
  26. mapillary_tools/geotag/video_extractors/base.py +1 -1
  27. mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
  28. mapillary_tools/geotag/video_extractors/gpx.py +61 -70
  29. mapillary_tools/geotag/video_extractors/native.py +34 -31
  30. mapillary_tools/history.py +128 -8
  31. mapillary_tools/http.py +211 -0
  32. mapillary_tools/mp4/construct_mp4_parser.py +8 -2
  33. mapillary_tools/process_geotag_properties.py +47 -35
  34. mapillary_tools/process_sequence_properties.py +340 -325
  35. mapillary_tools/sample_video.py +8 -8
  36. mapillary_tools/serializer/description.py +587 -0
  37. mapillary_tools/serializer/gpx.py +132 -0
  38. mapillary_tools/types.py +44 -610
  39. mapillary_tools/upload.py +327 -352
  40. mapillary_tools/upload_api_v4.py +125 -72
  41. mapillary_tools/uploader.py +797 -216
  42. mapillary_tools/utils.py +57 -5
  43. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
  44. mapillary_tools-0.14.1.dist-info/RECORD +76 -0
  45. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
  46. mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
  47. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
  48. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
  49. {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
mapillary_tools/geo.py CHANGED
@@ -51,6 +51,22 @@ def gps_distance(latlon_1: tuple[float, float], latlon_2: tuple[float, float]) -
51
51
  return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2)
52
52
 
53
53
 
54
+ def avg_speed(sequence: T.Sequence[PointLike]) -> float:
55
+ total_distance = 0.0
56
+ for cur, nxt in pairwise(sequence):
57
+ total_distance += gps_distance((cur.lat, cur.lon), (nxt.lat, nxt.lon))
58
+
59
+ if sequence:
60
+ time_diff = sequence[-1].time - sequence[0].time
61
+ else:
62
+ time_diff = 0.0
63
+
64
+ if time_diff == 0.0:
65
+ return float("inf")
66
+
67
+ return total_distance / time_diff
68
+
69
+
54
70
  def compute_bearing(
55
71
  latlon_1: tuple[float, float],
56
72
  latlon_2: tuple[float, float],
File without changes
@@ -46,12 +46,12 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
46
46
  map_results,
47
47
  desc="Extracting images",
48
48
  unit="images",
49
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
49
+ disable=LOG.isEnabledFor(logging.DEBUG),
50
50
  total=len(extractors),
51
51
  )
52
52
  )
53
53
 
54
- return results + error_metadatas
54
+ return T.cast(list[types.ImageMetadataOrError], results + error_metadatas)
55
55
 
56
56
  # This method is passed to multiprocessing
57
57
  # so it has to be classmethod or staticmethod to avoid pickling the instance
@@ -62,6 +62,8 @@ class GeotagImagesFromGeneric(abc.ABC, T.Generic[TImageExtractor]):
62
62
  try:
63
63
  return extractor.extract()
64
64
  except exceptions.MapillaryDescriptionError as ex:
65
+ if LOG.isEnabledFor(logging.DEBUG):
66
+ LOG.error(f"{cls.__name__}({image_path.name}): {ex}")
65
67
  return types.describe_error_metadata(
66
68
  ex, image_path, filetype=types.FileType.IMAGE
67
69
  )
@@ -112,12 +114,12 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
112
114
  map_results,
113
115
  desc="Extracting videos",
114
116
  unit="videos",
115
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
117
+ disable=LOG.isEnabledFor(logging.DEBUG),
116
118
  total=len(extractors),
117
119
  )
118
120
  )
119
121
 
120
- return results + error_metadatas
122
+ return T.cast(list[types.VideoMetadataOrError], results + error_metadatas)
121
123
 
122
124
  # This method is passed to multiprocessing
123
125
  # so it has to be classmethod or staticmethod to avoid pickling the instance
@@ -128,6 +130,8 @@ class GeotagVideosFromGeneric(abc.ABC, T.Generic[TVideoExtractor]):
128
130
  try:
129
131
  return extractor.extract()
130
132
  except exceptions.MapillaryDescriptionError as ex:
133
+ if LOG.isEnabledFor(logging.DEBUG):
134
+ LOG.error(f"{cls.__name__}({video_path.name}): {ex}")
131
135
  return types.describe_error_metadata(
132
136
  ex, video_path, filetype=types.FileType.VIDEO
133
137
  )
@@ -6,7 +6,6 @@ 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,
@@ -68,10 +67,34 @@ def process(
68
67
  reprocessable_paths = set(paths)
69
68
 
70
69
  for idx, option in enumerate(options):
71
- LOG.debug("Processing %d files with %s", len(reprocessable_paths), option)
70
+ if LOG.isEnabledFor(logging.DEBUG):
71
+ LOG.info(
72
+ f"==> Processing {len(reprocessable_paths)} files with source {option}..."
73
+ )
74
+ else:
75
+ LOG.info(
76
+ f"==> Processing {len(reprocessable_paths)} files with source {option.source.value}..."
77
+ )
78
+
79
+ image_videos, video_paths = _filter_images_and_videos(
80
+ reprocessable_paths, option.filetypes
81
+ )
72
82
 
73
- image_metadata_or_errors = _geotag_images(reprocessable_paths, option)
74
- video_metadata_or_errors = _geotag_videos(reprocessable_paths, option)
83
+ if image_videos:
84
+ image_geotag = _build_image_geotag(option)
85
+ image_metadata_or_errors = (
86
+ image_geotag.to_description(image_videos) if image_geotag else []
87
+ )
88
+ else:
89
+ image_metadata_or_errors = []
90
+
91
+ if video_paths:
92
+ video_geotag = _build_video_geotag(option)
93
+ video_metadata_or_errors = (
94
+ video_geotag.to_description(video_paths) if video_geotag else []
95
+ )
96
+ else:
97
+ video_metadata_or_errors = []
75
98
 
76
99
  more_option = idx < len(options) - 1
77
100
 
@@ -97,6 +120,8 @@ def _is_reprocessable(metadata: types.MetadataOrError) -> bool:
97
120
  (
98
121
  exceptions.MapillaryGeoTaggingError,
99
122
  exceptions.MapillaryVideoGPSNotFoundError,
123
+ exceptions.MapillaryExiftoolNotFoundError,
124
+ exceptions.MapillaryExifToolXMLNotFoundError,
100
125
  ),
101
126
  ):
102
127
  return True
@@ -140,152 +165,144 @@ def _ensure_source_path(option: SourceOption) -> Path:
140
165
  return option.source_path.source_path
141
166
 
142
167
 
143
- def _geotag_images(
144
- paths: T.Iterable[Path], option: SourceOption
145
- ) -> list[types.ImageMetadataOrError]:
146
- image_paths, _ = _filter_images_and_videos(paths, option.filetypes)
147
-
148
- if not image_paths:
149
- return []
150
-
168
+ def _build_image_geotag(option: SourceOption) -> base.GeotagImagesFromGeneric | None:
169
+ """
170
+ Build a GeotagImagesFromGeneric object based on the provided option.
171
+ """
151
172
  if option.interpolation is None:
152
173
  interpolation = InterpolationOption()
153
174
  else:
154
175
  interpolation = option.interpolation
155
176
 
156
- geotag: base.GeotagImagesFromGeneric
157
-
158
- if option.source is SourceType.NATIVE:
159
- geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
177
+ if option.source in [SourceType.EXIF, SourceType.NATIVE]:
178
+ return geotag_images_from_exif.GeotagImagesFromEXIF(
160
179
  num_processes=option.num_processes
161
180
  )
162
- return geotag.to_description(image_paths)
163
181
 
164
182
  if option.source is SourceType.EXIFTOOL_RUNTIME:
165
- geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
183
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolRunner(
166
184
  num_processes=option.num_processes
167
185
  )
168
- try:
169
- return geotag.to_description(image_paths)
170
- except exceptions.MapillaryExiftoolNotFoundError as ex:
171
- LOG.warning('Skip "%s" because: %s', option.source.value, ex)
172
- return []
173
186
 
174
187
  elif option.source is SourceType.EXIFTOOL_XML:
175
188
  # This is to ensure 'video_process --geotag={"source": "exiftool_xml", "source_path": "/tmp/xml_path"}'
176
189
  # to work
177
- geotag = geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
178
- xml_path=_ensure_source_path(option),
190
+ if option.source_path is None:
191
+ raise exceptions.MapillaryBadParameterError(
192
+ "source_path must be provided for EXIFTOOL_XML source"
193
+ )
194
+ return geotag_images_from_exiftool.GeotagImagesFromExifToolWithSamples(
195
+ source_path=option.source_path,
179
196
  num_processes=option.num_processes,
180
197
  )
181
- return geotag.to_description(image_paths)
182
198
 
183
199
  elif option.source is SourceType.GPX:
184
- geotag = geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
200
+ return geotag_images_from_gpx_file.GeotagImagesFromGPXFile(
185
201
  source_path=_ensure_source_path(option),
186
202
  use_gpx_start_time=interpolation.use_gpx_start_time,
187
203
  offset_time=interpolation.offset_time,
188
204
  num_processes=option.num_processes,
189
205
  )
190
- return geotag.to_description(image_paths)
191
206
 
192
207
  elif option.source is SourceType.NMEA:
193
- geotag = geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
208
+ return geotag_images_from_nmea_file.GeotagImagesFromNMEAFile(
194
209
  source_path=_ensure_source_path(option),
195
210
  use_gpx_start_time=interpolation.use_gpx_start_time,
196
211
  offset_time=interpolation.offset_time,
197
212
  num_processes=option.num_processes,
198
213
  )
199
214
 
200
- return geotag.to_description(image_paths)
201
-
202
- elif option.source is SourceType.EXIF:
203
- geotag = geotag_images_from_exif.GeotagImagesFromEXIF(
204
- num_processes=option.num_processes
205
- )
206
- return geotag.to_description(image_paths)
207
-
208
- elif option.source in [
209
- SourceType.GOPRO,
210
- SourceType.BLACKVUE,
211
- SourceType.CAMM,
212
- ]:
213
- map_geotag_source_to_filetype: dict[SourceType, FileType] = {
214
- SourceType.GOPRO: FileType.GOPRO,
215
- SourceType.BLACKVUE: FileType.BLACKVUE,
216
- SourceType.CAMM: FileType.CAMM,
217
- }
218
- video_paths = utils.find_videos([_ensure_source_path(option)])
219
- image_samples_by_video_path = utils.find_all_image_samples(
220
- image_paths, video_paths
221
- )
222
- video_paths_with_image_samples = list(image_samples_by_video_path.keys())
223
- video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
224
- filetypes={map_geotag_source_to_filetype[option.source]},
225
- num_processes=option.num_processes,
226
- ).to_description(video_paths_with_image_samples)
227
- geotag = geotag_images_from_video.GeotagImagesFromVideo(
228
- video_metadatas,
215
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
216
+ return geotag_images_from_video.GeotagImageSamplesFromVideo(
217
+ _ensure_source_path(option),
229
218
  offset_time=interpolation.offset_time,
230
219
  num_processes=option.num_processes,
231
220
  )
232
- return geotag.to_description(image_paths)
233
221
 
234
222
  else:
235
223
  raise ValueError(f"Invalid geotag source {option.source}")
236
224
 
237
225
 
238
- def _geotag_videos(
239
- paths: T.Iterable[Path], option: SourceOption
240
- ) -> list[types.VideoMetadataOrError]:
241
- _, video_paths = _filter_images_and_videos(paths, option.filetypes)
242
-
243
- if not video_paths:
244
- return []
245
-
246
- geotag: base.GeotagVideosFromGeneric
226
+ def _build_video_geotag(option: SourceOption) -> base.GeotagVideosFromGeneric | None:
227
+ """
228
+ Build a GeotagVideosFromGeneric object based on the provided option.
247
229
 
230
+ Examples:
231
+ >>> from pathlib import Path
232
+ >>> from mapillary_tools.geotag.options import SourceOption, SourceType
233
+ >>> opt = SourceOption(SourceType.NATIVE)
234
+ >>> geotagger = _build_video_geotag(opt)
235
+ >>> geotagger.__class__.__name__
236
+ 'GeotagVideosFromVideo'
237
+
238
+ >>> opt = SourceOption(SourceType.EXIFTOOL_RUNTIME)
239
+ >>> geotagger = _build_video_geotag(opt)
240
+ >>> geotagger.__class__.__name__
241
+ 'GeotagVideosFromExifToolRunner'
242
+
243
+ >>> opt = SourceOption(SourceType.EXIFTOOL_XML, source_path=Path("/tmp/test.xml"))
244
+ >>> geotagger = _build_video_geotag(opt)
245
+ >>> geotagger.__class__.__name__
246
+ 'GeotagVideosFromExifToolXML'
247
+
248
+ >>> opt = SourceOption(SourceType.GPX, source_path=Path("/tmp/test.gpx"))
249
+ >>> geotagger = _build_video_geotag(opt)
250
+ >>> geotagger.__class__.__name__
251
+ 'GeotagVideosFromGPX'
252
+
253
+ >>> opt = SourceOption(SourceType.NMEA, source_path=Path("/tmp/test.nmea"))
254
+ >>> _build_video_geotag(opt) is None
255
+ True
256
+
257
+ >>> opt = SourceOption(SourceType.EXIF)
258
+ >>> _build_video_geotag(opt) is None
259
+ True
260
+
261
+ >>> opt = SourceOption(SourceType.GOPRO)
262
+ >>> _build_video_geotag(opt) is None
263
+ True
264
+
265
+ >>> try:
266
+ ... _build_video_geotag(SourceOption("invalid"))
267
+ ... except ValueError as e:
268
+ ... "Invalid geotag source" in str(e)
269
+ True
270
+ """
248
271
  if option.source is SourceType.NATIVE:
249
- geotag = geotag_videos_from_video.GeotagVideosFromVideo(
272
+ return geotag_videos_from_video.GeotagVideosFromVideo(
250
273
  num_processes=option.num_processes, filetypes=option.filetypes
251
274
  )
252
- return geotag.to_description(video_paths)
253
275
 
254
276
  if option.source is SourceType.EXIFTOOL_RUNTIME:
255
- geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
277
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolRunner(
256
278
  num_processes=option.num_processes
257
279
  )
258
- try:
259
- return geotag.to_description(video_paths)
260
- except exceptions.MapillaryExiftoolNotFoundError as ex:
261
- LOG.warning('Skip "%s" because: %s', option.source.value, ex)
262
- return []
263
280
 
264
281
  elif option.source is SourceType.EXIFTOOL_XML:
265
- geotag = geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
266
- xml_path=_ensure_source_path(option),
282
+ if option.source_path is None:
283
+ raise exceptions.MapillaryBadParameterError(
284
+ "source_path must be provided for EXIFTOOL_XML source"
285
+ )
286
+ return geotag_videos_from_exiftool.GeotagVideosFromExifToolXML(
287
+ source_path=option.source_path,
267
288
  )
268
- return geotag.to_description(video_paths)
269
289
 
270
290
  elif option.source is SourceType.GPX:
271
- geotag = geotag_videos_from_gpx.GeotagVideosFromGPX()
272
- return geotag.to_description(video_paths)
291
+ return geotag_videos_from_gpx.GeotagVideosFromGPX(
292
+ source_path=option.source_path, num_processes=option.num_processes
293
+ )
273
294
 
274
295
  elif option.source is SourceType.NMEA:
275
296
  # TODO: geotag videos from NMEA
276
- return []
297
+ return None
277
298
 
278
299
  elif option.source is SourceType.EXIF:
279
300
  # Legacy image-specific geotag types
280
- return []
301
+ return None
281
302
 
282
- elif option.source in [
283
- SourceType.GOPRO,
284
- SourceType.BLACKVUE,
285
- SourceType.CAMM,
286
- ]:
303
+ elif option.source in [SourceType.GOPRO, SourceType.BLACKVUE, SourceType.CAMM]:
287
304
  # Legacy image-specific geotag types
288
- return []
305
+ return None
289
306
 
290
307
  else:
291
308
  raise ValueError(f"Invalid geotag source {option.source}")
@@ -13,29 +13,25 @@ else:
13
13
 
14
14
  from .. import constants, exceptions, exiftool_read, types, utils
15
15
  from ..exiftool_runner import ExiftoolRunner
16
+ from . import options
16
17
  from .base import GeotagImagesFromGeneric
17
18
  from .geotag_images_from_video import GeotagImagesFromVideo
18
19
  from .geotag_videos_from_exiftool import GeotagVideosFromExifToolXML
19
20
  from .image_extractors.exiftool import ImageExifToolExtractor
20
- from .utils import index_rdf_description_by_path
21
21
 
22
22
  LOG = logging.getLogger(__name__)
23
23
 
24
24
 
25
25
  class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
26
26
  def __init__(
27
- self,
28
- xml_path: Path,
29
- num_processes: int | None = None,
27
+ self, source_path: options.SourcePathOption, num_processes: int | None = None
30
28
  ):
31
- self.xml_path = xml_path
29
+ self.source_path = source_path
32
30
  super().__init__(num_processes=num_processes)
33
31
 
34
32
  @classmethod
35
33
  def build_image_extractors(
36
- cls,
37
- rdf_by_path: dict[str, ET.Element],
38
- image_paths: T.Iterable[Path],
34
+ cls, rdf_by_path: dict[str, ET.Element], image_paths: T.Iterable[Path]
39
35
  ) -> list[ImageExifToolExtractor | types.ErrorMetadata]:
40
36
  results: list[ImageExifToolExtractor | types.ErrorMetadata] = []
41
37
 
@@ -59,7 +55,9 @@ class GeotagImagesFromExifToolXML(GeotagImagesFromGeneric):
59
55
  def _generate_image_extractors(
60
56
  self, image_paths: T.Sequence[Path]
61
57
  ) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
62
- rdf_by_path = index_rdf_description_by_path([self.xml_path])
58
+ rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
59
+ self.source_path, image_paths
60
+ )
63
61
  return self.build_image_extractors(rdf_by_path, image_paths)
64
62
 
65
63
 
@@ -68,7 +66,10 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
68
66
  def _generate_image_extractors(
69
67
  self, image_paths: T.Sequence[Path]
70
68
  ) -> T.Sequence[ImageExifToolExtractor | types.ErrorMetadata]:
71
- runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
69
+ if constants.EXIFTOOL_PATH is None:
70
+ runner = ExiftoolRunner()
71
+ else:
72
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
72
73
 
73
74
  LOG.debug(
74
75
  "Extracting XML from %d images with ExifTool command: %s",
@@ -78,7 +79,13 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
78
79
  try:
79
80
  xml = runner.extract_xml(image_paths)
80
81
  except FileNotFoundError as ex:
81
- raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
82
+ exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
83
+ return [
84
+ types.describe_error_metadata(
85
+ exiftool_ex, image_path, filetype=types.FileType.IMAGE
86
+ )
87
+ for image_path in image_paths
88
+ ]
82
89
 
83
90
  try:
84
91
  xml_element = ET.fromstring(xml)
@@ -86,7 +93,7 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
86
93
  LOG.warning(
87
94
  "Failed to parse ExifTool XML: %s",
88
95
  str(ex),
89
- exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
96
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
90
97
  )
91
98
  rdf_by_path = {}
92
99
  else:
@@ -102,29 +109,30 @@ class GeotagImagesFromExifToolRunner(GeotagImagesFromGeneric):
102
109
  class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
103
110
  def __init__(
104
111
  self,
105
- xml_path: Path,
112
+ source_path: options.SourcePathOption,
106
113
  offset_time: float = 0.0,
107
114
  num_processes: int | None = None,
108
115
  ):
109
116
  super().__init__(num_processes=num_processes)
110
- self.xml_path = xml_path
117
+ self.source_path = source_path
111
118
  self.offset_time = offset_time
112
119
 
113
120
  def geotag_samples(
114
121
  self, image_paths: T.Sequence[Path]
115
122
  ) -> list[types.ImageMetadataOrError]:
116
123
  # Find all video paths in self.xml_path
117
- rdf_by_path = index_rdf_description_by_path([self.xml_path])
124
+ rdf_by_path = GeotagVideosFromExifToolXML.find_rdf_by_path(
125
+ self.source_path, image_paths
126
+ )
118
127
  video_paths = utils.find_videos(
119
- [Path(pathstr) for pathstr in rdf_by_path.keys()],
128
+ [Path(canonical_path) for canonical_path in rdf_by_path.keys()],
120
129
  skip_subfolders=True,
121
130
  )
122
131
  # Find all video paths that have sample images
123
132
  samples_by_video = utils.find_all_image_samples(image_paths, video_paths)
124
133
 
125
134
  video_metadata_or_errors = GeotagVideosFromExifToolXML(
126
- self.xml_path,
127
- num_processes=self.num_processes,
135
+ self.source_path, num_processes=self.num_processes
128
136
  ).to_description(list(samples_by_video.keys()))
129
137
  sample_paths = sum(samples_by_video.values(), [])
130
138
  sample_metadata_or_errors = GeotagImagesFromVideo(
@@ -146,8 +154,7 @@ class GeotagImagesFromExifToolWithSamples(GeotagImagesFromGeneric):
146
154
  non_sample_paths = [path for path in image_paths if path not in sample_paths]
147
155
 
148
156
  non_sample_metadata_or_errors = GeotagImagesFromExifToolXML(
149
- self.xml_path,
150
- num_processes=self.num_processes,
157
+ self.source_path, num_processes=self.num_processes
151
158
  ).to_description(non_sample_paths)
152
159
 
153
160
  return sample_metadata_or_errors + non_sample_metadata_or_errors
@@ -12,6 +12,7 @@ else:
12
12
  from typing_extensions import override
13
13
 
14
14
  from .. import exceptions, geo, types
15
+ from ..serializer.description import build_capture_time
15
16
  from .base import GeotagImagesFromGeneric
16
17
  from .geotag_images_from_exif import ImageEXIFExtractor
17
18
 
@@ -43,26 +44,26 @@ class GeotagImagesFromGPX(GeotagImagesFromGeneric):
43
44
 
44
45
  if image_metadata.time < sorted_points[0].time:
45
46
  delta = sorted_points[0].time - image_metadata.time
46
- gpx_start_time = types.datetime_to_map_capture_time(sorted_points[0].time)
47
- gpx_end_time = types.datetime_to_map_capture_time(sorted_points[-1].time)
47
+ gpx_start_time = build_capture_time(sorted_points[0].time)
48
+ gpx_end_time = build_capture_time(sorted_points[-1].time)
48
49
  # with the tolerance of 1ms
49
50
  if 0.001 < delta:
50
51
  raise exceptions.MapillaryOutsideGPXTrackError(
51
52
  f"The image date time is {round(delta, 3)} seconds behind the GPX start point",
52
- image_time=types.datetime_to_map_capture_time(image_metadata.time),
53
+ image_time=build_capture_time(image_metadata.time),
53
54
  gpx_start_time=gpx_start_time,
54
55
  gpx_end_time=gpx_end_time,
55
56
  )
56
57
 
57
58
  if sorted_points[-1].time < image_metadata.time:
58
59
  delta = image_metadata.time - sorted_points[-1].time
59
- gpx_start_time = types.datetime_to_map_capture_time(sorted_points[0].time)
60
- gpx_end_time = types.datetime_to_map_capture_time(sorted_points[-1].time)
60
+ gpx_start_time = build_capture_time(sorted_points[0].time)
61
+ gpx_end_time = build_capture_time(sorted_points[-1].time)
61
62
  # with the tolerance of 1ms
62
63
  if 0.001 < delta:
63
64
  raise exceptions.MapillaryOutsideGPXTrackError(
64
65
  f"The image time is {round(delta, 3)} seconds beyond the GPX end point",
65
- image_time=types.datetime_to_map_capture_time(image_metadata.time),
66
+ image_time=build_capture_time(image_metadata.time),
66
67
  gpx_start_time=gpx_start_time,
67
68
  gpx_end_time=gpx_end_time,
68
69
  )
@@ -13,6 +13,7 @@ else:
13
13
  from .. import types, utils
14
14
  from .base import GeotagImagesFromGeneric
15
15
  from .geotag_images_from_gpx import GeotagImagesFromGPX
16
+ from .geotag_videos_from_video import GeotagVideosFromVideo
16
17
 
17
18
 
18
19
  LOG = logging.getLogger(__name__)
@@ -90,3 +91,37 @@ class GeotagImagesFromVideo(GeotagImagesFromGeneric):
90
91
  assert len(final_image_metadatas) <= len(image_paths)
91
92
 
92
93
  return final_image_metadatas
94
+
95
+
96
+ class GeotagImageSamplesFromVideo(GeotagImagesFromGeneric):
97
+ def __init__(
98
+ self,
99
+ source_path: Path,
100
+ filetypes: set[types.FileType] | None = None,
101
+ offset_time: float = 0.0,
102
+ num_processes: int | None = None,
103
+ ):
104
+ super().__init__(num_processes=num_processes)
105
+ self.source_path = source_path
106
+ self.filetypes = filetypes
107
+ self.offset_time = offset_time
108
+
109
+ @override
110
+ def to_description(
111
+ self, image_paths: T.Sequence[Path]
112
+ ) -> list[types.ImageMetadataOrError]:
113
+ video_paths = utils.find_videos([self.source_path])
114
+ image_samples_by_video_path = utils.find_all_image_samples(
115
+ image_paths, video_paths
116
+ )
117
+ video_paths_with_image_samples = list(image_samples_by_video_path.keys())
118
+ video_metadatas = GeotagVideosFromVideo(
119
+ filetypes=self.filetypes,
120
+ num_processes=self.num_processes,
121
+ ).to_description(video_paths_with_image_samples)
122
+ geotag = GeotagImagesFromVideo(
123
+ video_metadatas,
124
+ offset_time=self.offset_time,
125
+ num_processes=self.num_processes,
126
+ )
127
+ return geotag.to_description(image_paths)