mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0__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 +198 -55
- mapillary_tools/authenticate.py +326 -64
- mapillary_tools/blackvue_parser.py +195 -0
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +429 -181
- mapillary_tools/commands/__main__.py +10 -6
- 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 +44 -13
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +65 -26
- mapillary_tools/constants.py +141 -18
- mapillary_tools/exceptions.py +37 -34
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +10 -8
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +97 -47
- mapillary_tools/exiftool_runner.py +57 -0
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +158 -118
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +307 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +136 -85
- mapillary_tools/geotag/geotag_images_from_gpx.py +60 -124
- 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 +88 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +52 -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 +182 -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/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +160 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +134 -20
- mapillary_tools/mp4/construct_mp4_parser.py +17 -10
- 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 +184 -414
- mapillary_tools/process_sequence_properties.py +594 -225
- mapillary_tools/sample_video.py +20 -26
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +98 -611
- mapillary_tools/upload.py +411 -387
- mapillary_tools/upload_api_v4.py +167 -142
- mapillary_tools/uploader.py +804 -284
- mapillary_tools/utils.py +49 -18
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/METADATA +93 -35
- mapillary_tools-0.14.0.dist-info/RECORD +75 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/blackvue_parser.py +0 -118
- 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/gpx_parser.py +0 -108
- 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.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0.dist-info}/top_level.txt +0 -0
mapillary_tools/types.py
CHANGED
|
@@ -1,65 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
1
4
|
import dataclasses
|
|
2
|
-
import datetime
|
|
3
5
|
import enum
|
|
4
6
|
import hashlib
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
7
|
import typing as T
|
|
8
8
|
import uuid
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Literal, TypedDict
|
|
11
|
-
|
|
12
|
-
import jsonschema
|
|
13
|
-
|
|
14
|
-
from . import exceptions, geo, utils
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
# http://wiki.gis.com/wiki/index.php/Decimal_degrees
|
|
18
|
-
# decimal places degrees distance
|
|
19
|
-
# 0 1.0 111 km
|
|
20
|
-
# 1 0.1 11.1 km
|
|
21
|
-
# 2 0.01 1.11 km
|
|
22
|
-
# 3 0.001 111 m
|
|
23
|
-
# 4 0.0001 11.1 m
|
|
24
|
-
# 5 0.00001 1.11 m
|
|
25
|
-
# 6 0.000001 0.111 m
|
|
26
|
-
# 7 0.0000001 1.11 cm
|
|
27
|
-
# 8 0.00000001 1.11 mm
|
|
28
|
-
_COORDINATES_PRECISION = 7
|
|
29
|
-
_ALTITUDE_PRECISION = 3
|
|
30
|
-
_ANGLE_PRECISION = 3
|
|
11
|
+
from . import geo, utils
|
|
31
12
|
|
|
32
13
|
|
|
33
14
|
class FileType(enum.Enum):
|
|
15
|
+
IMAGE = "image"
|
|
16
|
+
ZIP = "zip"
|
|
17
|
+
# VIDEO is a superset of all NATIVE_VIDEO_FILETYPES below.
|
|
18
|
+
# It also contains the videos that external geotag source (e.g. exiftool) supports
|
|
19
|
+
VIDEO = "video"
|
|
34
20
|
BLACKVUE = "blackvue"
|
|
35
21
|
CAMM = "camm"
|
|
36
22
|
GOPRO = "gopro"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
NATIVE_VIDEO_FILETYPES = {
|
|
26
|
+
FileType.BLACKVUE,
|
|
27
|
+
FileType.CAMM,
|
|
28
|
+
FileType.GOPRO,
|
|
29
|
+
}
|
|
40
30
|
|
|
41
31
|
|
|
42
32
|
@dataclasses.dataclass
|
|
43
33
|
class ImageMetadata(geo.Point):
|
|
44
34
|
filename: Path
|
|
45
|
-
#
|
|
46
|
-
md5sum:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
MAPFilename:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def update_md5sum(self, image_data: T.Optional[T.BinaryIO] = None) -> None:
|
|
35
|
+
# filetype should be always FileType.IMAGE
|
|
36
|
+
md5sum: str | None = None
|
|
37
|
+
width: int | None = None
|
|
38
|
+
height: int | None = None
|
|
39
|
+
filesize: int | None = None
|
|
40
|
+
|
|
41
|
+
# Fields starting with MAP* will be written to the image EXIF
|
|
42
|
+
MAPSequenceUUID: str | None = None
|
|
43
|
+
MAPDeviceMake: str | None = None
|
|
44
|
+
MAPDeviceModel: str | None = None
|
|
45
|
+
MAPGPSAccuracyMeters: float | None = None
|
|
46
|
+
MAPCameraUUID: str | None = None
|
|
47
|
+
MAPOrientation: int | None = None
|
|
48
|
+
MAPMetaTags: dict | None = None
|
|
49
|
+
MAPFilename: str | None = None
|
|
50
|
+
|
|
51
|
+
def update_md5sum(self, image_data: T.BinaryIO | None = None) -> None:
|
|
63
52
|
if self.md5sum is None:
|
|
64
53
|
if image_data is None:
|
|
65
54
|
with self.filename.open("rb") as fp:
|
|
@@ -77,13 +66,12 @@ class ImageMetadata(geo.Point):
|
|
|
77
66
|
@dataclasses.dataclass
|
|
78
67
|
class VideoMetadata:
|
|
79
68
|
filename: Path
|
|
80
|
-
# if None or absent, it will be calculated
|
|
81
|
-
md5sum: T.Optional[str]
|
|
82
69
|
filetype: FileType
|
|
83
70
|
points: T.Sequence[geo.Point]
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
71
|
+
md5sum: str | None = None
|
|
72
|
+
make: str | None = None
|
|
73
|
+
model: str | None = None
|
|
74
|
+
filesize: int | None = None
|
|
87
75
|
|
|
88
76
|
def update_md5sum(self) -> None:
|
|
89
77
|
if self.md5sum is None:
|
|
@@ -94,7 +82,7 @@ class VideoMetadata:
|
|
|
94
82
|
@dataclasses.dataclass
|
|
95
83
|
class ErrorMetadata:
|
|
96
84
|
filename: Path
|
|
97
|
-
filetype:
|
|
85
|
+
filetype: FileType
|
|
98
86
|
error: Exception
|
|
99
87
|
|
|
100
88
|
|
|
@@ -104,592 +92,94 @@ Metadata = T.Union[ImageMetadata, VideoMetadata]
|
|
|
104
92
|
MetadataOrError = T.Union[Metadata, ErrorMetadata]
|
|
105
93
|
|
|
106
94
|
|
|
107
|
-
class
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
user_upload_token: str
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class _CompassHeading(TypedDict, total=True):
|
|
116
|
-
TrueHeading: float
|
|
117
|
-
MagneticHeading: float
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class _ImageRequired(TypedDict, total=True):
|
|
121
|
-
MAPLatitude: float
|
|
122
|
-
MAPLongitude: float
|
|
123
|
-
MAPCaptureTime: str
|
|
95
|
+
class BaseSerializer(abc.ABC):
|
|
96
|
+
@classmethod
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def serialize(cls, metadatas: T.Sequence[MetadataOrError]) -> bytes:
|
|
99
|
+
raise NotImplementedError()
|
|
124
100
|
|
|
101
|
+
@classmethod
|
|
102
|
+
@abc.abstractmethod
|
|
103
|
+
def deserialize(cls, data: bytes) -> list[Metadata]:
|
|
104
|
+
raise NotImplementedError()
|
|
125
105
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
106
|
+
@classmethod
|
|
107
|
+
def deserialize_stream(cls, data: T.IO[bytes]) -> list[Metadata]:
|
|
108
|
+
return cls.deserialize(data.read())
|
|
129
109
|
|
|
130
110
|
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
def combine_filetype_filters(
|
|
112
|
+
a: set[FileType] | None, b: set[FileType] | None
|
|
113
|
+
) -> set[FileType] | None:
|
|
114
|
+
"""
|
|
115
|
+
>>> combine_filetype_filters({FileType.CAMM}, {FileType.GOPRO})
|
|
116
|
+
set()
|
|
133
117
|
|
|
118
|
+
>>> combine_filetype_filters({FileType.CAMM}, {FileType.GOPRO, FileType.VIDEO})
|
|
119
|
+
{<FileType.CAMM: 'camm'>}
|
|
134
120
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
MAPDeviceModel: str
|
|
138
|
-
MAPGPSAccuracyMeters: float
|
|
139
|
-
MAPCameraUUID: str
|
|
140
|
-
MAPOrientation: int
|
|
121
|
+
>>> combine_filetype_filters({FileType.GOPRO}, {FileType.GOPRO, FileType.VIDEO})
|
|
122
|
+
{<FileType.GOPRO: 'gopro'>}
|
|
141
123
|
|
|
124
|
+
>>> combine_filetype_filters({FileType.GOPRO}, {FileType.VIDEO})
|
|
125
|
+
{<FileType.GOPRO: 'gopro'>}
|
|
142
126
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# if None or absent, it will be calculated
|
|
147
|
-
md5sum: T.Optional[str]
|
|
148
|
-
filetype: Literal["image"]
|
|
149
|
-
filesize: T.Optional[int]
|
|
127
|
+
>>> expected = {FileType.CAMM, FileType.GOPRO}
|
|
128
|
+
>>> combine_filetype_filters({FileType.CAMM, FileType.GOPRO}, {FileType.VIDEO}) == expected
|
|
129
|
+
True
|
|
150
130
|
|
|
131
|
+
>>> expected = {FileType.CAMM, FileType.GOPRO, FileType.BLACKVUE, FileType.VIDEO}
|
|
132
|
+
>>> combine_filetype_filters({FileType.VIDEO}, {FileType.VIDEO}) == expected
|
|
133
|
+
True
|
|
134
|
+
"""
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# if None or absent, it will be calculated
|
|
155
|
-
md5sum: T.Optional[str]
|
|
156
|
-
filetype: str
|
|
157
|
-
MAPGPSTrack: T.List[T.Sequence[T.Union[float, int, None]]]
|
|
136
|
+
if a is None:
|
|
137
|
+
return b
|
|
158
138
|
|
|
139
|
+
if b is None:
|
|
140
|
+
return a
|
|
159
141
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
MAPDeviceModel: str
|
|
163
|
-
filesize: T.Optional[int]
|
|
142
|
+
# VIDEO is a superset of NATIVE_VIDEO_FILETYPES,
|
|
143
|
+
# so we add NATIVE_VIDEO_FILETYPES to each set for intersection later
|
|
164
144
|
|
|
145
|
+
if FileType.VIDEO in a:
|
|
146
|
+
a = a | NATIVE_VIDEO_FILETYPES
|
|
165
147
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
type: str
|
|
169
|
-
message: str
|
|
170
|
-
# vars is optional
|
|
171
|
-
vars: T.Dict
|
|
148
|
+
if FileType.VIDEO in b:
|
|
149
|
+
b = b | NATIVE_VIDEO_FILETYPES
|
|
172
150
|
|
|
151
|
+
return a.intersection(b)
|
|
173
152
|
|
|
174
|
-
class _ImageDescriptionErrorRequired(TypedDict, total=True):
|
|
175
|
-
filename: str
|
|
176
|
-
error: _ErrorDescription
|
|
177
153
|
|
|
154
|
+
M = T.TypeVar("M")
|
|
178
155
|
|
|
179
|
-
class ImageDescriptionError(_ImageDescriptionErrorRequired, total=False):
|
|
180
|
-
filetype: str
|
|
181
156
|
|
|
157
|
+
def separate_errors(
|
|
158
|
+
metadatas: T.Iterable[M | ErrorMetadata],
|
|
159
|
+
) -> tuple[list[M], list[ErrorMetadata]]:
|
|
160
|
+
good: list[M] = []
|
|
161
|
+
bad: list[ErrorMetadata] = []
|
|
182
162
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"message": str(exc),
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
exc_vars = vars(exc)
|
|
192
|
-
|
|
193
|
-
if exc_vars:
|
|
194
|
-
# handle unserializable exceptions
|
|
195
|
-
try:
|
|
196
|
-
vars_json = json.dumps(exc_vars)
|
|
197
|
-
except Exception:
|
|
198
|
-
vars_json = ""
|
|
199
|
-
if vars_json:
|
|
200
|
-
err["vars"] = json.loads(vars_json)
|
|
201
|
-
|
|
202
|
-
desc: ImageDescriptionError = {
|
|
203
|
-
"error": err,
|
|
204
|
-
"filename": str(filename.resolve()),
|
|
205
|
-
}
|
|
206
|
-
if filetype is not None:
|
|
207
|
-
desc["filetype"] = filetype.value
|
|
163
|
+
for metadata in metadatas:
|
|
164
|
+
if isinstance(metadata, ErrorMetadata):
|
|
165
|
+
bad.append(metadata)
|
|
166
|
+
else:
|
|
167
|
+
good.append(metadata)
|
|
208
168
|
|
|
209
|
-
return
|
|
169
|
+
return good, bad
|
|
210
170
|
|
|
211
171
|
|
|
212
172
|
def describe_error_metadata(
|
|
213
|
-
exc: Exception, filename: Path, filetype:
|
|
173
|
+
exc: Exception, filename: Path, filetype: FileType
|
|
214
174
|
) -> ErrorMetadata:
|
|
215
175
|
return ErrorMetadata(filename=filename, filetype=filetype, error=exc)
|
|
216
176
|
|
|
217
177
|
|
|
218
|
-
Description = T.Union[ImageDescription, VideoDescription]
|
|
219
|
-
DescriptionOrError = T.Union[ImageDescription, VideoDescription, ImageDescriptionError]
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
UserItemSchema = {
|
|
223
|
-
"type": "object",
|
|
224
|
-
"properties": {
|
|
225
|
-
"MAPOrganizationKey": {"type": ["integer", "string"]},
|
|
226
|
-
# Not in use. Keep here for back-compatibility
|
|
227
|
-
"MAPSettingsUsername": {"type": "string"},
|
|
228
|
-
"MAPSettingsUserKey": {"type": "string"},
|
|
229
|
-
"user_upload_token": {"type": "string"},
|
|
230
|
-
},
|
|
231
|
-
"required": ["user_upload_token"],
|
|
232
|
-
"additionalProperties": True,
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
ImageDescriptionEXIFSchema = {
|
|
237
|
-
"type": "object",
|
|
238
|
-
"properties": {
|
|
239
|
-
"MAPLatitude": {
|
|
240
|
-
"type": "number",
|
|
241
|
-
"description": "Latitude of the image",
|
|
242
|
-
"minimum": -90,
|
|
243
|
-
"maximum": 90,
|
|
244
|
-
},
|
|
245
|
-
"MAPLongitude": {
|
|
246
|
-
"type": "number",
|
|
247
|
-
"description": "Longitude of the image",
|
|
248
|
-
"minimum": -180,
|
|
249
|
-
"maximum": 180,
|
|
250
|
-
},
|
|
251
|
-
"MAPAltitude": {
|
|
252
|
-
"type": "number",
|
|
253
|
-
"description": "Altitude of the image, in meters",
|
|
254
|
-
},
|
|
255
|
-
"MAPCaptureTime": {
|
|
256
|
-
"type": "string",
|
|
257
|
-
"description": "Capture time of the image",
|
|
258
|
-
"pattern": "[0-9]{4}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]{2}_[0-9]+",
|
|
259
|
-
},
|
|
260
|
-
"MAPCompassHeading": {
|
|
261
|
-
"type": "object",
|
|
262
|
-
"properties": {
|
|
263
|
-
"TrueHeading": {"type": "number"},
|
|
264
|
-
"MagneticHeading": {"type": "number"},
|
|
265
|
-
},
|
|
266
|
-
"required": ["TrueHeading", "MagneticHeading"],
|
|
267
|
-
"additionalProperties": False,
|
|
268
|
-
"description": "Camera angle of the image, in degrees. If null, the angle will be interpolated",
|
|
269
|
-
},
|
|
270
|
-
"MAPSequenceUUID": {
|
|
271
|
-
"type": "string",
|
|
272
|
-
"description": "Arbitrary key for grouping images",
|
|
273
|
-
"pattern": "[a-zA-Z0-9_-]+",
|
|
274
|
-
},
|
|
275
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
276
|
-
"MAPMetaTags": {"type": "object"},
|
|
277
|
-
"MAPDeviceMake": {"type": "string"},
|
|
278
|
-
"MAPDeviceModel": {"type": "string"},
|
|
279
|
-
"MAPGPSAccuracyMeters": {"type": "number"},
|
|
280
|
-
"MAPCameraUUID": {"type": "string"},
|
|
281
|
-
# deprecated since v0.10.0; keep here for compatibility
|
|
282
|
-
"MAPFilename": {
|
|
283
|
-
"type": "string",
|
|
284
|
-
"description": "The base filename of the image",
|
|
285
|
-
},
|
|
286
|
-
"MAPOrientation": {"type": "integer"},
|
|
287
|
-
},
|
|
288
|
-
"required": [
|
|
289
|
-
"MAPLatitude",
|
|
290
|
-
"MAPLongitude",
|
|
291
|
-
"MAPCaptureTime",
|
|
292
|
-
],
|
|
293
|
-
"additionalProperties": False,
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
VideoDescriptionSchema = {
|
|
297
|
-
"type": "object",
|
|
298
|
-
"properties": {
|
|
299
|
-
"MAPGPSTrack": {
|
|
300
|
-
"type": "array",
|
|
301
|
-
"items": {
|
|
302
|
-
"type": "array",
|
|
303
|
-
"description": "track point",
|
|
304
|
-
"prefixItems": [
|
|
305
|
-
{
|
|
306
|
-
"type": "number",
|
|
307
|
-
"description": "Time offset of the track point, in milliseconds, relative to the beginning of the video",
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
"type": "number",
|
|
311
|
-
"description": "Longitude of the track point",
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
"type": "number",
|
|
315
|
-
"description": "Latitude of the track point",
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
"type": ["number", "null"],
|
|
319
|
-
"description": "Altitude of the track point in meters",
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
"type": ["number", "null"],
|
|
323
|
-
"description": "Camera angle of the track point, in degrees. If null, the angle will be interpolated",
|
|
324
|
-
},
|
|
325
|
-
],
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
"MAPDeviceMake": {
|
|
329
|
-
"type": "string",
|
|
330
|
-
"description": "Device make, e.g. GoPro, BlackVue, Insta360",
|
|
331
|
-
},
|
|
332
|
-
"MAPDeviceModel": {
|
|
333
|
-
"type": "string",
|
|
334
|
-
"description": "Device model, e.g. HERO10 Black, DR900S-1CH, Insta360 Titan",
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
"required": [
|
|
338
|
-
"MAPGPSTrack",
|
|
339
|
-
],
|
|
340
|
-
"additionalProperties": False,
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def merge_schema(*schemas: T.Dict) -> T.Dict:
|
|
345
|
-
for s in schemas:
|
|
346
|
-
assert s.get("type") == "object", "must be all object schemas"
|
|
347
|
-
properties = {}
|
|
348
|
-
all_required = []
|
|
349
|
-
additional_properties = True
|
|
350
|
-
for s in schemas:
|
|
351
|
-
properties.update(s.get("properties", {}))
|
|
352
|
-
all_required += s.get("required", [])
|
|
353
|
-
if "additionalProperties" in s:
|
|
354
|
-
additional_properties = s["additionalProperties"]
|
|
355
|
-
return {
|
|
356
|
-
"type": "object",
|
|
357
|
-
"properties": properties,
|
|
358
|
-
"required": sorted(set(all_required)),
|
|
359
|
-
"additionalProperties": additional_properties,
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
ImageDescriptionFileSchema = merge_schema(
|
|
364
|
-
ImageDescriptionEXIFSchema,
|
|
365
|
-
{
|
|
366
|
-
"type": "object",
|
|
367
|
-
"properties": {
|
|
368
|
-
"filename": {
|
|
369
|
-
"type": "string",
|
|
370
|
-
"description": "Absolute path of the image",
|
|
371
|
-
},
|
|
372
|
-
"md5sum": {
|
|
373
|
-
"type": ["string", "null"],
|
|
374
|
-
"description": "MD5 checksum of the image content. If not provided, the uploader will compute it",
|
|
375
|
-
},
|
|
376
|
-
"filesize": {
|
|
377
|
-
"type": ["number", "null"],
|
|
378
|
-
"description": "File size",
|
|
379
|
-
},
|
|
380
|
-
"filetype": {
|
|
381
|
-
"type": "string",
|
|
382
|
-
"enum": [FileType.IMAGE.value],
|
|
383
|
-
"description": "The image file type",
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
"required": [
|
|
387
|
-
"filename",
|
|
388
|
-
"filetype",
|
|
389
|
-
],
|
|
390
|
-
},
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
VideoDescriptionFileSchema = merge_schema(
|
|
394
|
-
VideoDescriptionSchema,
|
|
395
|
-
{
|
|
396
|
-
"type": "object",
|
|
397
|
-
"properties": {
|
|
398
|
-
"filename": {
|
|
399
|
-
"type": "string",
|
|
400
|
-
"description": "Absolute path of the video",
|
|
401
|
-
},
|
|
402
|
-
"md5sum": {
|
|
403
|
-
"type": ["string", "null"],
|
|
404
|
-
"description": "MD5 checksum of the video content. If not provided, the uploader will compute it",
|
|
405
|
-
},
|
|
406
|
-
"filesize": {
|
|
407
|
-
"type": ["number", "null"],
|
|
408
|
-
"description": "File size",
|
|
409
|
-
},
|
|
410
|
-
"filetype": {
|
|
411
|
-
"type": "string",
|
|
412
|
-
"enum": [
|
|
413
|
-
FileType.CAMM.value,
|
|
414
|
-
FileType.GOPRO.value,
|
|
415
|
-
FileType.BLACKVUE.value,
|
|
416
|
-
FileType.VIDEO.value,
|
|
417
|
-
],
|
|
418
|
-
"description": "The video file type",
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
"required": [
|
|
422
|
-
"filename",
|
|
423
|
-
"filetype",
|
|
424
|
-
],
|
|
425
|
-
},
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
ImageVideoDescriptionFileSchema = {
|
|
430
|
-
"oneOf": [VideoDescriptionFileSchema, ImageDescriptionFileSchema]
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def validate_image_desc(desc: T.Any) -> None:
|
|
435
|
-
try:
|
|
436
|
-
jsonschema.validate(instance=desc, schema=ImageDescriptionFileSchema)
|
|
437
|
-
except jsonschema.ValidationError as ex:
|
|
438
|
-
# do not use str(ex) which is more verbose
|
|
439
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
440
|
-
try:
|
|
441
|
-
map_capture_time_to_datetime(desc["MAPCaptureTime"])
|
|
442
|
-
except ValueError as ex:
|
|
443
|
-
raise exceptions.MapillaryMetadataValidationError(str(ex))
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def validate_video_desc(desc: T.Any) -> None:
|
|
447
|
-
try:
|
|
448
|
-
jsonschema.validate(instance=desc, schema=VideoDescriptionFileSchema)
|
|
449
|
-
except jsonschema.ValidationError as ex:
|
|
450
|
-
# do not use str(ex) which is more verbose
|
|
451
|
-
raise exceptions.MapillaryMetadataValidationError(ex.message)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def datetime_to_map_capture_time(time: T.Union[datetime.datetime, int, float]) -> str:
|
|
455
|
-
if isinstance(time, (float, int)):
|
|
456
|
-
dt = datetime.datetime.utcfromtimestamp(time)
|
|
457
|
-
# otherwise it will be assumed to be in local time
|
|
458
|
-
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
459
|
-
else:
|
|
460
|
-
# otherwise it will be assumed to be in local time
|
|
461
|
-
dt = time.astimezone(datetime.timezone.utc)
|
|
462
|
-
return datetime.datetime.strftime(dt, "%Y_%m_%d_%H_%M_%S_%f")[:-3]
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def map_capture_time_to_datetime(time: str) -> datetime.datetime:
|
|
466
|
-
dt = datetime.datetime.strptime(time, "%Y_%m_%d_%H_%M_%S_%f")
|
|
467
|
-
dt = dt.replace(tzinfo=datetime.timezone.utc)
|
|
468
|
-
return dt
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
@T.overload
|
|
472
|
-
def as_desc(metadata: ImageMetadata) -> ImageDescription: ...
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
@T.overload
|
|
476
|
-
def as_desc(metadata: ErrorMetadata) -> ImageDescriptionError: ...
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
@T.overload
|
|
480
|
-
def as_desc(metadata: VideoMetadata) -> VideoDescription: ...
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
def as_desc(metadata):
|
|
484
|
-
if isinstance(metadata, ErrorMetadata):
|
|
485
|
-
return _describe_error_desc(
|
|
486
|
-
metadata.error, metadata.filename, metadata.filetype
|
|
487
|
-
)
|
|
488
|
-
elif isinstance(metadata, VideoMetadata):
|
|
489
|
-
return _as_video_desc(metadata)
|
|
490
|
-
else:
|
|
491
|
-
assert isinstance(metadata, ImageMetadata)
|
|
492
|
-
return _as_image_desc(metadata)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def _as_video_desc(metadata: VideoMetadata) -> VideoDescription:
|
|
496
|
-
desc: VideoDescription = {
|
|
497
|
-
"filename": str(metadata.filename.resolve()),
|
|
498
|
-
"md5sum": metadata.md5sum,
|
|
499
|
-
"filetype": metadata.filetype.value,
|
|
500
|
-
"filesize": metadata.filesize,
|
|
501
|
-
"MAPGPSTrack": [_encode_point(p) for p in metadata.points],
|
|
502
|
-
}
|
|
503
|
-
if metadata.make:
|
|
504
|
-
desc["MAPDeviceMake"] = metadata.make
|
|
505
|
-
if metadata.model:
|
|
506
|
-
desc["MAPDeviceModel"] = metadata.model
|
|
507
|
-
return desc
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
def _as_image_desc(metadata: ImageMetadata) -> ImageDescription:
|
|
511
|
-
desc: ImageDescription = {
|
|
512
|
-
"filename": str(metadata.filename.resolve()),
|
|
513
|
-
"md5sum": metadata.md5sum,
|
|
514
|
-
"filesize": metadata.filesize,
|
|
515
|
-
"filetype": FileType.IMAGE.value,
|
|
516
|
-
"MAPLatitude": round(metadata.lat, _COORDINATES_PRECISION),
|
|
517
|
-
"MAPLongitude": round(metadata.lon, _COORDINATES_PRECISION),
|
|
518
|
-
"MAPCaptureTime": datetime_to_map_capture_time(metadata.time),
|
|
519
|
-
}
|
|
520
|
-
if metadata.alt is not None:
|
|
521
|
-
desc["MAPAltitude"] = round(metadata.alt, _ALTITUDE_PRECISION)
|
|
522
|
-
if metadata.angle is not None:
|
|
523
|
-
desc["MAPCompassHeading"] = {
|
|
524
|
-
"TrueHeading": round(metadata.angle, _ANGLE_PRECISION),
|
|
525
|
-
"MagneticHeading": round(metadata.angle, _ANGLE_PRECISION),
|
|
526
|
-
}
|
|
527
|
-
fields = dataclasses.fields(metadata)
|
|
528
|
-
for field in fields:
|
|
529
|
-
if field.name.startswith("MAP"):
|
|
530
|
-
value = getattr(metadata, field.name)
|
|
531
|
-
if value is not None:
|
|
532
|
-
# ignore error: TypedDict key must be a string literal;
|
|
533
|
-
# expected one of ("MAPMetaTags", "MAPDeviceMake", "MAPDeviceModel", "MAPGPSAccuracyMeters", "MAPCameraUUID", ...)
|
|
534
|
-
desc[field.name] = value # type: ignore
|
|
535
|
-
return desc
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
@T.overload
|
|
539
|
-
def from_desc(metadata: ImageDescription) -> ImageMetadata: ...
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
@T.overload
|
|
543
|
-
def from_desc(metadata: VideoDescription) -> VideoMetadata: ...
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
def from_desc(desc):
|
|
547
|
-
assert "error" not in desc
|
|
548
|
-
if desc["filetype"] == FileType.IMAGE.value:
|
|
549
|
-
return _from_image_desc(desc)
|
|
550
|
-
else:
|
|
551
|
-
return _from_video_desc(desc)
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
def _from_image_desc(desc) -> ImageMetadata:
|
|
555
|
-
kwargs: T.Dict = {}
|
|
556
|
-
for k, v in desc.items():
|
|
557
|
-
if k not in [
|
|
558
|
-
"filename",
|
|
559
|
-
"md5sum",
|
|
560
|
-
"filesize",
|
|
561
|
-
"filetype",
|
|
562
|
-
"MAPLatitude",
|
|
563
|
-
"MAPLongitude",
|
|
564
|
-
"MAPAltitude",
|
|
565
|
-
"MAPCaptureTime",
|
|
566
|
-
"MAPCompassHeading",
|
|
567
|
-
]:
|
|
568
|
-
kwargs[k] = v
|
|
569
|
-
|
|
570
|
-
return ImageMetadata(
|
|
571
|
-
filename=Path(desc["filename"]),
|
|
572
|
-
md5sum=desc.get("md5sum"),
|
|
573
|
-
filesize=desc.get("filesize"),
|
|
574
|
-
lat=desc["MAPLatitude"],
|
|
575
|
-
lon=desc["MAPLongitude"],
|
|
576
|
-
alt=desc.get("MAPAltitude"),
|
|
577
|
-
time=geo.as_unix_time(map_capture_time_to_datetime(desc["MAPCaptureTime"])),
|
|
578
|
-
angle=desc.get("MAPCompassHeading", {}).get("TrueHeading"),
|
|
579
|
-
width=None,
|
|
580
|
-
height=None,
|
|
581
|
-
**kwargs,
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
def _encode_point(p: geo.Point) -> T.Sequence[T.Union[float, int, None]]:
|
|
586
|
-
entry = [
|
|
587
|
-
int(p.time * 1000),
|
|
588
|
-
round(p.lon, _COORDINATES_PRECISION),
|
|
589
|
-
round(p.lat, _COORDINATES_PRECISION),
|
|
590
|
-
round(p.alt, _ALTITUDE_PRECISION) if p.alt is not None else None,
|
|
591
|
-
round(p.angle, _ANGLE_PRECISION) if p.angle is not None else None,
|
|
592
|
-
]
|
|
593
|
-
return entry
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def _decode_point(entry: T.Sequence[T.Any]) -> geo.Point:
|
|
597
|
-
time_ms, lon, lat, alt, angle = entry
|
|
598
|
-
return geo.Point(time=time_ms / 1000, lon=lon, lat=lat, alt=alt, angle=angle)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
def _from_video_desc(desc: VideoDescription) -> VideoMetadata:
|
|
602
|
-
return VideoMetadata(
|
|
603
|
-
filename=Path(desc["filename"]),
|
|
604
|
-
md5sum=desc["md5sum"],
|
|
605
|
-
filesize=desc["filesize"],
|
|
606
|
-
filetype=FileType(desc["filetype"]),
|
|
607
|
-
points=[_decode_point(entry) for entry in desc["MAPGPSTrack"]],
|
|
608
|
-
make=desc.get("MAPDeviceMake"),
|
|
609
|
-
model=desc.get("MAPDeviceModel"),
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
def validate_and_fail_desc(desc: DescriptionOrError) -> DescriptionOrError:
|
|
614
|
-
if "error" in desc:
|
|
615
|
-
return desc
|
|
616
|
-
|
|
617
|
-
filetype = desc.get("filetype")
|
|
618
|
-
try:
|
|
619
|
-
if filetype == FileType.IMAGE.value:
|
|
620
|
-
validate_image_desc(desc)
|
|
621
|
-
else:
|
|
622
|
-
validate_video_desc(desc)
|
|
623
|
-
except exceptions.MapillaryMetadataValidationError as ex:
|
|
624
|
-
return _describe_error_desc(
|
|
625
|
-
ex,
|
|
626
|
-
Path(desc["filename"]),
|
|
627
|
-
filetype=FileType(filetype) if filetype else None,
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
if not os.path.isfile(desc["filename"]):
|
|
631
|
-
return _describe_error_desc(
|
|
632
|
-
exceptions.MapillaryMetadataValidationError(
|
|
633
|
-
f"No such file {desc['filename']}"
|
|
634
|
-
),
|
|
635
|
-
Path(desc["filename"]),
|
|
636
|
-
filetype=FileType(filetype) if filetype else None,
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
return desc
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
# Same as validate_and_fail_desc but for the metadata dataclass
|
|
643
|
-
def validate_and_fail_metadata(metadata: MetadataOrError) -> MetadataOrError:
|
|
644
|
-
if isinstance(metadata, ErrorMetadata):
|
|
645
|
-
return metadata
|
|
646
|
-
|
|
647
|
-
filetype: T.Optional[FileType] = None
|
|
648
|
-
try:
|
|
649
|
-
if isinstance(metadata, ImageMetadata):
|
|
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))
|
|
656
|
-
except exceptions.MapillaryMetadataValidationError as ex:
|
|
657
|
-
# rethrow because the original error is too verbose
|
|
658
|
-
return describe_error_metadata(
|
|
659
|
-
ex,
|
|
660
|
-
metadata.filename,
|
|
661
|
-
filetype=filetype,
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
if not metadata.filename.is_file():
|
|
665
|
-
return describe_error_metadata(
|
|
666
|
-
exceptions.MapillaryMetadataValidationError(
|
|
667
|
-
f"No such file {metadata.filename}"
|
|
668
|
-
),
|
|
669
|
-
metadata.filename,
|
|
670
|
-
filetype=filetype,
|
|
671
|
-
)
|
|
672
|
-
|
|
673
|
-
return metadata
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
def desc_file_to_exif(
|
|
677
|
-
desc: ImageDescription,
|
|
678
|
-
) -> ImageDescription:
|
|
679
|
-
not_needed = ["MAPSequenceUUID"]
|
|
680
|
-
removed = {
|
|
681
|
-
key: value
|
|
682
|
-
for key, value in desc.items()
|
|
683
|
-
if key.startswith("MAP") and key not in not_needed
|
|
684
|
-
}
|
|
685
|
-
return T.cast(ImageDescription, removed)
|
|
686
|
-
|
|
687
|
-
|
|
688
178
|
def group_and_sort_images(
|
|
689
|
-
metadatas: T.
|
|
690
|
-
) ->
|
|
179
|
+
metadatas: T.Iterable[ImageMetadata],
|
|
180
|
+
) -> dict[str, list[ImageMetadata]]:
|
|
691
181
|
# group metadatas by uuid
|
|
692
|
-
sequences_by_uuid:
|
|
182
|
+
sequences_by_uuid: dict[str, list[ImageMetadata]] = {}
|
|
693
183
|
missing_sequence_uuid = str(uuid.uuid4())
|
|
694
184
|
for metadata in metadatas:
|
|
695
185
|
if metadata.MAPSequenceUUID is None:
|
|
@@ -709,13 +199,10 @@ def group_and_sort_images(
|
|
|
709
199
|
return sorted_sequences_by_uuid
|
|
710
200
|
|
|
711
201
|
|
|
712
|
-
def
|
|
202
|
+
def update_sequence_md5sum(sequence: T.Iterable[ImageMetadata]) -> str:
|
|
713
203
|
md5 = hashlib.md5()
|
|
714
204
|
for metadata in sequence:
|
|
205
|
+
metadata.update_md5sum()
|
|
715
206
|
assert isinstance(metadata.md5sum, str), "md5sum should be calculated"
|
|
716
207
|
md5.update(metadata.md5sum.encode("utf-8"))
|
|
717
208
|
return md5.hexdigest()
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
if __name__ == "__main__":
|
|
721
|
-
print(json.dumps(ImageVideoDescriptionFileSchema, indent=4))
|