mapillary-tools 0.14.4__tar.gz → 0.14.6__tar.gz

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.
Files changed (88) hide show
  1. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/PKG-INFO +8 -1
  2. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/README.md +7 -0
  3. mapillary_tools-0.14.6/mapillary_tools/__init__.py +6 -0
  4. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/api_v4.py +5 -0
  5. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/authenticate.py +5 -1
  6. mapillary_tools-0.14.6/mapillary_tools/blackvue_parser.py +313 -0
  7. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/camm/camm_builder.py +5 -1
  8. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/camm/camm_parser.py +5 -0
  9. mapillary_tools-0.14.6/mapillary_tools/commands/__init__.py +15 -0
  10. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/__main__.py +5 -0
  11. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/authenticate.py +5 -0
  12. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/process.py +12 -0
  13. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/process_and_upload.py +5 -1
  14. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/sample_video.py +5 -0
  15. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/upload.py +5 -0
  16. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/video_process.py +5 -0
  17. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/video_process_and_upload.py +5 -1
  18. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/commands/zip.py +5 -0
  19. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/config.py +5 -0
  20. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/constants.py +13 -1
  21. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exceptions.py +9 -0
  22. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exif_read.py +89 -0
  23. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exif_write.py +75 -26
  24. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exiftool_read.py +89 -0
  25. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exiftool_read_video.py +56 -0
  26. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/exiftool_runner.py +5 -0
  27. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/ffmpeg.py +5 -0
  28. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geo.py +91 -31
  29. mapillary_tools-0.14.6/mapillary_tools/geotag/__init__.py +4 -0
  30. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/base.py +5 -0
  31. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/factory.py +5 -0
  32. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_exif.py +5 -0
  33. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_exiftool.py +5 -0
  34. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_gpx.py +5 -0
  35. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +5 -0
  36. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +5 -0
  37. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_images_from_video.py +6 -0
  38. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_videos_from_exiftool.py +5 -0
  39. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_videos_from_gpx.py +5 -0
  40. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/geotag_videos_from_video.py +5 -0
  41. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/image_extractors/base.py +5 -0
  42. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/image_extractors/exif.py +6 -0
  43. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/image_extractors/exiftool.py +5 -0
  44. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/options.py +5 -0
  45. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/utils.py +5 -0
  46. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/video_extractors/base.py +5 -0
  47. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/video_extractors/exiftool.py +7 -0
  48. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/video_extractors/gpx.py +5 -0
  49. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/geotag/video_extractors/native.py +5 -0
  50. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/gpmf/gpmf_gps_filter.py +5 -0
  51. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/gpmf/gpmf_parser.py +5 -0
  52. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/gpmf/gps_filter.py +5 -0
  53. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/history.py +5 -0
  54. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/http.py +5 -1
  55. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/ipc.py +5 -0
  56. mapillary_tools-0.14.6/mapillary_tools/mp4/__init__.py +4 -0
  57. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/mp4/construct_mp4_parser.py +5 -0
  58. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/mp4/io_utils.py +5 -0
  59. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/mp4/mp4_sample_parser.py +20 -1
  60. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/mp4/simple_mp4_builder.py +5 -0
  61. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/mp4/simple_mp4_parser.py +5 -0
  62. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/process_geotag_properties.py +5 -0
  63. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/process_sequence_properties.py +213 -31
  64. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/sample_video.py +13 -1
  65. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/serializer/description.py +13 -0
  66. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/serializer/gpx.py +5 -1
  67. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/store.py +5 -0
  68. mapillary_tools-0.14.6/mapillary_tools/telemetry.py +180 -0
  69. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/types.py +6 -0
  70. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/upload.py +5 -0
  71. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/upload_api_v4.py +5 -0
  72. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/uploader.py +9 -0
  73. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools/utils.py +17 -1
  74. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/PKG-INFO +8 -1
  75. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/pyproject.toml +6 -0
  76. mapillary_tools-0.14.4/mapillary_tools/__init__.py +0 -1
  77. mapillary_tools-0.14.4/mapillary_tools/blackvue_parser.py +0 -195
  78. mapillary_tools-0.14.4/mapillary_tools/commands/__init__.py +0 -10
  79. mapillary_tools-0.14.4/mapillary_tools/geotag/__init__.py +0 -0
  80. mapillary_tools-0.14.4/mapillary_tools/mp4/__init__.py +0 -0
  81. mapillary_tools-0.14.4/mapillary_tools/telemetry.py +0 -72
  82. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/LICENSE +0 -0
  83. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/SOURCES.txt +0 -0
  84. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/dependency_links.txt +0 -0
  85. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/entry_points.txt +0 -0
  86. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/requires.txt +0 -0
  87. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/mapillary_tools.egg-info/top_level.txt +0 -0
  88. {mapillary_tools-0.14.4 → mapillary_tools-0.14.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapillary_tools
3
- Version: 0.14.4
3
+ Version: 0.14.6
4
4
  Summary: Mapillary Image/Video Import Pipeline
5
5
  Author-email: Mapillary <support@mapillary.com>
6
6
  License: BSD
@@ -34,6 +34,13 @@ Requires-Dist: tqdm<5.0,>=4.0
34
34
  Requires-Dist: typing-extensions>=4.12.2
35
35
  Dynamic: license-file
36
36
 
37
+ <!--
38
+ Copyright (c) Meta Platforms, Inc. and affiliates.
39
+
40
+ This source code is licensed under the BSD license found in the
41
+ LICENSE file in the root directory of this source tree.
42
+ -->
43
+
37
44
  <p align="center">
38
45
  <a href="https://github.com/mapillary/mapillary_tools/">
39
46
  <img src="https://raw.githubusercontent.com/mapillary/mapillary_tools/main/docs/images/logo.png">
@@ -1,3 +1,10 @@
1
+ <!--
2
+ Copyright (c) Meta Platforms, Inc. and affiliates.
3
+
4
+ This source code is licensed under the BSD license found in the
5
+ LICENSE file in the root directory of this source tree.
6
+ -->
7
+
1
8
  <p align="center">
2
9
  <a href="https://github.com/mapillary/mapillary_tools/">
3
10
  <img src="https://raw.githubusercontent.com/mapillary/mapillary_tools/main/docs/images/logo.png">
@@ -0,0 +1,6 @@
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
+
6
+ VERSION = "0.14.6"
@@ -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 enum
@@ -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
@@ -0,0 +1,313 @@
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
+
6
+ from __future__ import annotations
7
+
8
+ import dataclasses
9
+ import datetime
10
+ import json
11
+ import logging
12
+ import re
13
+ import typing as T
14
+
15
+ import pynmea2
16
+
17
+ from . import telemetry
18
+ from .mp4 import simple_mp4_parser as sparser
19
+
20
+
21
+ LOG = logging.getLogger(__name__)
22
+ NMEA_LINE_REGEX = re.compile(
23
+ rb"""
24
+ ^\s*
25
+ \[(\d+)\] # Timestamp
26
+ \s*
27
+ (\$\w{5}.*) # NMEA message
28
+ \s*
29
+ (\[\d+\])? # Strange timestamp
30
+ \s*$
31
+ """,
32
+ re.X,
33
+ )
34
+
35
+
36
+ @dataclasses.dataclass
37
+ class BlackVueInfo:
38
+ # None and [] are equivalent here. Use None as default because:
39
+ # ValueError: mutable default <class 'list'> for field gps is not allowed: use default_factory
40
+ gps: list[telemetry.GPSPoint] | None = None
41
+ make: str = "BlackVue"
42
+ model: str = ""
43
+
44
+
45
+ def extract_blackvue_info(fp: T.BinaryIO) -> BlackVueInfo | None:
46
+ try:
47
+ gps_data = sparser.parse_mp4_data_first(fp, [b"free", b"gps "])
48
+ except sparser.ParsingError:
49
+ gps_data = None
50
+
51
+ if gps_data is None:
52
+ return None
53
+
54
+ points = _parse_gps_box(gps_data)
55
+ points.sort(key=lambda p: p.time)
56
+
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
60
+ first_point_time = points[0].time
61
+ for p in points:
62
+ p.time = p.time - first_point_time
63
+
64
+ # Camera model
65
+ try:
66
+ cprt_bytes = sparser.parse_mp4_data_first(fp, [b"free", b"cprt"])
67
+ except sparser.ParsingError:
68
+ cprt_bytes = None
69
+ model = ""
70
+
71
+ if cprt_bytes is None:
72
+ model = ""
73
+ else:
74
+ model = _extract_camera_model_from_cprt(cprt_bytes)
75
+
76
+ return BlackVueInfo(model=model, gps=points)
77
+
78
+
79
+ def extract_camera_model(fp: T.BinaryIO) -> str:
80
+ try:
81
+ cprt_bytes = sparser.parse_mp4_data_first(fp, [b"free", b"cprt"])
82
+ except sparser.ParsingError:
83
+ return ""
84
+
85
+ if cprt_bytes is None:
86
+ return ""
87
+
88
+ return _extract_camera_model_from_cprt(cprt_bytes)
89
+
90
+
91
+ def _extract_camera_model_from_cprt(cprt_bytes: bytes) -> str:
92
+ """
93
+ >>> _extract_camera_model_from_cprt(b' {"model":"DR900X Plus","ver":0.918,"lang":"English","direct":1,"psn":"","temp":34,"GPS":1}')
94
+ 'DR900X Plus'
95
+ >>> _extract_camera_model_from_cprt(b' Pittasoft Co., Ltd.;DR900S-1CH;1.008;English;1;D90SS1HAE00661;T69;')
96
+ 'DR900S-1CH'
97
+ """
98
+ cprt_bytes = cprt_bytes.strip().strip(b"\x00")
99
+
100
+ try:
101
+ cprt_str = cprt_bytes.decode("utf8")
102
+ except UnicodeDecodeError:
103
+ return ""
104
+
105
+ try:
106
+ cprt_json = json.loads(cprt_str)
107
+ except json.JSONDecodeError:
108
+ cprt_json = None
109
+
110
+ if cprt_json is not None:
111
+ return str(cprt_json.get("model", "")).strip()
112
+
113
+ fields = cprt_str.split(";")
114
+ if 2 <= len(fields):
115
+ model = fields[1]
116
+ if model:
117
+ return model.strip()
118
+ else:
119
+ return ""
120
+ else:
121
+ return ""
122
+
123
+
124
+ def _compute_timezone_offset_from_rmc(
125
+ epoch_sec: float, message: pynmea2.NMEASentence
126
+ ) -> float | None:
127
+ """
128
+ Compute timezone offset from an RMC message which has full date+time.
129
+
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
139
+
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)
143
+
144
+
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.
153
+ """
154
+ if not hasattr(message, "timestamp") or not message.timestamp:
155
+ return None
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."""
182
+ for line_bytes in gps_data.splitlines():
183
+ match = NMEA_LINE_REGEX.match(line_bytes)
184
+ if match is None:
185
+ continue
186
+ nmea_line_bytes = match.group(2)
187
+
188
+ if not nmea_line_bytes:
189
+ continue
190
+
191
+ try:
192
+ nmea_line = nmea_line_bytes.decode("utf8")
193
+ except UnicodeDecodeError:
194
+ continue
195
+
196
+ if not nmea_line:
197
+ continue
198
+
199
+ try:
200
+ message = pynmea2.parse(nmea_line)
201
+ except pynmea2.nmea.ParseError:
202
+ continue
203
+
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)
269
+
270
+ # https://tavotech.com/gps-nmea-sentence-structure/
271
+ if message.sentence_type in ["GGA"]:
272
+ if not message.is_valid:
273
+ continue
274
+ point = telemetry.GPSPoint(
275
+ time=corrected_epoch,
276
+ lat=message.latitude,
277
+ lon=message.longitude,
278
+ alt=message.altitude,
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,
284
+ )
285
+ points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
286
+
287
+ elif message.sentence_type in ["RMC", "GLL"]:
288
+ if not message.is_valid:
289
+ continue
290
+ point = telemetry.GPSPoint(
291
+ time=corrected_epoch,
292
+ lat=message.latitude,
293
+ lon=message.longitude,
294
+ alt=None,
295
+ angle=None,
296
+ epoch_time=corrected_epoch,
297
+ fix=None,
298
+ precision=None,
299
+ ground_speed=None,
300
+ )
301
+ points_by_sentence_type.setdefault(message.sentence_type, []).append(point)
302
+
303
+ # This is the extraction order in exiftool
304
+ if "RMC" in points_by_sentence_type:
305
+ return points_by_sentence_type["RMC"]
306
+
307
+ if "GGA" in points_by_sentence_type:
308
+ return points_by_sentence_type["GGA"]
309
+
310
+ if "GLL" in points_by_sentence_type:
311
+ return points_by_sentence_type["GLL"]
312
+
313
+ return []
@@ -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
 
@@ -0,0 +1,15 @@
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
+
6
+ # ruff: noqa: F401
7
+ from . import (
8
+ authenticate,
9
+ process,
10
+ process_and_upload,
11
+ sample_video,
12
+ upload,
13
+ video_process,
14
+ video_process_and_upload,
15
+ )
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import argparse
2
7
  import enum
3
8
  import logging
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import argparse
2
7
  import inspect
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,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import argparse
2
7
  import inspect
3
8
  from pathlib import Path
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import inspect
2
7
 
3
8
  from .. import constants
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import logging
2
7
 
3
8
  from ..types import FileType
@@ -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
 
@@ -1,3 +1,8 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # This source code is licensed under the BSD license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
1
6
  import argparse
2
7
  import inspect
3
8
  from pathlib import Path
@@ -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 configparser
@@ -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", 600))
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
  ##################
@@ -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