mapillary-tools 0.14.5__py3-none-any.whl → 0.14.6__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 (78) hide show
  1. mapillary_tools/__init__.py +6 -1
  2. mapillary_tools/api_v4.py +5 -0
  3. mapillary_tools/authenticate.py +5 -1
  4. mapillary_tools/blackvue_parser.py +138 -20
  5. mapillary_tools/camm/camm_builder.py +5 -1
  6. mapillary_tools/camm/camm_parser.py +5 -0
  7. mapillary_tools/commands/__init__.py +5 -0
  8. mapillary_tools/commands/__main__.py +5 -0
  9. mapillary_tools/commands/authenticate.py +5 -0
  10. mapillary_tools/commands/process.py +12 -0
  11. mapillary_tools/commands/process_and_upload.py +5 -1
  12. mapillary_tools/commands/sample_video.py +5 -0
  13. mapillary_tools/commands/upload.py +5 -0
  14. mapillary_tools/commands/video_process.py +5 -0
  15. mapillary_tools/commands/video_process_and_upload.py +5 -1
  16. mapillary_tools/commands/zip.py +5 -0
  17. mapillary_tools/config.py +5 -0
  18. mapillary_tools/constants.py +13 -1
  19. mapillary_tools/exceptions.py +9 -0
  20. mapillary_tools/exif_read.py +89 -0
  21. mapillary_tools/exif_write.py +17 -0
  22. mapillary_tools/exiftool_read.py +89 -0
  23. mapillary_tools/exiftool_read_video.py +56 -0
  24. mapillary_tools/exiftool_runner.py +5 -0
  25. mapillary_tools/ffmpeg.py +5 -0
  26. mapillary_tools/geo.py +91 -31
  27. mapillary_tools/geotag/__init__.py +4 -0
  28. mapillary_tools/geotag/base.py +5 -0
  29. mapillary_tools/geotag/factory.py +5 -0
  30. mapillary_tools/geotag/geotag_images_from_exif.py +5 -0
  31. mapillary_tools/geotag/geotag_images_from_exiftool.py +5 -0
  32. mapillary_tools/geotag/geotag_images_from_gpx.py +5 -0
  33. mapillary_tools/geotag/geotag_images_from_gpx_file.py +5 -0
  34. mapillary_tools/geotag/geotag_images_from_nmea_file.py +5 -0
  35. mapillary_tools/geotag/geotag_images_from_video.py +6 -0
  36. mapillary_tools/geotag/geotag_videos_from_exiftool.py +5 -0
  37. mapillary_tools/geotag/geotag_videos_from_gpx.py +5 -0
  38. mapillary_tools/geotag/geotag_videos_from_video.py +5 -0
  39. mapillary_tools/geotag/image_extractors/base.py +5 -0
  40. mapillary_tools/geotag/image_extractors/exif.py +6 -0
  41. mapillary_tools/geotag/image_extractors/exiftool.py +5 -0
  42. mapillary_tools/geotag/options.py +5 -0
  43. mapillary_tools/geotag/utils.py +5 -0
  44. mapillary_tools/geotag/video_extractors/base.py +5 -0
  45. mapillary_tools/geotag/video_extractors/exiftool.py +7 -0
  46. mapillary_tools/geotag/video_extractors/gpx.py +5 -0
  47. mapillary_tools/geotag/video_extractors/native.py +5 -0
  48. mapillary_tools/gpmf/gpmf_gps_filter.py +5 -0
  49. mapillary_tools/gpmf/gpmf_parser.py +5 -0
  50. mapillary_tools/gpmf/gps_filter.py +5 -0
  51. mapillary_tools/history.py +5 -0
  52. mapillary_tools/http.py +5 -1
  53. mapillary_tools/ipc.py +5 -0
  54. mapillary_tools/mp4/__init__.py +4 -0
  55. mapillary_tools/mp4/construct_mp4_parser.py +5 -0
  56. mapillary_tools/mp4/io_utils.py +5 -0
  57. mapillary_tools/mp4/mp4_sample_parser.py +20 -1
  58. mapillary_tools/mp4/simple_mp4_builder.py +5 -0
  59. mapillary_tools/mp4/simple_mp4_parser.py +5 -0
  60. mapillary_tools/process_geotag_properties.py +5 -0
  61. mapillary_tools/process_sequence_properties.py +213 -31
  62. mapillary_tools/sample_video.py +13 -1
  63. mapillary_tools/serializer/description.py +13 -0
  64. mapillary_tools/serializer/gpx.py +5 -1
  65. mapillary_tools/store.py +5 -0
  66. mapillary_tools/telemetry.py +108 -0
  67. mapillary_tools/types.py +6 -0
  68. mapillary_tools/upload.py +5 -0
  69. mapillary_tools/upload_api_v4.py +5 -0
  70. mapillary_tools/uploader.py +9 -0
  71. mapillary_tools/utils.py +16 -1
  72. {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/METADATA +8 -1
  73. mapillary_tools-0.14.6.dist-info/RECORD +77 -0
  74. {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/WHEEL +1 -1
  75. mapillary_tools-0.14.5.dist-info/RECORD +0 -77
  76. {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/entry_points.txt +0 -0
  77. {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/licenses/LICENSE +0 -0
  78. {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import sys
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import logging
2
7
  import typing as T
3
8
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import dataclasses
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import statistics
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import json
mapillary_tools/http.py CHANGED
@@ -1,7 +1,11 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import logging
4
-
5
9
  import ssl
6
10
  import sys
7
11
  import typing as T
mapillary_tools/ipc.py CHANGED
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import json
2
7
  import logging
3
8
  import os
@@ -0,0 +1,4 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  # pyre-ignore-all-errors[5, 16, 21, 58]
2
7
  from __future__ import annotations
3
8
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import io
2
7
  import typing as T
3
8
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import datetime
@@ -7,6 +12,14 @@ from pathlib import Path
7
12
  from . import construct_mp4_parser as cparser, simple_mp4_parser as sparser
8
13
 
9
14
 
15
+ def _convert_to_signed_int32(unsigned_int32: int) -> int:
16
+ """Interpret an unsigned 32-bit value as negative if high bit is set."""
17
+ if (unsigned_int32 & (1 << 31)) == 0:
18
+ return unsigned_int32
19
+ else:
20
+ return unsigned_int32 - (1 << 32)
21
+
22
+
10
23
  class RawSample(T.NamedTuple):
11
24
  # 1-based index
12
25
  description_idx: int
@@ -192,7 +205,13 @@ def extract_raw_samples_from_stbl_data(
192
205
  composition_offsets = []
193
206
  for entry in data["entries"]:
194
207
  for _ in range(entry["sample_count"]):
195
- composition_offsets.append(entry["sample_offset"])
208
+ # Some encodings like H.264 and H.265 support negative offsets.
209
+ # We cannot rely on the version field since some encoders incorrectly set
210
+ # ctts version to 0 instead of 1 even when using signed offsets.
211
+ # Leigitimate positive values are relatively small so we can assume the value is signed.
212
+ composition_offsets.append(
213
+ _convert_to_signed_int32(entry["sample_offset"])
214
+ )
196
215
  elif box["type"] == b"stss":
197
216
  syncs = set(data["entries"])
198
217
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import dataclasses
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  # pyre-ignore-all-errors[5, 16, 21, 24, 58]
2
7
  from __future__ import annotations
3
8
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import datetime
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import functools
@@ -141,6 +146,55 @@ def duplication_check(
141
146
  return dedups, dups
142
147
 
143
148
 
149
+ def _check_null_island(
150
+ sequence: PointSequence,
151
+ ) -> tuple[PointSequence, list[types.ErrorMetadata]]:
152
+ """
153
+ Filter out images with null island (0, 0) GPS coordinates.
154
+
155
+ Returns:
156
+ Tuple of (valid images, error metadatas for null island images)
157
+ """
158
+ valid: PointSequence = []
159
+ errors: list[types.ErrorMetadata] = []
160
+
161
+ for image in sequence:
162
+ if image.lat == 0 and image.lon == 0:
163
+ ex = exceptions.MapillaryNullIslandError(
164
+ "GPS coordinates in Null Island (0, 0)"
165
+ )
166
+ errors.append(
167
+ types.describe_error_metadata(
168
+ exc=ex, filename=image.filename, filetype=types.FileType.IMAGE
169
+ )
170
+ )
171
+ else:
172
+ valid.append(image)
173
+
174
+ return valid, errors
175
+
176
+
177
+ def _check_sequences_null_island(
178
+ input_sequences: T.Sequence[PointSequence],
179
+ ) -> tuple[list[PointSequence], list[types.ErrorMetadata]]:
180
+ """Apply null island check to all sequences."""
181
+ output_sequences: list[PointSequence] = []
182
+ output_errors: list[types.ErrorMetadata] = []
183
+
184
+ for sequence in input_sequences:
185
+ output_sequence, errors = _check_null_island(sequence)
186
+ if output_sequence:
187
+ output_sequences.append(output_sequence)
188
+ output_errors.extend(errors)
189
+
190
+ if output_errors:
191
+ LOG.info(
192
+ f"Null island check: {len(output_errors)} images removed with (0, 0) coordinates"
193
+ )
194
+
195
+ return output_sequences, output_errors
196
+
197
+
144
198
  def _group_images_by(
145
199
  image_metadatas: T.Iterable[types.ImageMetadata],
146
200
  group_key_func: T.Callable[[types.ImageMetadata], T.Hashable],
@@ -283,49 +337,28 @@ def _video_name(video_metadata: types.VideoMetadata) -> str:
283
337
 
284
338
  def _check_sequences_by_limits(
285
339
  input_sequences: T.Sequence[PointSequence],
286
- max_sequence_filesize_in_bytes: int | None,
287
340
  max_capture_speed_kmh: float,
288
341
  ) -> tuple[list[PointSequence], list[types.ErrorMetadata]]:
289
342
  output_sequences: list[PointSequence] = []
290
343
  output_errors: list[types.ErrorMetadata] = []
291
344
 
292
345
  for sequence in input_sequences:
293
- try:
294
- if max_sequence_filesize_in_bytes is not None:
295
- sequence_filesize = sum(
296
- utils.get_file_size(image.filename)
297
- if image.filesize is None
298
- else image.filesize
299
- for image in sequence
300
- )
301
- if sequence_filesize > max_sequence_filesize_in_bytes:
302
- raise exceptions.MapillaryFileTooLargeError(
303
- f"Sequence file size {humanize.naturalsize(sequence_filesize)} exceeds max allowed {humanize.naturalsize(max_sequence_filesize_in_bytes)}",
304
- )
346
+ avg_speed_kmh = geo.avg_speed(sequence) * 3.6 # Convert m/s to km/h
347
+ too_fast = len(sequence) >= 2 and avg_speed_kmh > max_capture_speed_kmh
305
348
 
306
- contains_null_island = any(
307
- image.lat == 0 and image.lon == 0 for image in sequence
349
+ if too_fast:
350
+ error = exceptions.MapillaryCaptureSpeedTooFastError(
351
+ f"Capture speed {avg_speed_kmh:.3f} km/h exceeds max allowed {max_capture_speed_kmh:.3f} km/h",
308
352
  )
309
- if contains_null_island:
310
- raise exceptions.MapillaryNullIslandError(
311
- "GPS coordinates in Null Island (0, 0)"
312
- )
313
-
314
- avg_speed_kmh = geo.avg_speed(sequence) * 3.6 # Convert m/s to km/h
315
- too_fast = len(sequence) >= 2 and avg_speed_kmh > max_capture_speed_kmh
316
- if too_fast:
317
- raise exceptions.MapillaryCaptureSpeedTooFastError(
318
- f"Capture speed {avg_speed_kmh:.3f} km/h exceeds max allowed {max_capture_speed_kmh:.3f} km/h",
319
- )
320
- except exceptions.MapillaryDescriptionError as ex:
321
- LOG.error(f"{_sequence_name(sequence)}: {ex}")
353
+ LOG.error(f"{_sequence_name(sequence)}: {error}")
322
354
  for image in sequence:
323
355
  output_errors.append(
324
356
  types.describe_error_metadata(
325
- exc=ex, filename=image.filename, filetype=types.FileType.IMAGE
357
+ exc=error,
358
+ filename=image.filename,
359
+ filetype=types.FileType.IMAGE,
326
360
  )
327
361
  )
328
-
329
362
  else:
330
363
  output_sequences.append(sequence)
331
364
 
@@ -350,6 +383,7 @@ def _group_by_folder_and_camera(
350
383
  image_metadatas,
351
384
  lambda metadata: (
352
385
  str(metadata.filename.parent),
386
+ metadata.MAPCameraUUID,
353
387
  metadata.MAPDeviceMake,
354
388
  metadata.MAPDeviceModel,
355
389
  metadata.width,
@@ -397,6 +431,138 @@ def _check_sequences_duplication(
397
431
  return output_sequences, output_errors
398
432
 
399
433
 
434
+ def _check_sequences_zigzag(
435
+ input_sequences: T.Sequence[PointSequence],
436
+ window_size: int = 5,
437
+ deviation_threshold: float = 0.8,
438
+ min_deviations: int = 1,
439
+ min_distance: float = 50.0,
440
+ ) -> tuple[list[PointSequence], list[types.ErrorMetadata]]:
441
+ """
442
+ Check for zig-zag GPS patterns where images jump back and forth between locations.
443
+
444
+ Detects spatial deviations - when an image returns closer to earlier images
445
+ than the previous image was. This catches zig-zag patterns where the sequence
446
+ jumps to a different location and then returns.
447
+
448
+ Only marks deviation points as errors, keeping the main path intact.
449
+
450
+ Args:
451
+ input_sequences: List of image sequences to check
452
+ window_size: Number of images to look back when checking for deviations
453
+ deviation_threshold: Ratio threshold - if dist_curr < dist_prev * threshold,
454
+ it's considered a deviation
455
+ min_deviations: Minimum number of deviation events to mark errors
456
+ min_distance: Minimum distance (in meters) between consecutive images (prev to curr)
457
+ to consider for deviation detection. This filters out small-scale
458
+ movements like U-turns.
459
+ """
460
+ output_sequences: list[PointSequence] = []
461
+ output_errors: list[types.ErrorMetadata] = []
462
+
463
+ for sequence in input_sequences:
464
+ if len(sequence) < window_size + 1:
465
+ # Sequence too short to detect pattern
466
+ output_sequences.append(sequence)
467
+ continue
468
+
469
+ # Track which indices are detected as deviations
470
+ deviation_indices: set[int] = set()
471
+
472
+ for i in range(window_size, len(sequence)):
473
+ curr = sequence[i]
474
+ prev = sequence[i - 1]
475
+ ref = sequence[i - window_size]
476
+
477
+ # Distance between consecutive images (prev to curr)
478
+ dist_prev_curr = geo.gps_distance(
479
+ (prev.lat, prev.lon), (curr.lat, curr.lon)
480
+ )
481
+ # Distance from current image back to reference
482
+ dist_curr = geo.gps_distance((curr.lat, curr.lon), (ref.lat, ref.lon))
483
+ # Distance from previous image to reference
484
+ dist_prev = geo.gps_distance((prev.lat, prev.lon), (ref.lat, ref.lon))
485
+
486
+ # Deviation: current is closer to reference than previous was
487
+ # Only check if the jump between prev and curr is above min_distance
488
+ if (
489
+ dist_prev_curr > min_distance
490
+ and dist_curr < dist_prev * deviation_threshold
491
+ ):
492
+ LOG.debug(
493
+ f"Zigzag detected at {prev.filename.name}: "
494
+ f"dist_curr={dist_curr:.1f}m < dist_prev={dist_prev:.1f}m * {deviation_threshold}, "
495
+ f"jump={dist_prev_curr:.1f}m"
496
+ )
497
+
498
+ # Mark prev as deviation (it's the point that jumped away)
499
+ deviation_indices.add(i - 1)
500
+
501
+ # Walk backwards from prev to ref+1 to find more deviation points
502
+ # We're looking for deviations between prev and ref
503
+ # Use the same check as above: compare distance to ref and jump to curr
504
+ for j in range(i - 2, i - window_size, -1): # Stop at ref+1
505
+ point_j = sequence[j]
506
+
507
+ # Distance from j to ref
508
+ dist_j_to_ref = geo.gps_distance(
509
+ (point_j.lat, point_j.lon), (ref.lat, ref.lon)
510
+ )
511
+ # Distance from j to curr
512
+ dist_j_to_curr = geo.gps_distance(
513
+ (point_j.lat, point_j.lon), (curr.lat, curr.lon)
514
+ )
515
+
516
+ # Same check as original: j is a deviation if it's farther from ref
517
+ # than curr is, and the jump from j to curr is significant
518
+ if (
519
+ dist_j_to_curr > min_distance
520
+ and dist_curr < dist_j_to_ref * deviation_threshold
521
+ ):
522
+ deviation_indices.add(j)
523
+ LOG.debug(
524
+ f"Backwards walk: {point_j.filename.name} also marked as deviation"
525
+ )
526
+ else:
527
+ # j is on the normal path, stop walking backwards
528
+ break
529
+
530
+ if len(deviation_indices) >= min_deviations:
531
+ # Create errors only for deviation points
532
+ for idx in sorted(deviation_indices):
533
+ image = sequence[idx]
534
+ ex = exceptions.MapillaryZigZagError("GPS zig-zag deviation detected")
535
+ LOG.error(f"{image.filename.name}: {ex}")
536
+ output_errors.append(
537
+ types.describe_error_metadata(
538
+ exc=ex, filename=image.filename, filetype=types.FileType.IMAGE
539
+ )
540
+ )
541
+
542
+ # Keep non-deviation points in output sequence
543
+ non_deviation_points = [
544
+ sequence[idx]
545
+ for idx in range(len(sequence))
546
+ if idx not in deviation_indices
547
+ ]
548
+ if non_deviation_points:
549
+ output_sequences.append(non_deviation_points)
550
+ else:
551
+ output_sequences.append(sequence)
552
+
553
+ # Assertion to ensure all images accounted for
554
+ assert sum(len(s) for s in output_sequences) + len(output_errors) == sum(
555
+ len(s) for s in input_sequences
556
+ )
557
+
558
+ if output_errors:
559
+ LOG.info(
560
+ f"Zig-zag check: {len(output_errors)} images rejected due to GPS zig-zag patterns"
561
+ )
562
+
563
+ return output_sequences, output_errors
564
+
565
+
400
566
  class SplitState(T.TypedDict, total=False):
401
567
  sequence_images: int
402
568
  sequence_file_size: int
@@ -604,6 +770,7 @@ def process_sequence_properties(
604
770
  duplicate_distance: float = constants.DUPLICATE_DISTANCE,
605
771
  duplicate_angle: float = constants.DUPLICATE_ANGLE,
606
772
  max_capture_speed_kmh: float = constants.MAX_CAPTURE_SPEED_KMH,
773
+ skip_zigzag_check: bool = False,
607
774
  ) -> list[types.MetadataOrError]:
608
775
  LOG.info("==> Processing sequences...")
609
776
 
@@ -660,6 +827,10 @@ def process_sequence_properties(
660
827
  cutoff_time=cutoff_time,
661
828
  )
662
829
 
830
+ # Null island check
831
+ sequences, errors = _check_sequences_null_island(sequences)
832
+ error_metadatas.extend(errors)
833
+
663
834
  # Duplication check
664
835
  sequences, errors = _check_sequences_duplication(
665
836
  sequences,
@@ -678,11 +849,22 @@ def process_sequence_properties(
678
849
  # Check limits for sequences
679
850
  sequences, errors = _check_sequences_by_limits(
680
851
  sequences,
681
- max_sequence_filesize_in_bytes=max_sequence_filesize_in_bytes,
682
852
  max_capture_speed_kmh=max_capture_speed_kmh,
683
853
  )
684
854
  error_metadatas.extend(errors)
685
855
 
856
+ # Check for zig-zag GPS patterns
857
+ # NOTE: This is done after _check_sequences_null_island to filter zero coordinates
858
+ if not skip_zigzag_check:
859
+ sequences, errors = _check_sequences_zigzag(
860
+ sequences,
861
+ window_size=constants.ZIGZAG_WINDOW_SIZE,
862
+ deviation_threshold=constants.ZIGZAG_DEVIATION_THRESHOLD,
863
+ min_deviations=constants.ZIGZAG_MIN_DEVIATIONS,
864
+ min_distance=constants.ZIGZAG_MIN_DISTANCE,
865
+ )
866
+ error_metadatas.extend(errors)
867
+
686
868
  # Split sequences by cutoff distance
687
869
  # NOTE: The speed limit check probably rejects most anomalies
688
870
  sequences = _split_sequences_by_limits(
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import datetime
@@ -349,7 +354,14 @@ def _sample_single_video_by_distance(
349
354
  f"interpolated time {interp.time} should match the video sample time {video_sample.exact_composition_time}"
350
355
  )
351
356
 
352
- timestamp = start_time + datetime.timedelta(seconds=interp.time)
357
+ # Try to use GPS epoch time if available (for timelapse videos)
358
+ gps_epoch_time = interp.get_gps_epoch_time()
359
+ if gps_epoch_time is not None:
360
+ timestamp = datetime.datetime.fromtimestamp(
361
+ gps_epoch_time, tz=datetime.timezone.utc
362
+ )
363
+ else:
364
+ timestamp = start_time + datetime.timedelta(seconds=interp.time)
353
365
  exif_edit = ExifEdit(sample_paths[0])
354
366
  exif_edit.add_date_time_original(timestamp)
355
367
  exif_edit.add_gps_datetime(timestamp)
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  from __future__ import annotations
2
7
 
3
8
  import dataclasses
@@ -84,6 +89,7 @@ class VideoDescription(_SharedDescription, total=False):
84
89
  MAPGPSTrack: Required[list[T.Sequence[float | int | None]]]
85
90
  MAPDeviceMake: str
86
91
  MAPDeviceModel: str
92
+ MAPCameraUUID: str
87
93
 
88
94
 
89
95
  class _ErrorObject(TypedDict, total=False):
@@ -201,6 +207,10 @@ VideoDescriptionSchema = {
201
207
  "type": "string",
202
208
  "description": "Device model, e.g. HERO10 Black, DR900S-1CH, Insta360 Titan",
203
209
  },
210
+ "MAPCameraUUID": {
211
+ "type": "string",
212
+ "description": "Camera unique identifier, typically derived from camera serial number",
213
+ },
204
214
  },
205
215
  "required": [
206
216
  "MAPGPSTrack",
@@ -397,6 +407,8 @@ class DescriptionJSONSerializer(BaseSerializer):
397
407
  desc["MAPDeviceMake"] = metadata.make
398
408
  if metadata.model:
399
409
  desc["MAPDeviceModel"] = metadata.model
410
+ if metadata.camera_uuid:
411
+ desc["MAPCameraUUID"] = metadata.camera_uuid
400
412
  return desc
401
413
 
402
414
  @classmethod
@@ -490,6 +502,7 @@ class DescriptionJSONSerializer(BaseSerializer):
490
502
  points=[PointEncoder.decode(entry) for entry in desc["MAPGPSTrack"]],
491
503
  make=desc.get("MAPDeviceMake"),
492
504
  model=desc.get("MAPDeviceModel"),
505
+ camera_uuid=desc.get("MAPCameraUUID"),
493
506
  )
494
507
 
495
508
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import datetime
2
7
  import json
3
8
  import sys
@@ -12,7 +17,6 @@ import gpxpy
12
17
  import gpxpy.gpx
13
18
 
14
19
  from .. import geo, types
15
-
16
20
  from ..telemetry import CAMMGPSPoint, GPSPoint
17
21
  from ..types import (
18
22
  BaseSerializer,
mapillary_tools/store.py CHANGED
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  """
2
7
  This module provides a persistent key-value store based on SQLite.
3
8