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/__init__.py
CHANGED
mapillary_tools/api_v4.py
CHANGED
mapillary_tools/authenticate.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 getpass
|
|
@@ -7,7 +12,6 @@ import sys
|
|
|
7
12
|
import typing as T
|
|
8
13
|
|
|
9
14
|
import jsonschema
|
|
10
|
-
|
|
11
15
|
import requests
|
|
12
16
|
|
|
13
17
|
from . import api_v4, config, constants, exceptions, http
|
|
@@ -1,7 +1,12 @@
|
|
|
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
|
|
4
|
-
|
|
9
|
+
import datetime
|
|
5
10
|
import json
|
|
6
11
|
import logging
|
|
7
12
|
import re
|
|
@@ -9,7 +14,7 @@ import typing as T
|
|
|
9
14
|
|
|
10
15
|
import pynmea2
|
|
11
16
|
|
|
12
|
-
from . import
|
|
17
|
+
from . import telemetry
|
|
13
18
|
from .mp4 import simple_mp4_parser as sparser
|
|
14
19
|
|
|
15
20
|
|
|
@@ -32,7 +37,7 @@ NMEA_LINE_REGEX = re.compile(
|
|
|
32
37
|
class BlackVueInfo:
|
|
33
38
|
# None and [] are equivalent here. Use None as default because:
|
|
34
39
|
# ValueError: mutable default <class 'list'> for field gps is not allowed: use default_factory
|
|
35
|
-
gps: list[
|
|
40
|
+
gps: list[telemetry.GPSPoint] | None = None
|
|
36
41
|
make: str = "BlackVue"
|
|
37
42
|
model: str = ""
|
|
38
43
|
|
|
@@ -50,9 +55,11 @@ def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None:
|
|
|
50
55
|
points.sort(key=lambda p: p.time)
|
|
51
56
|
|
|
52
57
|
if points:
|
|
58
|
+
# Convert the time field to relative time to the first point
|
|
59
|
+
# epoch_time stays as the original time in seconds
|
|
53
60
|
first_point_time = points[0].time
|
|
54
61
|
for p in points:
|
|
55
|
-
p.time =
|
|
62
|
+
p.time = p.time - first_point_time
|
|
56
63
|
|
|
57
64
|
# Camera model
|
|
58
65
|
try:
|
|
@@ -114,25 +121,64 @@ def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
|
|
|
114
121
|
return ""
|
|
115
122
|
|
|
116
123
|
|
|
117
|
-
def
|
|
124
|
+
def _compute_timezone_offset_from_rmc(
|
|
125
|
+
epoch_sec: float, message: pynmea2.NMEASentence
|
|
126
|
+
) -> float | None:
|
|
118
127
|
"""
|
|
119
|
-
|
|
120
|
-
[Point(time=1623057074211, lat=51.150436666666664, lon=-114.03067833333333, alt=1097.36, angle=None)]
|
|
128
|
+
Compute timezone offset from an RMC message which has full date+time.
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
Returns the offset to add to camera epoch to get correct UTC time,
|
|
131
|
+
or None if this message doesn't have the required datetime.
|
|
132
|
+
"""
|
|
133
|
+
if (
|
|
134
|
+
message.sentence_type != "RMC"
|
|
135
|
+
or not hasattr(message, "datetime")
|
|
136
|
+
or not message.datetime
|
|
137
|
+
):
|
|
138
|
+
return None
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
correct_epoch = message.datetime.replace(tzinfo=datetime.timezone.utc).timestamp()
|
|
141
|
+
# Rounding needed to avoid floating point precision issues
|
|
142
|
+
return round(correct_epoch - epoch_sec, 3)
|
|
127
143
|
|
|
128
|
-
>>> list(_parse_gps_box(b"[1629874404069]$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B"))
|
|
129
|
-
[Point(time=1629874404069, lat=44.06899883333333, lon=-121.31433716666666, alt=None, angle=None)]
|
|
130
144
|
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
def _compute_timezone_offset_from_time_only(
|
|
146
|
+
epoch_sec: float, message: pynmea2.NMEASentence
|
|
147
|
+
) -> float | None:
|
|
148
|
+
"""
|
|
149
|
+
Compute timezone offset from GGA/GLL which only have time (no date).
|
|
150
|
+
|
|
151
|
+
Uses the date from camera epoch and replaces the time with NMEA time assuming camera date is correct.
|
|
152
|
+
Handles day boundary when camera and GPS times differ by more than 12 hours.
|
|
133
153
|
"""
|
|
134
|
-
|
|
154
|
+
if not hasattr(message, "timestamp") or not message.timestamp:
|
|
155
|
+
return None
|
|
135
156
|
|
|
157
|
+
camera_dt = datetime.datetime.fromtimestamp(epoch_sec, tz=datetime.timezone.utc)
|
|
158
|
+
|
|
159
|
+
nmea_time = message.timestamp
|
|
160
|
+
corrected_dt = camera_dt.replace(
|
|
161
|
+
hour=nmea_time.hour,
|
|
162
|
+
minute=nmea_time.minute,
|
|
163
|
+
second=nmea_time.second,
|
|
164
|
+
microsecond=getattr(nmea_time, "microsecond", 0),
|
|
165
|
+
)
|
|
166
|
+
# Handle day boundary e.g. camera time is 23:00, GPS time is 01:00 or vice versa
|
|
167
|
+
camera_secs = camera_dt.hour * 3600 + camera_dt.minute * 60 + camera_dt.second
|
|
168
|
+
nmea_secs = nmea_time.hour * 3600 + nmea_time.minute * 60 + nmea_time.second
|
|
169
|
+
if camera_secs - nmea_secs > 12 * 3600:
|
|
170
|
+
corrected_dt += datetime.timedelta(days=1)
|
|
171
|
+
elif nmea_secs - camera_secs > 12 * 3600:
|
|
172
|
+
corrected_dt -= datetime.timedelta(days=1)
|
|
173
|
+
|
|
174
|
+
# Rounding needed to avoid floating point precision issues
|
|
175
|
+
return round(corrected_dt.timestamp() - epoch_sec, 3)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_nmea_lines(
|
|
179
|
+
gps_data: bytes,
|
|
180
|
+
) -> T.Iterator[tuple[int, pynmea2.NMEASentence]]:
|
|
181
|
+
"""Parse NMEA lines from GPS data, yielding (epoch_ms, message) tuples."""
|
|
136
182
|
for line_bytes in gps_data.splitlines():
|
|
137
183
|
match = NMEA_LINE_REGEX.match(line_bytes)
|
|
138
184
|
if match is None:
|
|
@@ -156,29 +202,101 @@ def _parse_gps_box(gps_data: bytes) -> list[geo.Point]:
|
|
|
156
202
|
continue
|
|
157
203
|
|
|
158
204
|
epoch_ms = int(match.group(1))
|
|
205
|
+
yield epoch_ms, message
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _parse_gps_box(gps_data: bytes) -> list[telemetry.GPSPoint]:
|
|
209
|
+
"""
|
|
210
|
+
>>> list(_parse_gps_box(b"[1623057074211]$GPGGA,202530.00,5109.0262,N,11401.8407,W,5,40,0.5,1097.36,M,-17.00,M,18,TSTR*61"))
|
|
211
|
+
[GPSPoint(time=1623097530.0, lat=51.150436666666664, lon=-114.03067833333333, alt=1097.36, angle=None, epoch_time=1623097530.0, fix=<GPSFix.FIX_3D: 3>, precision=None, ground_speed=None)]
|
|
212
|
+
|
|
213
|
+
>>> list(_parse_gps_box(b"[1629874404069]$GNGGA,175322.00,3244.53126,N,11710.97811,W,1,12,0.84,17.4,M,-34.0,M,,*45"))
|
|
214
|
+
[GPSPoint(time=1629914002.0, lat=32.742187666666666, lon=-117.1829685, alt=17.4, angle=None, epoch_time=1629914002.0, fix=<GPSFix.FIX_3D: 3>, precision=None, ground_speed=None)]
|
|
215
|
+
|
|
216
|
+
>>> list(_parse_gps_box(b"[1629874404069]$GNGLL,4404.14012,N,12118.85993,W,001037.00,A,A*67"))
|
|
217
|
+
[GPSPoint(time=1629850237.0, lat=44.069002, lon=-121.31433216666667, alt=None, angle=None, epoch_time=1629850237.0, fix=None, precision=None, ground_speed=None)]
|
|
218
|
+
|
|
219
|
+
>>> list(_parse_gps_box(b"[1629874404069]$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B"))
|
|
220
|
+
[GPSPoint(time=1484007031.0, lat=44.06899883333333, lon=-121.31433716666666, alt=None, angle=None, epoch_time=1484007031.0, fix=None, precision=None, ground_speed=None)]
|
|
221
|
+
|
|
222
|
+
>>> list(_parse_gps_box(b"[1623057074211]$GPVTG,,T,,M,0.078,N,0.144,K,D*28[1623057075215]"))
|
|
223
|
+
[]
|
|
224
|
+
"""
|
|
225
|
+
timezone_offset: float | None = None
|
|
226
|
+
parsed_lines: list[tuple[float, pynmea2.NMEASentence]] = []
|
|
227
|
+
first_valid_gga_gll: tuple[float, pynmea2.NMEASentence] | None = None
|
|
228
|
+
|
|
229
|
+
# First pass: collect parsed_lines and compute timezone offset from the first valid RMC message
|
|
230
|
+
for epoch_ms, message in _parse_nmea_lines(gps_data):
|
|
231
|
+
# Rounding needed to avoid floating point precision issues
|
|
232
|
+
epoch_sec = round(epoch_ms / 1000, 3)
|
|
233
|
+
parsed_lines.append((epoch_sec, message))
|
|
234
|
+
if timezone_offset is None and message.sentence_type == "RMC":
|
|
235
|
+
if hasattr(message, "is_valid") and message.is_valid:
|
|
236
|
+
timezone_offset = _compute_timezone_offset_from_rmc(epoch_sec, message)
|
|
237
|
+
if timezone_offset is not None:
|
|
238
|
+
LOG.debug(
|
|
239
|
+
"Computed timezone offset %.1fs from RMC (%s %s)",
|
|
240
|
+
timezone_offset,
|
|
241
|
+
message.datestamp,
|
|
242
|
+
message.timestamp,
|
|
243
|
+
)
|
|
244
|
+
# Track first valid GGA/GLL for fallback
|
|
245
|
+
if first_valid_gga_gll is None and message.sentence_type in ["GGA", "GLL"]:
|
|
246
|
+
if hasattr(message, "is_valid") and message.is_valid:
|
|
247
|
+
first_valid_gga_gll = (epoch_sec, message)
|
|
248
|
+
|
|
249
|
+
# Fallback: if no RMC found, try GGA/GLL (less reliable - no date info)
|
|
250
|
+
if timezone_offset is None and first_valid_gga_gll is not None:
|
|
251
|
+
epoch_sec, message = first_valid_gga_gll
|
|
252
|
+
timezone_offset = _compute_timezone_offset_from_time_only(epoch_sec, message)
|
|
253
|
+
if timezone_offset is not None:
|
|
254
|
+
LOG.debug(
|
|
255
|
+
"Computed timezone offset %.1fs from %s (fallback, no date info)",
|
|
256
|
+
timezone_offset,
|
|
257
|
+
message.sentence_type,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# If no offset could be determined, use 0 (camera clock assumed correct)
|
|
261
|
+
if timezone_offset is None:
|
|
262
|
+
timezone_offset = 0.0
|
|
263
|
+
|
|
264
|
+
points_by_sentence_type: dict[str, list[telemetry.GPSPoint]] = {}
|
|
265
|
+
|
|
266
|
+
# Second pass: apply offset to all GPS points
|
|
267
|
+
for epoch_sec, message in parsed_lines:
|
|
268
|
+
corrected_epoch = round(epoch_sec + timezone_offset, 3)
|
|
159
269
|
|
|
160
270
|
# https://tavotech.com/gps-nmea-sentence-structure/
|
|
161
271
|
if message.sentence_type in ["GGA"]:
|
|
162
272
|
if not message.is_valid:
|
|
163
273
|
continue
|
|
164
|
-
point =
|
|
165
|
-
time=
|
|
274
|
+
point = telemetry.GPSPoint(
|
|
275
|
+
time=corrected_epoch,
|
|
166
276
|
lat=message.latitude,
|
|
167
277
|
lon=message.longitude,
|
|
168
278
|
alt=message.altitude,
|
|
169
279
|
angle=None,
|
|
280
|
+
epoch_time=corrected_epoch,
|
|
281
|
+
fix=telemetry.GPSFix.FIX_3D if message.gps_qual >= 1 else None,
|
|
282
|
+
precision=None,
|
|
283
|
+
ground_speed=None,
|
|
170
284
|
)
|
|
171
285
|
points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
|
|
172
286
|
|
|
173
287
|
elif message.sentence_type in ["RMC", "GLL"]:
|
|
174
288
|
if not message.is_valid:
|
|
175
289
|
continue
|
|
176
|
-
point =
|
|
177
|
-
time=
|
|
290
|
+
point = telemetry.GPSPoint(
|
|
291
|
+
time=corrected_epoch,
|
|
178
292
|
lat=message.latitude,
|
|
179
293
|
lon=message.longitude,
|
|
180
294
|
alt=None,
|
|
181
295
|
angle=None,
|
|
296
|
+
epoch_time=corrected_epoch,
|
|
297
|
+
fix=None,
|
|
298
|
+
precision=None,
|
|
299
|
+
ground_speed=None,
|
|
182
300
|
)
|
|
183
301
|
points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
|
|
184
302
|
|
|
@@ -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 io
|
|
@@ -9,7 +14,6 @@ from ..mp4 import (
|
|
|
9
14
|
mp4_sample_parser as sample_parser,
|
|
10
15
|
simple_mp4_builder as builder,
|
|
11
16
|
)
|
|
12
|
-
|
|
13
17
|
from . import camm_parser
|
|
14
18
|
|
|
15
19
|
|
|
@@ -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, 11, 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 argparse
|
|
@@ -203,6 +208,13 @@ class Command:
|
|
|
203
208
|
default=constants.DUPLICATE_ANGLE,
|
|
204
209
|
required=False,
|
|
205
210
|
)
|
|
211
|
+
group_sequence.add_argument(
|
|
212
|
+
"--skip_zigzag_check",
|
|
213
|
+
help="Skip the GPS zig-zag pattern detection check.",
|
|
214
|
+
action="store_true",
|
|
215
|
+
default=False,
|
|
216
|
+
required=False,
|
|
217
|
+
)
|
|
206
218
|
|
|
207
219
|
def run(self, vars_args: dict):
|
|
208
220
|
metadatas = process_geotag_properties(
|
|
@@ -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
|
import inspect
|
|
2
7
|
|
|
3
8
|
from ..authenticate import fetch_user_items
|
|
4
|
-
|
|
5
9
|
from .process import Command as ProcessCommand
|
|
6
10
|
from .upload import Command as UploadCommand
|
|
7
11
|
|
|
@@ -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
|
import inspect
|
|
2
7
|
|
|
3
8
|
from ..authenticate import fetch_user_items
|
|
4
|
-
|
|
5
9
|
from .upload import Command as UploadCommand
|
|
6
10
|
from .video_process import Command as VideoProcessCommand
|
|
7
11
|
|
mapillary_tools/commands/zip.py
CHANGED
mapillary_tools/config.py
CHANGED
mapillary_tools/constants.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 functools
|
|
@@ -115,7 +120,7 @@ MAPILLARY__EXPERIMENTAL_ENABLE_IMU: bool = _yes_or_no(
|
|
|
115
120
|
###### SEQUENCE PROCESSING ######
|
|
116
121
|
#################################
|
|
117
122
|
# In meters
|
|
118
|
-
CUTOFF_DISTANCE = float(os.getenv(_ENV_PREFIX + "CUTOFF_DISTANCE",
|
|
123
|
+
CUTOFF_DISTANCE = float(os.getenv(_ENV_PREFIX + "CUTOFF_DISTANCE", 100))
|
|
119
124
|
# In seconds
|
|
120
125
|
CUTOFF_TIME = float(os.getenv(_ENV_PREFIX + "CUTOFF_TIME", 60))
|
|
121
126
|
DUPLICATE_DISTANCE = float(os.getenv(_ENV_PREFIX + "DUPLICATE_DISTANCE", 0.1))
|
|
@@ -136,6 +141,13 @@ MAX_SEQUENCE_FILESIZE: int | None = _parse_filesize(
|
|
|
136
141
|
MAX_SEQUENCE_PIXELS: int | None = _parse_pixels(
|
|
137
142
|
os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_PIXELS", "6G")
|
|
138
143
|
)
|
|
144
|
+
# Zig-zag detection parameters
|
|
145
|
+
ZIGZAG_WINDOW_SIZE = int(os.getenv(_ENV_PREFIX + "ZIGZAG_WINDOW_SIZE", 5))
|
|
146
|
+
ZIGZAG_DEVIATION_THRESHOLD = float(
|
|
147
|
+
os.getenv(_ENV_PREFIX + "ZIGZAG_DEVIATION_THRESHOLD", 0.8)
|
|
148
|
+
)
|
|
149
|
+
ZIGZAG_MIN_DEVIATIONS = int(os.getenv(_ENV_PREFIX + "ZIGZAG_MIN_DEVIATIONS", 1))
|
|
150
|
+
ZIGZAG_MIN_DISTANCE = float(os.getenv(_ENV_PREFIX + "ZIGZAG_MIN_DISTANCE", 30))
|
|
139
151
|
|
|
140
152
|
|
|
141
153
|
##################
|
mapillary_tools/exceptions.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 typing as T
|
|
@@ -107,6 +112,10 @@ class MapillaryNullIslandError(MapillaryDescriptionError):
|
|
|
107
112
|
pass
|
|
108
113
|
|
|
109
114
|
|
|
115
|
+
class MapillaryZigZagError(MapillaryDescriptionError):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
110
119
|
class MapillaryUploadConnectionError(MapillaryUserError):
|
|
111
120
|
exit_code = 12
|
|
112
121
|
|
mapillary_tools/exif_read.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 abc
|
|
@@ -14,6 +19,8 @@ from pathlib import Path
|
|
|
14
19
|
import exifread
|
|
15
20
|
from exifread.utils import Ratio
|
|
16
21
|
|
|
22
|
+
from .utils import sanitize_serial
|
|
23
|
+
|
|
17
24
|
|
|
18
25
|
LOG = logging.getLogger(__name__)
|
|
19
26
|
XMP_NAMESPACES = {
|
|
@@ -24,6 +31,7 @@ XMP_NAMESPACES = {
|
|
|
24
31
|
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
25
32
|
"x": "adobe:ns:meta/",
|
|
26
33
|
"GPano": "http://ns.google.com/photos/1.0/panorama/",
|
|
34
|
+
"aux": "http://ns.adobe.com/exif/1.0/aux/",
|
|
27
35
|
}
|
|
28
36
|
# https://github.com/ianare/exif-py/issues/167
|
|
29
37
|
EXIFREAD_LOG = logging.getLogger("exifread")
|
|
@@ -329,6 +337,10 @@ class ExifReadABC(abc.ABC):
|
|
|
329
337
|
def extract_orientation(self) -> int:
|
|
330
338
|
raise NotImplementedError
|
|
331
339
|
|
|
340
|
+
@abc.abstractmethod
|
|
341
|
+
def extract_camera_uuid(self) -> str | None:
|
|
342
|
+
raise NotImplementedError
|
|
343
|
+
|
|
332
344
|
|
|
333
345
|
class ExifReadFromXMP(ExifReadABC):
|
|
334
346
|
def __init__(self, etree: et.ElementTree):
|
|
@@ -477,6 +489,39 @@ class ExifReadFromXMP(ExifReadABC):
|
|
|
477
489
|
return 1
|
|
478
490
|
return orientation
|
|
479
491
|
|
|
492
|
+
def extract_camera_uuid(self) -> str | None:
|
|
493
|
+
"""
|
|
494
|
+
Extract camera unique identifier from serial number tags in XMP.
|
|
495
|
+
Builds a composite ID from body and lens serial numbers.
|
|
496
|
+
"""
|
|
497
|
+
body_serial = self._extract_alternative_fields(
|
|
498
|
+
[
|
|
499
|
+
"exif:SerialNumber",
|
|
500
|
+
"exif:BodySerialNumber",
|
|
501
|
+
"exif:CameraSerialNumber",
|
|
502
|
+
"exifEX:SerialNumber",
|
|
503
|
+
"exifEX:BodySerialNumber",
|
|
504
|
+
"aux:SerialNumber",
|
|
505
|
+
],
|
|
506
|
+
str,
|
|
507
|
+
)
|
|
508
|
+
lens_serial = self._extract_alternative_fields(
|
|
509
|
+
[
|
|
510
|
+
"exif:LensSerialNumber",
|
|
511
|
+
"exifEX:LensSerialNumber",
|
|
512
|
+
"aux:LensSerialNumber",
|
|
513
|
+
],
|
|
514
|
+
str,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
parts = [
|
|
518
|
+
s for s in [sanitize_serial(body_serial), sanitize_serial(lens_serial)] if s
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
if parts:
|
|
522
|
+
return "_".join(parts)
|
|
523
|
+
return None
|
|
524
|
+
|
|
480
525
|
def _extract_alternative_fields(
|
|
481
526
|
self,
|
|
482
527
|
fields: T.Iterable[str],
|
|
@@ -811,6 +856,38 @@ class ExifReadFromEXIF(ExifReadABC):
|
|
|
811
856
|
return 1
|
|
812
857
|
return orientation
|
|
813
858
|
|
|
859
|
+
def extract_camera_uuid(self) -> str | None:
|
|
860
|
+
"""
|
|
861
|
+
Extract camera unique identifier from serial number EXIF tags.
|
|
862
|
+
Builds a composite ID from body and lens serial numbers.
|
|
863
|
+
"""
|
|
864
|
+
body_serial = self._extract_alternative_fields(
|
|
865
|
+
[
|
|
866
|
+
"EXIF BodySerialNumber",
|
|
867
|
+
"EXIF SerialNumber",
|
|
868
|
+
"EXIF CameraSerialNumber",
|
|
869
|
+
"Image BodySerialNumber",
|
|
870
|
+
"MakerNote SerialNumber",
|
|
871
|
+
"MakerNote InternalSerialNumber",
|
|
872
|
+
],
|
|
873
|
+
str,
|
|
874
|
+
)
|
|
875
|
+
lens_serial = self._extract_alternative_fields(
|
|
876
|
+
[
|
|
877
|
+
"EXIF LensSerialNumber",
|
|
878
|
+
"Image LensSerialNumber",
|
|
879
|
+
],
|
|
880
|
+
str,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
parts = [
|
|
884
|
+
s for s in [sanitize_serial(body_serial), sanitize_serial(lens_serial)] if s
|
|
885
|
+
]
|
|
886
|
+
|
|
887
|
+
if parts:
|
|
888
|
+
return "_".join(parts)
|
|
889
|
+
return None
|
|
890
|
+
|
|
814
891
|
def _extract_alternative_fields(
|
|
815
892
|
self,
|
|
816
893
|
fields: T.Iterable[str],
|
|
@@ -982,3 +1059,15 @@ class ExifRead(ExifReadFromEXIF):
|
|
|
982
1059
|
if val is not None:
|
|
983
1060
|
return val
|
|
984
1061
|
return None
|
|
1062
|
+
|
|
1063
|
+
def extract_camera_uuid(self) -> str | None:
|
|
1064
|
+
val = super().extract_camera_uuid()
|
|
1065
|
+
if val is not None:
|
|
1066
|
+
return val
|
|
1067
|
+
xmp = self._xmp_with_reason("camera_uuid")
|
|
1068
|
+
if xmp is None:
|
|
1069
|
+
return None
|
|
1070
|
+
val = xmp.extract_camera_uuid()
|
|
1071
|
+
if val is not None:
|
|
1072
|
+
return val
|
|
1073
|
+
return None
|
mapillary_tools/exif_write.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, 21, 24]
|
|
2
7
|
from __future__ import annotations
|
|
3
8
|
|
|
@@ -214,6 +219,18 @@ class ExifEdit:
|
|
|
214
219
|
else:
|
|
215
220
|
del self._ef[ifd][tag]
|
|
216
221
|
# retry later
|
|
222
|
+
elif "thumbnail is too large" in message.lower():
|
|
223
|
+
# Handle oversized thumbnails (max 64kB per EXIF spec)
|
|
224
|
+
if thumbnail_removed:
|
|
225
|
+
raise exc
|
|
226
|
+
LOG.debug(
|
|
227
|
+
"Thumbnail too large (max 64kB) -- removing thumbnail and 1st: %s",
|
|
228
|
+
exc,
|
|
229
|
+
)
|
|
230
|
+
del self._ef["thumbnail"]
|
|
231
|
+
del self._ef["1st"]
|
|
232
|
+
thumbnail_removed = True
|
|
233
|
+
# retry later
|
|
217
234
|
else:
|
|
218
235
|
raise exc
|
|
219
236
|
except Exception as exc:
|