mapillary-tools 0.12.1__py3-none-any.whl → 0.13.1__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 +94 -4
- mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
- mapillary_tools/camm/camm_parser.py +561 -0
- mapillary_tools/commands/__init__.py +0 -1
- mapillary_tools/commands/__main__.py +0 -6
- mapillary_tools/commands/process.py +0 -50
- mapillary_tools/commands/upload.py +1 -26
- mapillary_tools/constants.py +2 -2
- mapillary_tools/exiftool_read_video.py +13 -11
- mapillary_tools/ffmpeg.py +2 -2
- mapillary_tools/geo.py +0 -54
- mapillary_tools/geotag/blackvue_parser.py +4 -4
- mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
- mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
- mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
- mapillary_tools/geotag/gpmf_parser.py +346 -83
- mapillary_tools/mp4/__init__.py +0 -0
- mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
- mapillary_tools/mp4/mp4_sample_parser.py +322 -0
- mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
- mapillary_tools/process_geotag_properties.py +25 -19
- mapillary_tools/process_sequence_properties.py +6 -6
- mapillary_tools/sample_video.py +17 -16
- mapillary_tools/telemetry.py +71 -0
- mapillary_tools/types.py +18 -0
- mapillary_tools/upload.py +74 -233
- mapillary_tools/upload_api_v4.py +8 -9
- mapillary_tools/utils.py +9 -16
- mapillary_tools/video_data_extraction/cli_options.py +0 -1
- mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
- mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
- mapillary_tools-0.13.1.dist-info/RECORD +75 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
- mapillary_tools/commands/upload_blackvue.py +0 -33
- mapillary_tools/commands/upload_camm.py +0 -33
- mapillary_tools/commands/upload_zip.py +0 -33
- mapillary_tools/geotag/camm_parser.py +0 -306
- mapillary_tools/geotag/mp4_sample_parser.py +0 -426
- mapillary_tools/process_import_meta_properties.py +0 -76
- mapillary_tools-0.12.1.dist-info/RECORD +0 -77
- /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
- /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/top_level.txt +0 -0
mapillary_tools/upload_api_v4.py
CHANGED
|
@@ -8,13 +8,12 @@ import typing as T
|
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
+
from .api_v4 import MAPILLARY_GRAPH_API_ENDPOINT, request_get, request_post
|
|
12
|
+
|
|
11
13
|
LOG = logging.getLogger(__name__)
|
|
12
14
|
MAPILLARY_UPLOAD_ENDPOINT = os.getenv(
|
|
13
15
|
"MAPILLARY_UPLOAD_ENDPOINT", "https://rupload.facebook.com/mapillary_public_uploads"
|
|
14
16
|
)
|
|
15
|
-
MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
|
|
16
|
-
"MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
|
|
17
|
-
)
|
|
18
17
|
DEFAULT_CHUNK_SIZE = 1024 * 1024 * 16 # 16MB
|
|
19
18
|
# According to the docs, UPLOAD_REQUESTS_TIMEOUT can be a tuple of
|
|
20
19
|
# (connection_timeout, read_timeout): https://requests.readthedocs.io/en/latest/user/advanced/#timeouts
|
|
@@ -93,7 +92,7 @@ class UploadService:
|
|
|
93
92
|
}
|
|
94
93
|
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
95
94
|
LOG.debug("GET %s", url)
|
|
96
|
-
resp =
|
|
95
|
+
resp = request_get(
|
|
97
96
|
url,
|
|
98
97
|
headers=headers,
|
|
99
98
|
timeout=REQUESTS_TIMEOUT,
|
|
@@ -134,7 +133,7 @@ class UploadService:
|
|
|
134
133
|
}
|
|
135
134
|
url = f"{MAPILLARY_UPLOAD_ENDPOINT}/{self.session_key}"
|
|
136
135
|
LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
|
|
137
|
-
resp =
|
|
136
|
+
resp = request_post(
|
|
138
137
|
url,
|
|
139
138
|
headers=headers,
|
|
140
139
|
data=chunk,
|
|
@@ -154,9 +153,9 @@ class UploadService:
|
|
|
154
153
|
if not chunk:
|
|
155
154
|
break
|
|
156
155
|
|
|
157
|
-
assert (
|
|
158
|
-
offset
|
|
159
|
-
)
|
|
156
|
+
assert offset == self.entity_size, (
|
|
157
|
+
f"Offset ends at {offset} but the entity size is {self.entity_size}"
|
|
158
|
+
)
|
|
160
159
|
|
|
161
160
|
payload = resp.json()
|
|
162
161
|
try:
|
|
@@ -180,7 +179,7 @@ class UploadService:
|
|
|
180
179
|
url = f"{MAPILLARY_GRAPH_API_ENDPOINT}/finish_upload"
|
|
181
180
|
|
|
182
181
|
LOG.debug("POST %s HEADERS %s", url, json.dumps(_sanitize_headers(headers)))
|
|
183
|
-
resp =
|
|
182
|
+
resp = request_post(
|
|
184
183
|
url,
|
|
185
184
|
headers=headers,
|
|
186
185
|
json=data,
|
mapillary_tools/utils.py
CHANGED
|
@@ -123,7 +123,6 @@ def deduplicate_paths(paths: T.Iterable[Path]) -> T.Generator[Path, None, None]:
|
|
|
123
123
|
def find_images(
|
|
124
124
|
import_paths: T.Sequence[Path],
|
|
125
125
|
skip_subfolders: bool = False,
|
|
126
|
-
check_file_suffix: bool = False,
|
|
127
126
|
) -> T.List[Path]:
|
|
128
127
|
image_paths: T.List[Path] = []
|
|
129
128
|
for path in import_paths:
|
|
@@ -134,10 +133,7 @@ def find_images(
|
|
|
134
133
|
if is_image_file(file)
|
|
135
134
|
)
|
|
136
135
|
else:
|
|
137
|
-
if
|
|
138
|
-
if is_image_file(path):
|
|
139
|
-
image_paths.append(path)
|
|
140
|
-
else:
|
|
136
|
+
if is_image_file(path):
|
|
141
137
|
image_paths.append(path)
|
|
142
138
|
return list(deduplicate_paths(image_paths))
|
|
143
139
|
|
|
@@ -145,7 +141,6 @@ def find_images(
|
|
|
145
141
|
def find_videos(
|
|
146
142
|
import_paths: T.Sequence[Path],
|
|
147
143
|
skip_subfolders: bool = False,
|
|
148
|
-
check_file_suffix: bool = False,
|
|
149
144
|
) -> T.List[Path]:
|
|
150
145
|
video_paths: T.List[Path] = []
|
|
151
146
|
for path in import_paths:
|
|
@@ -156,10 +151,7 @@ def find_videos(
|
|
|
156
151
|
if is_video_file(file)
|
|
157
152
|
)
|
|
158
153
|
else:
|
|
159
|
-
if
|
|
160
|
-
if is_video_file(path):
|
|
161
|
-
video_paths.append(path)
|
|
162
|
-
else:
|
|
154
|
+
if is_video_file(path):
|
|
163
155
|
video_paths.append(path)
|
|
164
156
|
return list(deduplicate_paths(video_paths))
|
|
165
157
|
|
|
@@ -167,7 +159,6 @@ def find_videos(
|
|
|
167
159
|
def find_zipfiles(
|
|
168
160
|
import_paths: T.Sequence[Path],
|
|
169
161
|
skip_subfolders: bool = False,
|
|
170
|
-
check_file_suffix: bool = False,
|
|
171
162
|
) -> T.List[Path]:
|
|
172
163
|
zip_paths: T.List[Path] = []
|
|
173
164
|
for path in import_paths:
|
|
@@ -178,10 +169,7 @@ def find_zipfiles(
|
|
|
178
169
|
if file.suffix.lower() in [".zip"]
|
|
179
170
|
)
|
|
180
171
|
else:
|
|
181
|
-
if
|
|
182
|
-
if path.suffix.lower() in [".zip"]:
|
|
183
|
-
zip_paths.append(path)
|
|
184
|
-
else:
|
|
172
|
+
if path.suffix.lower() in [".zip"]:
|
|
185
173
|
zip_paths.append(path)
|
|
186
174
|
return list(deduplicate_paths(zip_paths))
|
|
187
175
|
|
|
@@ -199,5 +187,10 @@ def find_xml_files(import_paths: T.Sequence[Path]) -> T.List[Path]:
|
|
|
199
187
|
if file.suffix.lower() in [".xml"]
|
|
200
188
|
)
|
|
201
189
|
else:
|
|
202
|
-
|
|
190
|
+
if path.suffix.lower() in [".xml"]:
|
|
191
|
+
xml_paths.append(path)
|
|
203
192
|
return list(deduplicate_paths(xml_paths))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_file_size(path: Path) -> int:
|
|
196
|
+
return os.path.getsize(path)
|
|
@@ -5,20 +5,19 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import tqdm
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from mapillary_tools.types import (
|
|
8
|
+
from .. import exceptions, geo, utils
|
|
9
|
+
from ..geotag import gpmf_gps_filter, utils as video_utils
|
|
10
|
+
from ..telemetry import GPSPoint
|
|
11
|
+
from ..types import (
|
|
13
12
|
ErrorMetadata,
|
|
14
13
|
FileType,
|
|
15
14
|
MetadataOrError,
|
|
16
15
|
VideoMetadata,
|
|
17
16
|
VideoMetadataOrError,
|
|
18
17
|
)
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
18
|
+
from . import video_data_parser_factory
|
|
19
|
+
from .cli_options import CliOptions
|
|
20
|
+
from .extractors.base_parser import BaseParser
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
LOG = logging.getLogger(__name__)
|
|
@@ -33,9 +32,7 @@ class VideoDataExtractor:
|
|
|
33
32
|
def process(self) -> T.List[MetadataOrError]:
|
|
34
33
|
paths = self.options["paths"]
|
|
35
34
|
self._check_paths(paths)
|
|
36
|
-
video_files = utils.find_videos(
|
|
37
|
-
paths, check_file_suffix=self.options["check_file_suffix"]
|
|
38
|
-
)
|
|
35
|
+
video_files = utils.find_videos(paths)
|
|
39
36
|
self._check_sources_cardinality(video_files)
|
|
40
37
|
|
|
41
38
|
num_processes = self.options["num_processes"] or None
|
|
@@ -81,7 +78,7 @@ class VideoDataExtractor:
|
|
|
81
78
|
make = parser.extract_make()
|
|
82
79
|
except Exception as e:
|
|
83
80
|
ex = e
|
|
84
|
-
LOG.
|
|
81
|
+
LOG.warning(
|
|
85
82
|
'%(filename)s: Exception for parser %(parser)s while processing source %(source)s: "%(e)s"',
|
|
86
83
|
{**log_vars, "e": e},
|
|
87
84
|
)
|
|
@@ -95,6 +92,7 @@ class VideoDataExtractor:
|
|
|
95
92
|
filename=file,
|
|
96
93
|
filetype=FileType.VIDEO,
|
|
97
94
|
md5sum=None,
|
|
95
|
+
filesize=utils.get_file_size(file),
|
|
98
96
|
points=points,
|
|
99
97
|
make=make,
|
|
100
98
|
model=model,
|
|
@@ -124,12 +122,7 @@ class VideoDataExtractor:
|
|
|
124
122
|
{**log_vars, "points": len(points)},
|
|
125
123
|
)
|
|
126
124
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if parser.must_rebase_times_to_zero:
|
|
130
|
-
points = self._rebase_times(points)
|
|
131
|
-
|
|
132
|
-
return points
|
|
125
|
+
return self._sanitize_points(points)
|
|
133
126
|
|
|
134
127
|
@staticmethod
|
|
135
128
|
def _check_paths(import_paths: T.Sequence[Path]):
|
|
@@ -163,11 +156,11 @@ class VideoDataExtractor:
|
|
|
163
156
|
|
|
164
157
|
points = geo.extend_deduplicate_points(points)
|
|
165
158
|
|
|
166
|
-
if all(isinstance(p,
|
|
159
|
+
if all(isinstance(p, GPSPoint) for p in points):
|
|
167
160
|
points = T.cast(
|
|
168
161
|
T.Sequence[geo.Point],
|
|
169
162
|
gpmf_gps_filter.remove_noisy_points(
|
|
170
|
-
T.cast(T.Sequence[
|
|
163
|
+
T.cast(T.Sequence[GPSPoint], points)
|
|
171
164
|
),
|
|
172
165
|
)
|
|
173
166
|
if not points:
|
|
@@ -181,14 +174,3 @@ class VideoDataExtractor:
|
|
|
181
174
|
raise exceptions.MapillaryStationaryVideoError("Stationary video")
|
|
182
175
|
|
|
183
176
|
return points
|
|
184
|
-
|
|
185
|
-
@staticmethod
|
|
186
|
-
def _rebase_times(points: T.Sequence[geo.Point]):
|
|
187
|
-
"""
|
|
188
|
-
Make point times start from 0
|
|
189
|
-
"""
|
|
190
|
-
if points:
|
|
191
|
-
first_timestamp = points[0].time
|
|
192
|
-
for p in points:
|
|
193
|
-
p.time = p.time - first_timestamp
|
|
194
|
-
return points
|
|
@@ -2,15 +2,11 @@ import abc
|
|
|
2
2
|
import functools
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
import sys
|
|
6
5
|
import typing as T
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
CliOptions,
|
|
12
|
-
CliParserOptions,
|
|
13
|
-
)
|
|
8
|
+
from ... import geo
|
|
9
|
+
from ..cli_options import CliOptions, CliParserOptions
|
|
14
10
|
|
|
15
11
|
LOG = logging.getLogger(__name__)
|
|
16
12
|
|
|
@@ -37,11 +33,6 @@ class BaseParser(metaclass=abc.ABCMeta):
|
|
|
37
33
|
def parser_label(self) -> str:
|
|
38
34
|
raise NotImplementedError
|
|
39
35
|
|
|
40
|
-
@property
|
|
41
|
-
@abc.abstractmethod
|
|
42
|
-
def must_rebase_times_to_zero(self) -> bool:
|
|
43
|
-
raise NotImplementedError
|
|
44
|
-
|
|
45
36
|
@abc.abstractmethod
|
|
46
37
|
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
47
38
|
raise NotImplementedError
|
|
@@ -71,3 +62,14 @@ class BaseParser(metaclass=abc.ABCMeta):
|
|
|
71
62
|
).resolve()
|
|
72
63
|
|
|
73
64
|
return abs_path if abs_path.is_file() else None
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
68
|
+
"""
|
|
69
|
+
Make point times start from 0
|
|
70
|
+
"""
|
|
71
|
+
if points:
|
|
72
|
+
first_timestamp = points[0].time
|
|
73
|
+
for p in points:
|
|
74
|
+
p.time = (p.time - first_timestamp) + offset
|
|
75
|
+
return points
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import typing as T
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from ... import geo
|
|
4
|
+
from ...geotag import blackvue_parser
|
|
5
|
+
from ...mp4 import simple_mp4_parser as sparser
|
|
6
|
+
from .base_parser import BaseParser
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class BlackVueParser(BaseParser):
|
|
@@ -21,7 +22,7 @@ class BlackVueParser(BaseParser):
|
|
|
21
22
|
points = blackvue_parser.extract_points(fp) or []
|
|
22
23
|
self.pointsFound = len(points) > 0
|
|
23
24
|
return points
|
|
24
|
-
except
|
|
25
|
+
except sparser.ParsingError:
|
|
25
26
|
return []
|
|
26
27
|
|
|
27
28
|
def extract_make(self) -> T.Optional[str]:
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import typing as T
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
from
|
|
4
|
+
from ... import geo
|
|
5
|
+
from ...camm import camm_parser
|
|
6
|
+
from ...mp4 import simple_mp4_parser as sparser
|
|
7
|
+
from .base_parser import BaseParser
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class CammParser(BaseParser):
|
|
@@ -12,8 +13,12 @@ class CammParser(BaseParser):
|
|
|
12
13
|
parser_label = "camm"
|
|
13
14
|
|
|
14
15
|
@functools.cached_property
|
|
15
|
-
def
|
|
16
|
-
|
|
16
|
+
def _camera_info(self) -> T.Tuple[str, str]:
|
|
17
|
+
source_path = self.geotag_source_path
|
|
18
|
+
if not source_path:
|
|
19
|
+
return "", ""
|
|
20
|
+
|
|
21
|
+
with source_path.open("rb") as fp:
|
|
17
22
|
return camm_parser.extract_camera_make_and_model(fp)
|
|
18
23
|
|
|
19
24
|
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
@@ -23,19 +28,11 @@ class CammParser(BaseParser):
|
|
|
23
28
|
with source_path.open("rb") as fp:
|
|
24
29
|
try:
|
|
25
30
|
return camm_parser.extract_points(fp) or []
|
|
26
|
-
except
|
|
31
|
+
except sparser.ParsingError:
|
|
27
32
|
return []
|
|
28
33
|
|
|
29
34
|
def extract_make(self) -> T.Optional[str]:
|
|
30
|
-
|
|
31
|
-
if not source_path:
|
|
32
|
-
return None
|
|
33
|
-
with source_path.open("rb") as fp:
|
|
34
|
-
return self.__camera_info[0] or None
|
|
35
|
+
return self._camera_info[0] or None
|
|
35
36
|
|
|
36
37
|
def extract_model(self) -> T.Optional[str]:
|
|
37
|
-
|
|
38
|
-
if not source_path:
|
|
39
|
-
return None
|
|
40
|
-
with source_path.open("rb") as fp:
|
|
41
|
-
return self.__camera_info[1] or None
|
|
38
|
+
return self._camera_info[1] or None
|
|
@@ -3,15 +3,10 @@ import subprocess
|
|
|
3
3
|
import typing as T
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
)
|
|
11
|
-
from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
|
|
12
|
-
from mapillary_tools.video_data_extraction.extractors.exiftool_xml_parser import (
|
|
13
|
-
ExiftoolXmlParser,
|
|
14
|
-
)
|
|
6
|
+
from ... import constants, exceptions, geo
|
|
7
|
+
from ..cli_options import CliOptions, CliParserOptions
|
|
8
|
+
from .base_parser import BaseParser
|
|
9
|
+
from .exiftool_xml_parser import ExiftoolXmlParser
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
class ExiftoolRuntimeParser(BaseParser):
|
|
@@ -3,20 +3,16 @@ import xml.etree.ElementTree as ET
|
|
|
3
3
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
CliParserOptions,
|
|
13
|
-
)
|
|
14
|
-
from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
|
|
6
|
+
from ... import geo
|
|
7
|
+
from ...exiftool_read import EXIFTOOL_NAMESPACES
|
|
8
|
+
from ...exiftool_read_video import ExifToolReadVideo
|
|
9
|
+
from ...geotag.geotag_videos_from_exiftool_video import _DESCRIPTION_TAG
|
|
10
|
+
from ..cli_options import CliOptions, CliParserOptions
|
|
11
|
+
from .base_parser import BaseParser
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
class ExiftoolXmlParser(BaseParser):
|
|
18
15
|
default_source_pattern = "%g.xml"
|
|
19
|
-
must_rebase_times_to_zero = True
|
|
20
16
|
parser_label = "exiftool_xml"
|
|
21
17
|
|
|
22
18
|
exifToolReadVideo: T.Optional[ExifToolReadVideo] = None
|
|
@@ -42,9 +38,11 @@ class ExiftoolXmlParser(BaseParser):
|
|
|
42
38
|
self.exifToolReadVideo = ExifToolReadVideo(ET.ElementTree(element))
|
|
43
39
|
|
|
44
40
|
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
45
|
-
|
|
41
|
+
gps_points = (
|
|
46
42
|
self.exifToolReadVideo.extract_gps_track() if self.exifToolReadVideo else []
|
|
47
43
|
)
|
|
44
|
+
self._rebase_times(gps_points)
|
|
45
|
+
return gps_points
|
|
48
46
|
|
|
49
47
|
def extract_make(self) -> T.Optional[str]:
|
|
50
48
|
return self.exifToolReadVideo.extract_make() if self.exifToolReadVideo else None
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import typing as T
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
from mapillary_tools.video_data_extraction.extractors.blackvue_parser import (
|
|
11
|
-
BlackVueParser,
|
|
12
|
-
)
|
|
13
|
-
from mapillary_tools.video_data_extraction.extractors.camm_parser import CammParser
|
|
14
|
-
from mapillary_tools.video_data_extraction.extractors.gopro_parser import GoProParser
|
|
4
|
+
from ... import geo
|
|
5
|
+
from ..cli_options import CliOptions, CliParserOptions
|
|
6
|
+
from .base_parser import BaseParser
|
|
7
|
+
from .blackvue_parser import BlackVueParser
|
|
8
|
+
from .camm_parser import CammParser
|
|
9
|
+
from .gopro_parser import GoProParser
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
class GenericVideoParser(BaseParser):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import typing as T
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from ... import geo
|
|
4
|
+
from ...geotag import gpmf_parser
|
|
5
|
+
from ...mp4 import simple_mp4_parser as sparser
|
|
6
|
+
from .base_parser import BaseParser
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class GoProParser(BaseParser):
|
|
@@ -21,10 +22,16 @@ class GoProParser(BaseParser):
|
|
|
21
22
|
points = gpmf_parser.extract_points(fp) or []
|
|
22
23
|
self.pointsFound = len(points) > 0
|
|
23
24
|
return points
|
|
24
|
-
except
|
|
25
|
+
except sparser.ParsingError:
|
|
25
26
|
return []
|
|
26
27
|
|
|
27
28
|
def extract_make(self) -> T.Optional[str]:
|
|
29
|
+
model = self.extract_model()
|
|
30
|
+
if model:
|
|
31
|
+
return "GoPro"
|
|
32
|
+
|
|
33
|
+
# make sure self.pointsFound is updated
|
|
34
|
+
_ = self.extract_points()
|
|
28
35
|
# If no points were found, assume this is not a GoPro
|
|
29
36
|
return "GoPro" if self.pointsFound else None
|
|
30
37
|
|
|
@@ -1,29 +1,108 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
1
3
|
import typing as T
|
|
2
4
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
5
|
+
from ... import geo, telemetry
|
|
6
|
+
from ...geotag import geotag_images_from_gpx_file
|
|
7
|
+
from .base_parser import BaseParser
|
|
8
|
+
from .generic_video_parser import GenericVideoParser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
LOG = logging.getLogger(__name__)
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
class GpxParser(BaseParser):
|
|
9
15
|
default_source_pattern = "%g.gpx"
|
|
10
|
-
must_rebase_times_to_zero = True
|
|
11
16
|
parser_label = "gpx"
|
|
12
17
|
|
|
13
18
|
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
14
19
|
path = self.geotag_source_path
|
|
15
20
|
if not path:
|
|
16
21
|
return []
|
|
22
|
+
|
|
17
23
|
try:
|
|
18
|
-
|
|
19
|
-
except Exception as
|
|
20
|
-
|
|
24
|
+
gpx_tracks = geotag_images_from_gpx_file.parse_gpx(path)
|
|
25
|
+
except Exception as ex:
|
|
26
|
+
raise RuntimeError(
|
|
27
|
+
f"Error parsing GPX {path}: {ex.__class__.__name__}: {ex}"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if 1 < len(gpx_tracks):
|
|
31
|
+
LOG.warning(
|
|
32
|
+
"Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
|
|
33
|
+
len(gpx_tracks),
|
|
34
|
+
self.videoPath,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
|
|
38
|
+
if not gpx_points:
|
|
39
|
+
return gpx_points
|
|
40
|
+
|
|
41
|
+
offset = self._synx_gpx_by_first_gps_timestamp(gpx_points)
|
|
42
|
+
|
|
43
|
+
self._rebase_times(gpx_points, offset=offset)
|
|
44
|
+
|
|
45
|
+
return gpx_points
|
|
46
|
+
|
|
47
|
+
def _synx_gpx_by_first_gps_timestamp(
|
|
48
|
+
self, gpx_points: T.Sequence[geo.Point]
|
|
49
|
+
) -> float:
|
|
50
|
+
offset: float = 0.0
|
|
51
|
+
|
|
52
|
+
if not gpx_points:
|
|
53
|
+
return offset
|
|
54
|
+
|
|
55
|
+
first_gpx_dt = datetime.datetime.fromtimestamp(
|
|
56
|
+
gpx_points[0].time, tz=datetime.timezone.utc
|
|
57
|
+
)
|
|
58
|
+
LOG.info("First GPX timestamp: %s", first_gpx_dt)
|
|
59
|
+
|
|
60
|
+
# Extract first GPS timestamp (if found) for synchronization
|
|
61
|
+
# Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
|
|
62
|
+
parser = GenericVideoParser(self.videoPath, self.options, {})
|
|
63
|
+
gps_points = parser.extract_points()
|
|
64
|
+
|
|
65
|
+
if not gps_points:
|
|
66
|
+
LOG.warning(
|
|
67
|
+
"Skip GPX synchronization because no GPS found in video %s",
|
|
68
|
+
self.videoPath,
|
|
69
|
+
)
|
|
70
|
+
return offset
|
|
71
|
+
|
|
72
|
+
first_gps_point = gps_points[0]
|
|
73
|
+
if isinstance(first_gps_point, telemetry.GPSPoint):
|
|
74
|
+
if first_gps_point.epoch_time is not None:
|
|
75
|
+
first_gps_dt = datetime.datetime.fromtimestamp(
|
|
76
|
+
first_gps_point.epoch_time, tz=datetime.timezone.utc
|
|
77
|
+
)
|
|
78
|
+
LOG.info("First GPS timestamp: %s", first_gps_dt)
|
|
79
|
+
offset = gpx_points[0].time - first_gps_point.epoch_time
|
|
80
|
+
if offset:
|
|
81
|
+
LOG.warning(
|
|
82
|
+
"Found offset between GPX %s and video GPS timestamps %s: %s seconds",
|
|
83
|
+
first_gpx_dt,
|
|
84
|
+
first_gps_dt,
|
|
85
|
+
offset,
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
LOG.info(
|
|
89
|
+
"GPX and GPS are perfectly synchronized (all starts from %s)",
|
|
90
|
+
first_gpx_dt,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
LOG.warning(
|
|
94
|
+
"Skip GPX synchronization because no GPS epoch time found in video %s",
|
|
95
|
+
self.videoPath,
|
|
96
|
+
)
|
|
21
97
|
|
|
22
|
-
|
|
23
|
-
return points
|
|
98
|
+
return offset
|
|
24
99
|
|
|
25
100
|
def extract_make(self) -> T.Optional[str]:
|
|
26
|
-
|
|
101
|
+
# Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
|
|
102
|
+
parser = GenericVideoParser(self.videoPath, self.options, {})
|
|
103
|
+
return parser.extract_make()
|
|
27
104
|
|
|
28
105
|
def extract_model(self) -> T.Optional[str]:
|
|
29
|
-
|
|
106
|
+
# Use an empty dictionary to force video parsers to extract make/model from the video metadata itself
|
|
107
|
+
parser = GenericVideoParser(self.videoPath, self.options, {})
|
|
108
|
+
return parser.extract_model()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import typing as T
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
from
|
|
3
|
+
from ... import geo
|
|
4
|
+
from ...geotag import geotag_images_from_nmea_file
|
|
5
|
+
from .base_parser import BaseParser
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class NmeaParser(BaseParser):
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import typing as T
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
from mapillary_tools.video_data_extraction.extractors.generic_video_parser import (
|
|
19
|
-
GenericVideoParser,
|
|
20
|
-
)
|
|
21
|
-
from mapillary_tools.video_data_extraction.extractors.gopro_parser import GoProParser
|
|
22
|
-
from mapillary_tools.video_data_extraction.extractors.gpx_parser import GpxParser
|
|
23
|
-
from mapillary_tools.video_data_extraction.extractors.nmea_parser import NmeaParser
|
|
4
|
+
from .cli_options import CliOptions
|
|
5
|
+
|
|
6
|
+
from .extractors.base_parser import BaseParser
|
|
7
|
+
|
|
8
|
+
from .extractors.blackvue_parser import BlackVueParser
|
|
9
|
+
from .extractors.camm_parser import CammParser
|
|
10
|
+
|
|
11
|
+
from .extractors.exiftool_runtime_parser import ExiftoolRuntimeParser
|
|
12
|
+
from .extractors.exiftool_xml_parser import ExiftoolXmlParser
|
|
13
|
+
from .extractors.generic_video_parser import GenericVideoParser
|
|
14
|
+
from .extractors.gopro_parser import GoProParser
|
|
15
|
+
from .extractors.gpx_parser import GpxParser
|
|
16
|
+
from .extractors.nmea_parser import NmeaParser
|
|
24
17
|
|
|
25
18
|
|
|
26
19
|
known_parsers = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: mapillary_tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Mapillary Image/Video Import Pipeline
|
|
5
5
|
Home-page: https://github.com/mapillary/mapillary_tools
|
|
6
6
|
Author: Mapillary
|
|
@@ -18,7 +18,14 @@ Requires-Dist: requests[socks]<3.0.0,>=2.20.0
|
|
|
18
18
|
Requires-Dist: tqdm<5.0,>=4.0
|
|
19
19
|
Requires-Dist: typing_extensions
|
|
20
20
|
Requires-Dist: jsonschema~=4.17.3
|
|
21
|
-
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
22
29
|
|
|
23
30
|
<p align="center">
|
|
24
31
|
<a href="https://github.com/mapillary/mapillary_tools/">
|