mapillary-tools 0.13.3a1__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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +237 -16
- 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 +429 -181
- mapillary_tools/commands/__main__.py +12 -6
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -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 +31 -13
- mapillary_tools/constants.py +47 -6
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +46 -33
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +144 -120
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +291 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
- mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +53 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +160 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
- mapillary_tools/geotag/video_extractors/native.py +157 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +7 -13
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +155 -392
- mapillary_tools/process_sequence_properties.py +562 -208
- mapillary_tools/sample_video.py +13 -20
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +111 -58
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +42 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.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,14 +13,13 @@ 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
|
|
|
18
19
|
|
|
19
20
|
def _normalize_path(
|
|
20
21
|
video_import_path: Path, skip_subfolders: bool
|
|
21
|
-
) ->
|
|
22
|
+
) -> tuple[Path, list[Path]]:
|
|
22
23
|
if video_import_path.is_dir():
|
|
23
24
|
video_list = utils.find_videos(
|
|
24
25
|
[video_import_path], skip_subfolders=skip_subfolders
|
|
@@ -46,12 +47,11 @@ 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,
|
|
53
53
|
video_duration_ratio=constants.VIDEO_DURATION_RATIO,
|
|
54
|
-
video_start_time:
|
|
54
|
+
video_start_time: str | None = None,
|
|
55
55
|
skip_sample_errors: bool = False,
|
|
56
56
|
rerun: bool = False,
|
|
57
57
|
) -> None:
|
|
@@ -62,7 +62,7 @@ def sample_video(
|
|
|
62
62
|
f"Expect either non-negative video_sample_distance or positive video_sample_interval but got {video_sample_distance} and {video_sample_interval} respectively"
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
-
video_start_time_dt:
|
|
65
|
+
video_start_time_dt: datetime.datetime | None = None
|
|
66
66
|
if video_start_time is not None:
|
|
67
67
|
try:
|
|
68
68
|
video_start_time_dt = types.map_capture_time_to_datetime(video_start_time)
|
|
@@ -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(
|
|
@@ -189,7 +179,7 @@ def _sample_single_video_by_interval(
|
|
|
189
179
|
sample_dir: Path,
|
|
190
180
|
sample_interval: float,
|
|
191
181
|
duration_ratio: float,
|
|
192
|
-
start_time:
|
|
182
|
+
start_time: datetime.datetime | None = None,
|
|
193
183
|
) -> None:
|
|
194
184
|
ffmpeg = ffmpeglib.FFMPEG(constants.FFMPEG_PATH, constants.FFPROBE_PATH)
|
|
195
185
|
|
|
@@ -229,7 +219,7 @@ def _sample_video_stream_by_distance(
|
|
|
229
219
|
points: T.Sequence[geo.Point],
|
|
230
220
|
video_track_parser: mp4_sample_parser.TrackBoxParser,
|
|
231
221
|
sample_distance: float,
|
|
232
|
-
) ->
|
|
222
|
+
) -> dict[int, tuple[mp4_sample_parser.Sample, geo.Point]]:
|
|
233
223
|
"""
|
|
234
224
|
Locate video frames along the track (points), then resample them by the minimal sample_distance, and return the sparse frames.
|
|
235
225
|
"""
|
|
@@ -285,7 +275,7 @@ def _sample_single_video_by_distance(
|
|
|
285
275
|
video_path: Path,
|
|
286
276
|
sample_dir: Path,
|
|
287
277
|
sample_distance: float,
|
|
288
|
-
start_time:
|
|
278
|
+
start_time: datetime.datetime | None = None,
|
|
289
279
|
) -> None:
|
|
290
280
|
ffmpeg = ffmpeglib.FFMPEG(constants.FFMPEG_PATH, constants.FFPROBE_PATH)
|
|
291
281
|
|
|
@@ -299,9 +289,12 @@ def _sample_single_video_by_distance(
|
|
|
299
289
|
)
|
|
300
290
|
|
|
301
291
|
LOG.info("Extracting video metdata")
|
|
302
|
-
|
|
303
|
-
|
|
292
|
+
|
|
293
|
+
video_metadatas = geotag_videos_from_video.GeotagVideosFromVideo().to_description(
|
|
294
|
+
[video_path]
|
|
304
295
|
)
|
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
2
|
-
import typing as T
|
|
3
4
|
from enum import Enum, unique
|
|
4
5
|
|
|
5
6
|
from .geo import Point
|
|
@@ -12,16 +13,8 @@ class GPSFix(Enum):
|
|
|
12
13
|
FIX_3D = 3
|
|
13
14
|
|
|
14
15
|
|
|
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
16
|
@dataclasses.dataclass(order=True)
|
|
24
|
-
class
|
|
17
|
+
class TimestampedMeasurement:
|
|
25
18
|
"""Base class for all telemetry measurements.
|
|
26
19
|
|
|
27
20
|
All telemetry measurements must have a timestamp in seconds.
|
|
@@ -32,8 +25,28 @@ class TelemetryMeasurement:
|
|
|
32
25
|
time: float
|
|
33
26
|
|
|
34
27
|
|
|
28
|
+
@dataclasses.dataclass
|
|
29
|
+
class GPSPoint(TimestampedMeasurement, Point):
|
|
30
|
+
epoch_time: float | None
|
|
31
|
+
fix: GPSFix | None
|
|
32
|
+
precision: float | None
|
|
33
|
+
ground_speed: float | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclasses.dataclass
|
|
37
|
+
class CAMMGPSPoint(TimestampedMeasurement, Point):
|
|
38
|
+
time_gps_epoch: float
|
|
39
|
+
gps_fix_type: int
|
|
40
|
+
horizontal_accuracy: float
|
|
41
|
+
vertical_accuracy: float
|
|
42
|
+
velocity_east: float
|
|
43
|
+
velocity_north: float
|
|
44
|
+
velocity_up: float
|
|
45
|
+
speed_accuracy: float
|
|
46
|
+
|
|
47
|
+
|
|
35
48
|
@dataclasses.dataclass(order=True)
|
|
36
|
-
class GyroscopeData(
|
|
49
|
+
class GyroscopeData(TimestampedMeasurement):
|
|
37
50
|
"""Gyroscope signal in radians/seconds around XYZ axes of the camera."""
|
|
38
51
|
|
|
39
52
|
x: float
|
|
@@ -42,7 +55,7 @@ class GyroscopeData(TelemetryMeasurement):
|
|
|
42
55
|
|
|
43
56
|
|
|
44
57
|
@dataclasses.dataclass(order=True)
|
|
45
|
-
class AccelerationData(
|
|
58
|
+
class AccelerationData(TimestampedMeasurement):
|
|
46
59
|
"""Accelerometer reading in meters/second^2 along XYZ axes of the camera."""
|
|
47
60
|
|
|
48
61
|
x: float
|
|
@@ -51,7 +64,7 @@ class AccelerationData(TelemetryMeasurement):
|
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
@dataclasses.dataclass(order=True)
|
|
54
|
-
class MagnetometerData(
|
|
67
|
+
class MagnetometerData(TimestampedMeasurement):
|
|
55
68
|
"""Ambient magnetic field."""
|
|
56
69
|
|
|
57
70
|
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,35 +33,40 @@ _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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
MAPFilename: T.Optional[str] = None
|
|
60
|
-
filesize: T.Optional[int] = None
|
|
61
|
-
|
|
62
|
-
def update_md5sum(self, image_data: T.Optional[T.BinaryIO] = None) -> None:
|
|
56
|
+
md5sum: str | None = None
|
|
57
|
+
width: int | None = None
|
|
58
|
+
height: int | None = None
|
|
59
|
+
MAPSequenceUUID: str | None = None
|
|
60
|
+
MAPDeviceMake: str | None = None
|
|
61
|
+
MAPDeviceModel: str | None = None
|
|
62
|
+
MAPGPSAccuracyMeters: float | None = None
|
|
63
|
+
MAPCameraUUID: str | None = None
|
|
64
|
+
MAPOrientation: int | None = None
|
|
65
|
+
MAPMetaTags: dict | None = None
|
|
66
|
+
MAPFilename: str | None = None
|
|
67
|
+
filesize: int | None = None
|
|
68
|
+
|
|
69
|
+
def update_md5sum(self, image_data: T.BinaryIO | None = None) -> None:
|
|
63
70
|
if self.md5sum is None:
|
|
64
71
|
if image_data is None:
|
|
65
72
|
with self.filename.open("rb") as fp:
|
|
@@ -77,13 +84,12 @@ class ImageMetadata(geo.Point):
|
|
|
77
84
|
@dataclasses.dataclass
|
|
78
85
|
class VideoMetadata:
|
|
79
86
|
filename: Path
|
|
80
|
-
# if None or absent, it will be calculated
|
|
81
|
-
md5sum: T.Optional[str]
|
|
82
87
|
filetype: FileType
|
|
83
88
|
points: T.Sequence[geo.Point]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
md5sum: str | None = None
|
|
90
|
+
make: str | None = None
|
|
91
|
+
model: str | None = None
|
|
92
|
+
filesize: int | None = None
|
|
87
93
|
|
|
88
94
|
def update_md5sum(self) -> None:
|
|
89
95
|
if self.md5sum is None:
|
|
@@ -94,7 +100,7 @@ class VideoMetadata:
|
|
|
94
100
|
@dataclasses.dataclass
|
|
95
101
|
class ErrorMetadata:
|
|
96
102
|
filename: Path
|
|
97
|
-
filetype:
|
|
103
|
+
filetype: FileType
|
|
98
104
|
error: Exception
|
|
99
105
|
|
|
100
106
|
|
|
@@ -104,8 +110,37 @@ Metadata = T.Union[ImageMetadata, VideoMetadata]
|
|
|
104
110
|
MetadataOrError = T.Union[Metadata, ErrorMetadata]
|
|
105
111
|
|
|
106
112
|
|
|
113
|
+
# Assume {GOPRO, VIDEO} are the NATIVE_VIDEO_FILETYPES:
|
|
114
|
+
# a | b = result
|
|
115
|
+
# {CAMM} | {GOPRO} = {}
|
|
116
|
+
# {CAMM} | {GOPRO, VIDEO} = {CAMM}
|
|
117
|
+
# {GOPRO} | {GOPRO, VIDEO} = {GOPRO}
|
|
118
|
+
# {GOPRO} | {VIDEO} = {GOPRO}
|
|
119
|
+
# {CAMM, GOPRO} | {VIDEO} = {CAMM, GOPRO}
|
|
120
|
+
# {VIDEO} | {VIDEO} = {CAMM, GOPRO, VIDEO}
|
|
121
|
+
def combine_filetype_filters(
|
|
122
|
+
a: set[FileType] | None, b: set[FileType] | None
|
|
123
|
+
) -> set[FileType] | None:
|
|
124
|
+
if a is None:
|
|
125
|
+
return b
|
|
126
|
+
|
|
127
|
+
if b is None:
|
|
128
|
+
return a
|
|
129
|
+
|
|
130
|
+
# VIDEO is a superset of NATIVE_VIDEO_FILETYPES,
|
|
131
|
+
# so we add NATIVE_VIDEO_FILETYPES to each set for intersection later
|
|
132
|
+
|
|
133
|
+
if FileType.VIDEO in a:
|
|
134
|
+
a = a | NATIVE_VIDEO_FILETYPES
|
|
135
|
+
|
|
136
|
+
if FileType.VIDEO in b:
|
|
137
|
+
b = b | NATIVE_VIDEO_FILETYPES
|
|
138
|
+
|
|
139
|
+
return a.intersection(b)
|
|
140
|
+
|
|
141
|
+
|
|
107
142
|
class UserItem(TypedDict, total=False):
|
|
108
|
-
MAPOrganizationKey:
|
|
143
|
+
MAPOrganizationKey: int | str
|
|
109
144
|
# Not in use. Keep here for back-compatibility
|
|
110
145
|
MAPSettingsUsername: str
|
|
111
146
|
MAPSettingsUserKey: str
|
|
@@ -144,23 +179,22 @@ class ImageDescription(_SequenceOnly, _Image, MetaProperties, total=True):
|
|
|
144
179
|
# filename is required
|
|
145
180
|
filename: str
|
|
146
181
|
# if None or absent, it will be calculated
|
|
147
|
-
md5sum:
|
|
182
|
+
md5sum: str | None
|
|
148
183
|
filetype: Literal["image"]
|
|
149
|
-
filesize:
|
|
184
|
+
filesize: int | None
|
|
150
185
|
|
|
151
186
|
|
|
152
187
|
class _VideoDescriptionRequired(TypedDict, total=True):
|
|
153
188
|
filename: str
|
|
154
|
-
|
|
155
|
-
md5sum: T.Optional[str]
|
|
189
|
+
md5sum: str | None
|
|
156
190
|
filetype: str
|
|
157
|
-
MAPGPSTrack:
|
|
191
|
+
MAPGPSTrack: list[T.Sequence[float | int | None]]
|
|
158
192
|
|
|
159
193
|
|
|
160
194
|
class VideoDescription(_VideoDescriptionRequired, total=False):
|
|
161
195
|
MAPDeviceMake: str
|
|
162
196
|
MAPDeviceModel: str
|
|
163
|
-
filesize:
|
|
197
|
+
filesize: int | None
|
|
164
198
|
|
|
165
199
|
|
|
166
200
|
class _ErrorDescription(TypedDict, total=False):
|
|
@@ -168,7 +202,7 @@ class _ErrorDescription(TypedDict, total=False):
|
|
|
168
202
|
type: str
|
|
169
203
|
message: str
|
|
170
204
|
# vars is optional
|
|
171
|
-
vars:
|
|
205
|
+
vars: dict
|
|
172
206
|
|
|
173
207
|
|
|
174
208
|
class _ImageDescriptionErrorRequired(TypedDict, total=True):
|
|
@@ -180,8 +214,26 @@ class ImageDescriptionError(_ImageDescriptionErrorRequired, total=False):
|
|
|
180
214
|
filetype: str
|
|
181
215
|
|
|
182
216
|
|
|
217
|
+
M = T.TypeVar("M")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def separate_errors(
|
|
221
|
+
metadatas: T.Iterable[M | ErrorMetadata],
|
|
222
|
+
) -> tuple[list[M], list[ErrorMetadata]]:
|
|
223
|
+
good: list[M] = []
|
|
224
|
+
bad: list[ErrorMetadata] = []
|
|
225
|
+
|
|
226
|
+
for metadata in metadatas:
|
|
227
|
+
if isinstance(metadata, ErrorMetadata):
|
|
228
|
+
bad.append(metadata)
|
|
229
|
+
else:
|
|
230
|
+
good.append(metadata)
|
|
231
|
+
|
|
232
|
+
return good, bad
|
|
233
|
+
|
|
234
|
+
|
|
183
235
|
def _describe_error_desc(
|
|
184
|
-
exc: Exception, filename: Path, filetype:
|
|
236
|
+
exc: Exception, filename: Path, filetype: FileType | None
|
|
185
237
|
) -> ImageDescriptionError:
|
|
186
238
|
err: _ErrorDescription = {
|
|
187
239
|
"type": exc.__class__.__name__,
|
|
@@ -210,7 +262,7 @@ def _describe_error_desc(
|
|
|
210
262
|
|
|
211
263
|
|
|
212
264
|
def describe_error_metadata(
|
|
213
|
-
exc: Exception, filename: Path, filetype:
|
|
265
|
+
exc: Exception, filename: Path, filetype: FileType
|
|
214
266
|
) -> ErrorMetadata:
|
|
215
267
|
return ErrorMetadata(filename=filename, filetype=filetype, error=exc)
|
|
216
268
|
|
|
@@ -278,7 +330,6 @@ ImageDescriptionEXIFSchema = {
|
|
|
278
330
|
"MAPDeviceModel": {"type": "string"},
|
|
279
331
|
"MAPGPSAccuracyMeters": {"type": "number"},
|
|
280
332
|
"MAPCameraUUID": {"type": "string"},
|
|
281
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
282
333
|
"MAPFilename": {
|
|
283
334
|
"type": "string",
|
|
284
335
|
"description": "The base filename of the image",
|
|
@@ -341,7 +392,7 @@ VideoDescriptionSchema = {
|
|
|
341
392
|
}
|
|
342
393
|
|
|
343
394
|
|
|
344
|
-
def merge_schema(*schemas:
|
|
395
|
+
def merge_schema(*schemas: dict) -> dict:
|
|
345
396
|
for s in schemas:
|
|
346
397
|
assert s.get("type") == "object", "must be all object schemas"
|
|
347
398
|
properties = {}
|
|
@@ -436,11 +487,11 @@ def validate_image_desc(desc: T.Any) -> None:
|
|
|
436
487
|
jsonschema.validate(instance=desc, schema=ImageDescriptionFileSchema)
|
|
437
488
|
except jsonschema.ValidationError as ex:
|
|
438
489
|
# do not use str(ex) which is more verbose
|
|
439
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
490
|
+
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
440
491
|
try:
|
|
441
492
|
map_capture_time_to_datetime(desc["MAPCaptureTime"])
|
|
442
493
|
except ValueError as ex:
|
|
443
|
-
raise exceptions.MapillaryMetadataValidationError(str(ex))
|
|
494
|
+
raise exceptions.MapillaryMetadataValidationError(str(ex)) from ex
|
|
444
495
|
|
|
445
496
|
|
|
446
497
|
def validate_video_desc(desc: T.Any) -> None:
|
|
@@ -448,12 +499,12 @@ def validate_video_desc(desc: T.Any) -> None:
|
|
|
448
499
|
jsonschema.validate(instance=desc, schema=VideoDescriptionFileSchema)
|
|
449
500
|
except jsonschema.ValidationError as ex:
|
|
450
501
|
# do not use str(ex) which is more verbose
|
|
451
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
502
|
+
raise exceptions.MapillaryMetadataValidationError(ex.message) from ex
|
|
452
503
|
|
|
453
504
|
|
|
454
|
-
def datetime_to_map_capture_time(time:
|
|
505
|
+
def datetime_to_map_capture_time(time: datetime.datetime | int | float) -> str:
|
|
455
506
|
if isinstance(time, (float, int)):
|
|
456
|
-
dt = datetime.datetime.
|
|
507
|
+
dt = datetime.datetime.fromtimestamp(time, datetime.timezone.utc)
|
|
457
508
|
# otherwise it will be assumed to be in local time
|
|
458
509
|
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
459
510
|
else:
|
|
@@ -552,7 +603,7 @@ def from_desc(desc):
|
|
|
552
603
|
|
|
553
604
|
|
|
554
605
|
def _from_image_desc(desc) -> ImageMetadata:
|
|
555
|
-
kwargs:
|
|
606
|
+
kwargs: dict = {}
|
|
556
607
|
for k, v in desc.items():
|
|
557
608
|
if k not in [
|
|
558
609
|
"filename",
|
|
@@ -582,7 +633,7 @@ def _from_image_desc(desc) -> ImageMetadata:
|
|
|
582
633
|
)
|
|
583
634
|
|
|
584
635
|
|
|
585
|
-
def _encode_point(p: geo.Point) -> T.Sequence[
|
|
636
|
+
def _encode_point(p: geo.Point) -> T.Sequence[float | int | None]:
|
|
586
637
|
entry = [
|
|
587
638
|
int(p.time * 1000),
|
|
588
639
|
round(p.lon, _COORDINATES_PRECISION),
|
|
@@ -644,15 +695,16 @@ def validate_and_fail_metadata(metadata: MetadataOrError) -> MetadataOrError:
|
|
|
644
695
|
if isinstance(metadata, ErrorMetadata):
|
|
645
696
|
return metadata
|
|
646
697
|
|
|
647
|
-
|
|
698
|
+
if isinstance(metadata, ImageMetadata):
|
|
699
|
+
filetype = FileType.IMAGE
|
|
700
|
+
validate = validate_image_desc
|
|
701
|
+
else:
|
|
702
|
+
assert isinstance(metadata, VideoMetadata)
|
|
703
|
+
filetype = metadata.filetype
|
|
704
|
+
validate = validate_video_desc
|
|
705
|
+
|
|
648
706
|
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))
|
|
707
|
+
validate(as_desc(metadata))
|
|
656
708
|
except exceptions.MapillaryMetadataValidationError as ex:
|
|
657
709
|
# rethrow because the original error is too verbose
|
|
658
710
|
return describe_error_metadata(
|
|
@@ -686,10 +738,10 @@ def desc_file_to_exif(
|
|
|
686
738
|
|
|
687
739
|
|
|
688
740
|
def group_and_sort_images(
|
|
689
|
-
metadatas: T.
|
|
690
|
-
) ->
|
|
741
|
+
metadatas: T.Iterable[ImageMetadata],
|
|
742
|
+
) -> dict[str, list[ImageMetadata]]:
|
|
691
743
|
# group metadatas by uuid
|
|
692
|
-
sequences_by_uuid:
|
|
744
|
+
sequences_by_uuid: dict[str, list[ImageMetadata]] = {}
|
|
693
745
|
missing_sequence_uuid = str(uuid.uuid4())
|
|
694
746
|
for metadata in metadatas:
|
|
695
747
|
if metadata.MAPSequenceUUID is None:
|
|
@@ -709,9 +761,10 @@ def group_and_sort_images(
|
|
|
709
761
|
return sorted_sequences_by_uuid
|
|
710
762
|
|
|
711
763
|
|
|
712
|
-
def
|
|
764
|
+
def update_sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
|
|
713
765
|
md5 = hashlib.md5()
|
|
714
766
|
for metadata in sequence:
|
|
767
|
+
metadata.update_md5sum()
|
|
715
768
|
assert isinstance(metadata.md5sum, str), "md5sum should be calculated"
|
|
716
769
|
md5.update(metadata.md5sum.encode("utf-8"))
|
|
717
770
|
return md5.hexdigest()
|