mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +106 -7
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +2 -0
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +311 -261
- mapillary_tools/upload_api_v4.py +55 -95
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
mapillary_tools/sample_video.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import datetime
|
|
2
4
|
import logging
|
|
3
5
|
import os
|
|
@@ -11,7 +13,6 @@ from . import constants, exceptions, ffmpeg as ffmpeglib, geo, types, utils
|
|
|
11
13
|
from .exif_write import ExifEdit
|
|
12
14
|
from .geotag import geotag_videos_from_video
|
|
13
15
|
from .mp4 import mp4_sample_parser
|
|
14
|
-
from .process_geotag_properties import GeotagSource
|
|
15
16
|
|
|
16
17
|
LOG = logging.getLogger(__name__)
|
|
17
18
|
|
|
@@ -46,7 +47,6 @@ def sample_video(
|
|
|
46
47
|
video_import_path: Path,
|
|
47
48
|
import_path: Path,
|
|
48
49
|
# None if called from the sample_video command
|
|
49
|
-
geotag_source: T.Optional[GeotagSource] = None,
|
|
50
50
|
skip_subfolders=False,
|
|
51
51
|
video_sample_distance=constants.VIDEO_SAMPLE_DISTANCE,
|
|
52
52
|
video_sample_interval=constants.VIDEO_SAMPLE_INTERVAL,
|
|
@@ -86,16 +86,6 @@ def sample_video(
|
|
|
86
86
|
elif sample_dir.is_file():
|
|
87
87
|
os.remove(sample_dir)
|
|
88
88
|
|
|
89
|
-
if geotag_source is None:
|
|
90
|
-
geotag_source = "exif"
|
|
91
|
-
|
|
92
|
-
# If it is not exif, then we use the legacy interval-based sample and geotag them in "process" for backward compatibility
|
|
93
|
-
if geotag_source not in ["exif"]:
|
|
94
|
-
if 0 <= video_sample_distance:
|
|
95
|
-
raise exceptions.MapillaryBadParameterError(
|
|
96
|
-
f'Geotagging from "{geotag_source}" works with the legacy interval-based sampling only. To switch back, rerun the command with "--video_sample_distance -1 --video_sample_interval 2"'
|
|
97
|
-
)
|
|
98
|
-
|
|
99
89
|
for video_path in video_list:
|
|
100
90
|
# need to resolve video_path because video_dir might be absolute
|
|
101
91
|
sample_dir = Path(import_path).joinpath(
|
|
@@ -299,9 +289,12 @@ def _sample_single_video_by_distance(
|
|
|
299
289
|
)
|
|
300
290
|
|
|
301
291
|
LOG.info("Extracting video metdata")
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
292
|
+
|
|
293
|
+
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo(
|
|
294
|
+
[video_path]
|
|
295
|
+
).to_description()
|
|
296
|
+
assert len(video_metadatas) == 1, "expect 1 video metadata"
|
|
297
|
+
video_metadata = video_metadatas[0]
|
|
305
298
|
if isinstance(video_metadata, types.ErrorMetadata):
|
|
306
299
|
LOG.warning(str(video_metadata.error))
|
|
307
300
|
return
|
mapillary_tools/telemetry.py
CHANGED
|
@@ -12,16 +12,8 @@ class GPSFix(Enum):
|
|
|
12
12
|
FIX_3D = 3
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@dataclasses.dataclass
|
|
16
|
-
class GPSPoint(Point):
|
|
17
|
-
epoch_time: T.Optional[float]
|
|
18
|
-
fix: T.Optional[GPSFix]
|
|
19
|
-
precision: T.Optional[float]
|
|
20
|
-
ground_speed: T.Optional[float]
|
|
21
|
-
|
|
22
|
-
|
|
23
15
|
@dataclasses.dataclass(order=True)
|
|
24
|
-
class
|
|
16
|
+
class TimestampedMeasurement:
|
|
25
17
|
"""Base class for all telemetry measurements.
|
|
26
18
|
|
|
27
19
|
All telemetry measurements must have a timestamp in seconds.
|
|
@@ -32,8 +24,28 @@ class TelemetryMeasurement:
|
|
|
32
24
|
time: float
|
|
33
25
|
|
|
34
26
|
|
|
27
|
+
@dataclasses.dataclass
|
|
28
|
+
class GPSPoint(TimestampedMeasurement, Point):
|
|
29
|
+
epoch_time: T.Optional[float]
|
|
30
|
+
fix: T.Optional[GPSFix]
|
|
31
|
+
precision: T.Optional[float]
|
|
32
|
+
ground_speed: T.Optional[float]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class CAMMGPSPoint(TimestampedMeasurement, Point):
|
|
37
|
+
time_gps_epoch: float
|
|
38
|
+
gps_fix_type: int
|
|
39
|
+
horizontal_accuracy: float
|
|
40
|
+
vertical_accuracy: float
|
|
41
|
+
velocity_east: float
|
|
42
|
+
velocity_north: float
|
|
43
|
+
velocity_up: float
|
|
44
|
+
speed_accuracy: float
|
|
45
|
+
|
|
46
|
+
|
|
35
47
|
@dataclasses.dataclass(order=True)
|
|
36
|
-
class GyroscopeData(
|
|
48
|
+
class GyroscopeData(TimestampedMeasurement):
|
|
37
49
|
"""Gyroscope signal in radians/seconds around XYZ axes of the camera."""
|
|
38
50
|
|
|
39
51
|
x: float
|
|
@@ -42,7 +54,7 @@ class GyroscopeData(TelemetryMeasurement):
|
|
|
42
54
|
|
|
43
55
|
|
|
44
56
|
@dataclasses.dataclass(order=True)
|
|
45
|
-
class AccelerationData(
|
|
57
|
+
class AccelerationData(TimestampedMeasurement):
|
|
46
58
|
"""Accelerometer reading in meters/second^2 along XYZ axes of the camera."""
|
|
47
59
|
|
|
48
60
|
x: float
|
|
@@ -51,7 +63,7 @@ class AccelerationData(TelemetryMeasurement):
|
|
|
51
63
|
|
|
52
64
|
|
|
53
65
|
@dataclasses.dataclass(order=True)
|
|
54
|
-
class MagnetometerData(
|
|
66
|
+
class MagnetometerData(TimestampedMeasurement):
|
|
55
67
|
"""Ambient magnetic field."""
|
|
56
68
|
|
|
57
69
|
x: float
|
mapillary_tools/types.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
2
4
|
import datetime
|
|
3
5
|
import enum
|
|
@@ -31,19 +33,28 @@ _ANGLE_PRECISION = 3
|
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class FileType(enum.Enum):
|
|
36
|
+
IMAGE = "image"
|
|
37
|
+
ZIP = "zip"
|
|
38
|
+
# VIDEO is a superset of all NATIVE_VIDEO_FILETYPES below.
|
|
39
|
+
# It also contains the videos that external geotag source (e.g. exiftool) supports
|
|
40
|
+
VIDEO = "video"
|
|
34
41
|
BLACKVUE = "blackvue"
|
|
35
42
|
CAMM = "camm"
|
|
36
43
|
GOPRO = "gopro"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
NATIVE_VIDEO_FILETYPES = {
|
|
47
|
+
FileType.BLACKVUE,
|
|
48
|
+
FileType.CAMM,
|
|
49
|
+
FileType.GOPRO,
|
|
50
|
+
}
|
|
40
51
|
|
|
41
52
|
|
|
42
53
|
@dataclasses.dataclass
|
|
43
54
|
class ImageMetadata(geo.Point):
|
|
44
55
|
filename: Path
|
|
45
56
|
# if None or absent, it will be calculated
|
|
46
|
-
md5sum: T.Optional[str]
|
|
57
|
+
md5sum: T.Optional[str] = None
|
|
47
58
|
# filetype: is always FileType.IMAGE
|
|
48
59
|
width: T.Optional[int] = None
|
|
49
60
|
height: T.Optional[int] = None
|
|
@@ -55,7 +66,6 @@ class ImageMetadata(geo.Point):
|
|
|
55
66
|
MAPOrientation: T.Optional[int] = None
|
|
56
67
|
# deprecated since v0.10.0; keep here for compatibility
|
|
57
68
|
MAPMetaTags: T.Optional[T.Dict] = None
|
|
58
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
59
69
|
MAPFilename: T.Optional[str] = None
|
|
60
70
|
filesize: T.Optional[int] = None
|
|
61
71
|
|
|
@@ -78,9 +88,9 @@ class ImageMetadata(geo.Point):
|
|
|
78
88
|
class VideoMetadata:
|
|
79
89
|
filename: Path
|
|
80
90
|
# if None or absent, it will be calculated
|
|
81
|
-
md5sum: T.Optional[str]
|
|
82
91
|
filetype: FileType
|
|
83
92
|
points: T.Sequence[geo.Point]
|
|
93
|
+
md5sum: T.Optional[str] = None
|
|
84
94
|
make: T.Optional[str] = None
|
|
85
95
|
model: T.Optional[str] = None
|
|
86
96
|
filesize: T.Optional[int] = None
|
|
@@ -94,7 +104,7 @@ class VideoMetadata:
|
|
|
94
104
|
@dataclasses.dataclass
|
|
95
105
|
class ErrorMetadata:
|
|
96
106
|
filename: Path
|
|
97
|
-
filetype:
|
|
107
|
+
filetype: FileType
|
|
98
108
|
error: Exception
|
|
99
109
|
|
|
100
110
|
|
|
@@ -104,6 +114,35 @@ Metadata = T.Union[ImageMetadata, VideoMetadata]
|
|
|
104
114
|
MetadataOrError = T.Union[Metadata, ErrorMetadata]
|
|
105
115
|
|
|
106
116
|
|
|
117
|
+
# Assume {GOPRO, VIDEO} are the NATIVE_VIDEO_FILETYPES:
|
|
118
|
+
# a | b = result
|
|
119
|
+
# {CAMM} | {GOPRO} = {}
|
|
120
|
+
# {CAMM} | {GOPRO, VIDEO} = {CAMM}
|
|
121
|
+
# {GOPRO} | {GOPRO, VIDEO} = {GOPRO}
|
|
122
|
+
# {GOPRO} | {VIDEO} = {GOPRO}
|
|
123
|
+
# {CAMM, GOPRO} | {VIDEO} = {CAMM, GOPRO}
|
|
124
|
+
# {VIDEO} | {VIDEO} = {CAMM, GOPRO, VIDEO}
|
|
125
|
+
def combine_filetype_filters(
|
|
126
|
+
a: set[FileType] | None, b: set[FileType] | None
|
|
127
|
+
) -> set[FileType] | None:
|
|
128
|
+
if a is None:
|
|
129
|
+
return b
|
|
130
|
+
|
|
131
|
+
if b is None:
|
|
132
|
+
return a
|
|
133
|
+
|
|
134
|
+
# VIDEO is a superset of NATIVE_VIDEO_FILETYPES,
|
|
135
|
+
# so we add NATIVE_VIDEO_FILETYPES to each set for intersection later
|
|
136
|
+
|
|
137
|
+
if FileType.VIDEO in a:
|
|
138
|
+
a = a | NATIVE_VIDEO_FILETYPES
|
|
139
|
+
|
|
140
|
+
if FileType.VIDEO in b:
|
|
141
|
+
b = b | NATIVE_VIDEO_FILETYPES
|
|
142
|
+
|
|
143
|
+
return a.intersection(b)
|
|
144
|
+
|
|
145
|
+
|
|
107
146
|
class UserItem(TypedDict, total=False):
|
|
108
147
|
MAPOrganizationKey: T.Union[int, str]
|
|
109
148
|
# Not in use. Keep here for back-compatibility
|
|
@@ -180,6 +219,24 @@ class ImageDescriptionError(_ImageDescriptionErrorRequired, total=False):
|
|
|
180
219
|
filetype: str
|
|
181
220
|
|
|
182
221
|
|
|
222
|
+
M = T.TypeVar("M")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def separate_errors(
|
|
226
|
+
metadatas: T.Iterable[M | ErrorMetadata],
|
|
227
|
+
) -> tuple[list[M], list[ErrorMetadata]]:
|
|
228
|
+
good: list[M] = []
|
|
229
|
+
bad: list[ErrorMetadata] = []
|
|
230
|
+
|
|
231
|
+
for metadata in metadatas:
|
|
232
|
+
if isinstance(metadata, ErrorMetadata):
|
|
233
|
+
bad.append(metadata)
|
|
234
|
+
else:
|
|
235
|
+
good.append(metadata)
|
|
236
|
+
|
|
237
|
+
return good, bad
|
|
238
|
+
|
|
239
|
+
|
|
183
240
|
def _describe_error_desc(
|
|
184
241
|
exc: Exception, filename: Path, filetype: T.Optional[FileType]
|
|
185
242
|
) -> ImageDescriptionError:
|
|
@@ -210,7 +267,7 @@ def _describe_error_desc(
|
|
|
210
267
|
|
|
211
268
|
|
|
212
269
|
def describe_error_metadata(
|
|
213
|
-
exc: Exception, filename: Path, filetype:
|
|
270
|
+
exc: Exception, filename: Path, filetype: FileType
|
|
214
271
|
) -> ErrorMetadata:
|
|
215
272
|
return ErrorMetadata(filename=filename, filetype=filetype, error=exc)
|
|
216
273
|
|
|
@@ -278,7 +335,6 @@ ImageDescriptionEXIFSchema = {
|
|
|
278
335
|
"MAPDeviceModel": {"type": "string"},
|
|
279
336
|
"MAPGPSAccuracyMeters": {"type": "number"},
|
|
280
337
|
"MAPCameraUUID": {"type": "string"},
|
|
281
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
282
338
|
"MAPFilename": {
|
|
283
339
|
"type": "string",
|
|
284
340
|
"description": "The base filename of the image",
|
|
@@ -436,11 +492,11 @@ def validate_image_desc(desc: T.Any) -> None:
|
|
|
436
492
|
jsonschema.validate(instance=desc, schema=ImageDescriptionFileSchema)
|
|
437
493
|
except jsonschema.ValidationError as ex:
|
|
438
494
|
# do not use str(ex) which is more verbose
|
|
439
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
495
|
+
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
440
496
|
try:
|
|
441
497
|
map_capture_time_to_datetime(desc["MAPCaptureTime"])
|
|
442
498
|
except ValueError as ex:
|
|
443
|
-
raise exceptions.MapillaryMetadataValidationError(str(ex))
|
|
499
|
+
raise exceptions.MapillaryMetadataValidationError(str(ex)) from ex
|
|
444
500
|
|
|
445
501
|
|
|
446
502
|
def validate_video_desc(desc: T.Any) -> None:
|
|
@@ -448,12 +504,12 @@ def validate_video_desc(desc: T.Any) -> None:
|
|
|
448
504
|
jsonschema.validate(instance=desc, schema=VideoDescriptionFileSchema)
|
|
449
505
|
except jsonschema.ValidationError as ex:
|
|
450
506
|
# do not use str(ex) which is more verbose
|
|
451
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
507
|
+
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
452
508
|
|
|
453
509
|
|
|
454
510
|
def datetime_to_map_capture_time(time: T.Union[datetime.datetime, int, float]) -> str:
|
|
455
511
|
if isinstance(time, (float, int)):
|
|
456
|
-
dt = datetime.datetime.
|
|
512
|
+
dt = datetime.datetime.fromtimestamp(time, datetime.timezone.utc)
|
|
457
513
|
# otherwise it will be assumed to be in local time
|
|
458
514
|
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
459
515
|
else:
|
|
@@ -644,15 +700,16 @@ def validate_and_fail_metadata(metadata: MetadataOrError) -> MetadataOrError:
|
|
|
644
700
|
if isinstance(metadata, ErrorMetadata):
|
|
645
701
|
return metadata
|
|
646
702
|
|
|
647
|
-
|
|
703
|
+
if isinstance(metadata, ImageMetadata):
|
|
704
|
+
filetype = FileType.IMAGE
|
|
705
|
+
validate = validate_image_desc
|
|
706
|
+
else:
|
|
707
|
+
assert isinstance(metadata, VideoMetadata)
|
|
708
|
+
filetype = metadata.filetype
|
|
709
|
+
validate = validate_video_desc
|
|
710
|
+
|
|
648
711
|
try:
|
|
649
|
-
|
|
650
|
-
filetype = FileType.IMAGE
|
|
651
|
-
validate_image_desc(as_desc(metadata))
|
|
652
|
-
else:
|
|
653
|
-
assert isinstance(metadata, VideoMetadata)
|
|
654
|
-
filetype = metadata.filetype
|
|
655
|
-
validate_video_desc(as_desc(metadata))
|
|
712
|
+
validate(as_desc(metadata))
|
|
656
713
|
except exceptions.MapillaryMetadataValidationError as ex:
|
|
657
714
|
# rethrow because the original error is too verbose
|
|
658
715
|
return describe_error_metadata(
|
|
@@ -709,9 +766,10 @@ def group_and_sort_images(
|
|
|
709
766
|
return sorted_sequences_by_uuid
|
|
710
767
|
|
|
711
768
|
|
|
712
|
-
def
|
|
769
|
+
def update_sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
|
|
713
770
|
md5 = hashlib.md5()
|
|
714
771
|
for metadata in sequence:
|
|
772
|
+
metadata.update_md5sum()
|
|
715
773
|
assert isinstance(metadata.md5sum, str), "md5sum should be calculated"
|
|
716
774
|
md5.update(metadata.md5sum.encode("utf-8"))
|
|
717
775
|
return md5.hexdigest()
|