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.
Files changed (59) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +94 -4
  3. mapillary_tools/{geotag → camm}/camm_builder.py +73 -61
  4. mapillary_tools/camm/camm_parser.py +561 -0
  5. mapillary_tools/commands/__init__.py +0 -1
  6. mapillary_tools/commands/__main__.py +0 -6
  7. mapillary_tools/commands/process.py +0 -50
  8. mapillary_tools/commands/upload.py +1 -26
  9. mapillary_tools/constants.py +2 -2
  10. mapillary_tools/exiftool_read_video.py +13 -11
  11. mapillary_tools/ffmpeg.py +2 -2
  12. mapillary_tools/geo.py +0 -54
  13. mapillary_tools/geotag/blackvue_parser.py +4 -4
  14. mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
  15. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
  16. mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
  17. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
  18. mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
  19. mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
  20. mapillary_tools/geotag/gpmf_parser.py +346 -83
  21. mapillary_tools/mp4/__init__.py +0 -0
  22. mapillary_tools/{geotag → mp4}/construct_mp4_parser.py +32 -16
  23. mapillary_tools/mp4/mp4_sample_parser.py +322 -0
  24. mapillary_tools/{geotag → mp4}/simple_mp4_builder.py +64 -38
  25. mapillary_tools/process_geotag_properties.py +25 -19
  26. mapillary_tools/process_sequence_properties.py +6 -6
  27. mapillary_tools/sample_video.py +17 -16
  28. mapillary_tools/telemetry.py +71 -0
  29. mapillary_tools/types.py +18 -0
  30. mapillary_tools/upload.py +74 -233
  31. mapillary_tools/upload_api_v4.py +8 -9
  32. mapillary_tools/utils.py +9 -16
  33. mapillary_tools/video_data_extraction/cli_options.py +0 -1
  34. mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
  35. mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
  36. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
  37. mapillary_tools/video_data_extraction/extractors/camm_parser.py +13 -16
  38. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
  39. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
  40. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
  41. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
  42. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +90 -11
  43. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
  44. mapillary_tools/video_data_extraction/video_data_parser_factory.py +13 -20
  45. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/METADATA +10 -3
  46. mapillary_tools-0.13.1.dist-info/RECORD +75 -0
  47. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/WHEEL +1 -1
  48. mapillary_tools/commands/upload_blackvue.py +0 -33
  49. mapillary_tools/commands/upload_camm.py +0 -33
  50. mapillary_tools/commands/upload_zip.py +0 -33
  51. mapillary_tools/geotag/camm_parser.py +0 -306
  52. mapillary_tools/geotag/mp4_sample_parser.py +0 -426
  53. mapillary_tools/process_import_meta_properties.py +0 -76
  54. mapillary_tools-0.12.1.dist-info/RECORD +0 -77
  55. /mapillary_tools/{geotag → mp4}/io_utils.py +0 -0
  56. /mapillary_tools/{geotag → mp4}/simple_mp4_parser.py +0 -0
  57. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/LICENSE +0 -0
  58. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/entry_points.txt +0 -0
  59. {mapillary_tools-0.12.1.dist-info → mapillary_tools-0.13.1.dist-info}/top_level.txt +0 -0
@@ -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 = requests.get(
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 = requests.post(
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 == self.entity_size
159
- ), f"Offset ends at {offset} but the entity size is {self.entity_size}"
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 = requests.post(
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 check_file_suffix:
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 check_file_suffix:
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 check_file_suffix:
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
- xml_paths.append(path)
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)
@@ -20,4 +20,3 @@ class CliOptions(T.TypedDict, total=False):
20
20
  num_processes: int
21
21
  device_make: T.Optional[str]
22
22
  device_model: T.Optional[str]
23
- check_file_suffix: bool
@@ -5,20 +5,19 @@ from pathlib import Path
5
5
 
6
6
  import tqdm
7
7
 
8
- import mapillary_tools.geotag.utils as video_utils
9
-
10
- from mapillary_tools import exceptions, geo, utils
11
- from mapillary_tools.geotag import gpmf_gps_filter
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 mapillary_tools.video_data_extraction import video_data_parser_factory
20
- from mapillary_tools.video_data_extraction.cli_options import CliOptions
21
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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.warn(
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
- points = self._sanitize_points(points)
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, geo.PointWithFix) for p in points):
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[geo.PointWithFix], points)
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 mapillary_tools import geo
10
- from mapillary_tools.video_data_extraction.cli_options import (
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 mapillary_tools import geo
4
- from mapillary_tools.geotag import blackvue_parser, simple_mp4_parser
5
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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 simple_mp4_parser.ParsingError:
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 mapillary_tools import geo
5
- from mapillary_tools.geotag import camm_parser, simple_mp4_parser
6
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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 __camera_info(self) -> T.Tuple[str, str]:
16
- with self.videoPath.open("rb") as fp:
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 simple_mp4_parser.ParsingError:
31
+ except sparser.ParsingError:
27
32
  return []
28
33
 
29
34
  def extract_make(self) -> T.Optional[str]:
30
- source_path = self.geotag_source_path
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
- source_path = self.geotag_source_path
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 mapillary_tools import constants, exceptions, geo
7
- from mapillary_tools.video_data_extraction.cli_options import (
8
- CliOptions,
9
- CliParserOptions,
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 mapillary_tools import geo
7
- from mapillary_tools.exiftool_read import EXIFTOOL_NAMESPACES
8
- from mapillary_tools.exiftool_read_video import ExifToolReadVideo
9
- from mapillary_tools.geotag.geotag_videos_from_exiftool_video import _DESCRIPTION_TAG
10
- from mapillary_tools.video_data_extraction.cli_options import (
11
- CliOptions,
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
- return (
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 mapillary_tools import geo
5
- from mapillary_tools.video_data_extraction.cli_options import (
6
- CliOptions,
7
- CliParserOptions,
8
- )
9
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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 mapillary_tools import geo
4
- from mapillary_tools.geotag import gpmf_parser, simple_mp4_parser
5
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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 simple_mp4_parser.ParsingError:
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 mapillary_tools import geo
4
- from mapillary_tools.geotag import geotag_images_from_gpx_file
5
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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
- tracks = geotag_images_from_gpx_file.parse_gpx(path)
19
- except Exception as e:
20
- return []
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
- points: T.Sequence[geo.Point] = sum(tracks, [])
23
- return points
98
+ return offset
24
99
 
25
100
  def extract_make(self) -> T.Optional[str]:
26
- return None
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
- return None
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 mapillary_tools import geo
4
- from mapillary_tools.geotag import geotag_images_from_nmea_file
5
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
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 mapillary_tools.video_data_extraction.cli_options import CliOptions
5
-
6
- from mapillary_tools.video_data_extraction.extractors.base_parser import BaseParser
7
-
8
- from mapillary_tools.video_data_extraction.extractors.blackvue_parser import (
9
- BlackVueParser,
10
- )
11
- from mapillary_tools.video_data_extraction.extractors.camm_parser import CammParser
12
- from mapillary_tools.video_data_extraction.extractors.exiftool_runtime_parser import (
13
- ExiftoolRuntimeParser,
14
- )
15
- from mapillary_tools.video_data_extraction.extractors.exiftool_xml_parser import (
16
- ExiftoolXmlParser,
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
1
+ Metadata-Version: 2.2
2
2
  Name: mapillary_tools
3
- Version: 0.12.1
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
- Requires-Dist: dataclasses; python_version <= "3.6"
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/">