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.
- mapillary_tools/__init__.py +6 -1
- mapillary_tools/api_v4.py +5 -0
- mapillary_tools/authenticate.py +5 -1
- mapillary_tools/blackvue_parser.py +138 -20
- mapillary_tools/camm/camm_builder.py +5 -1
- mapillary_tools/camm/camm_parser.py +5 -0
- mapillary_tools/commands/__init__.py +5 -0
- mapillary_tools/commands/__main__.py +5 -0
- mapillary_tools/commands/authenticate.py +5 -0
- mapillary_tools/commands/process.py +12 -0
- mapillary_tools/commands/process_and_upload.py +5 -1
- mapillary_tools/commands/sample_video.py +5 -0
- mapillary_tools/commands/upload.py +5 -0
- mapillary_tools/commands/video_process.py +5 -0
- mapillary_tools/commands/video_process_and_upload.py +5 -1
- mapillary_tools/commands/zip.py +5 -0
- mapillary_tools/config.py +5 -0
- mapillary_tools/constants.py +13 -1
- mapillary_tools/exceptions.py +9 -0
- mapillary_tools/exif_read.py +89 -0
- mapillary_tools/exif_write.py +17 -0
- mapillary_tools/exiftool_read.py +89 -0
- mapillary_tools/exiftool_read_video.py +56 -0
- mapillary_tools/exiftool_runner.py +5 -0
- mapillary_tools/ffmpeg.py +5 -0
- mapillary_tools/geo.py +91 -31
- mapillary_tools/geotag/__init__.py +4 -0
- mapillary_tools/geotag/base.py +5 -0
- mapillary_tools/geotag/factory.py +5 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +5 -0
- mapillary_tools/geotag/geotag_images_from_exiftool.py +5 -0
- mapillary_tools/geotag/geotag_images_from_gpx.py +5 -0
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +5 -0
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +5 -0
- mapillary_tools/geotag/geotag_images_from_video.py +6 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +5 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +5 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +5 -0
- mapillary_tools/geotag/image_extractors/base.py +5 -0
- mapillary_tools/geotag/image_extractors/exif.py +6 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +5 -0
- mapillary_tools/geotag/options.py +5 -0
- mapillary_tools/geotag/utils.py +5 -0
- mapillary_tools/geotag/video_extractors/base.py +5 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +7 -0
- mapillary_tools/geotag/video_extractors/gpx.py +5 -0
- mapillary_tools/geotag/video_extractors/native.py +5 -0
- mapillary_tools/gpmf/gpmf_gps_filter.py +5 -0
- mapillary_tools/gpmf/gpmf_parser.py +5 -0
- mapillary_tools/gpmf/gps_filter.py +5 -0
- mapillary_tools/history.py +5 -0
- mapillary_tools/http.py +5 -1
- mapillary_tools/ipc.py +5 -0
- mapillary_tools/mp4/__init__.py +4 -0
- mapillary_tools/mp4/construct_mp4_parser.py +5 -0
- mapillary_tools/mp4/io_utils.py +5 -0
- mapillary_tools/mp4/mp4_sample_parser.py +20 -1
- mapillary_tools/mp4/simple_mp4_builder.py +5 -0
- mapillary_tools/mp4/simple_mp4_parser.py +5 -0
- mapillary_tools/process_geotag_properties.py +5 -0
- mapillary_tools/process_sequence_properties.py +213 -31
- mapillary_tools/sample_video.py +13 -1
- mapillary_tools/serializer/description.py +13 -0
- mapillary_tools/serializer/gpx.py +5 -1
- mapillary_tools/store.py +5 -0
- mapillary_tools/telemetry.py +108 -0
- mapillary_tools/types.py +6 -0
- mapillary_tools/upload.py +5 -0
- mapillary_tools/upload_api_v4.py +5 -0
- mapillary_tools/uploader.py +9 -0
- mapillary_tools/utils.py +16 -1
- {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/METADATA +8 -1
- mapillary_tools-0.14.6.dist-info/RECORD +77 -0
- {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/WHEEL +1 -1
- mapillary_tools-0.14.5.dist-info/RECORD +0 -77
- {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.5.dist-info → mapillary_tools-0.14.6.dist-info}/top_level.txt +0 -0
mapillary_tools/history.py
CHANGED
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
mapillary_tools/mp4/__init__.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
|
# pyre-ignore-all-errors[5, 16, 21, 58]
|
|
2
7
|
from __future__ import annotations
|
|
3
8
|
|
mapillary_tools/mp4/io_utils.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
|
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
|
-
|
|
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
|
# 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 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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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=
|
|
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(
|
mapillary_tools/sample_video.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
|
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
|
-
|
|
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