mapillary-tools 0.14.0a1__py3-none-any.whl → 0.14.0b1__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 (76) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +5 -4
  3. mapillary_tools/authenticate.py +9 -9
  4. mapillary_tools/blackvue_parser.py +79 -22
  5. mapillary_tools/camm/camm_parser.py +5 -5
  6. mapillary_tools/commands/__main__.py +1 -2
  7. mapillary_tools/config.py +41 -18
  8. mapillary_tools/constants.py +3 -2
  9. mapillary_tools/exceptions.py +1 -1
  10. mapillary_tools/exif_read.py +65 -65
  11. mapillary_tools/exif_write.py +7 -7
  12. mapillary_tools/exiftool_read.py +23 -46
  13. mapillary_tools/exiftool_read_video.py +88 -49
  14. mapillary_tools/exiftool_runner.py +4 -24
  15. mapillary_tools/ffmpeg.py +417 -242
  16. mapillary_tools/geo.py +4 -21
  17. mapillary_tools/geotag/__init__.py +0 -1
  18. mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
  19. mapillary_tools/geotag/factory.py +105 -103
  20. mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
  21. mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
  22. mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
  23. mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
  24. mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
  25. mapillary_tools/geotag/geotag_images_from_video.py +51 -14
  26. mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
  27. mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
  28. mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
  29. mapillary_tools/geotag/image_extractors/base.py +18 -0
  30. mapillary_tools/geotag/image_extractors/exif.py +60 -0
  31. mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
  32. mapillary_tools/geotag/options.py +26 -3
  33. mapillary_tools/geotag/utils.py +62 -0
  34. mapillary_tools/geotag/video_extractors/base.py +18 -0
  35. mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
  36. mapillary_tools/geotag/video_extractors/gpx.py +116 -0
  37. mapillary_tools/geotag/video_extractors/native.py +135 -0
  38. mapillary_tools/gpmf/gpmf_parser.py +16 -16
  39. mapillary_tools/gpmf/gps_filter.py +5 -3
  40. mapillary_tools/history.py +8 -3
  41. mapillary_tools/mp4/construct_mp4_parser.py +9 -8
  42. mapillary_tools/mp4/mp4_sample_parser.py +27 -27
  43. mapillary_tools/mp4/simple_mp4_builder.py +10 -9
  44. mapillary_tools/mp4/simple_mp4_parser.py +13 -12
  45. mapillary_tools/process_geotag_properties.py +21 -15
  46. mapillary_tools/process_sequence_properties.py +49 -49
  47. mapillary_tools/sample_video.py +15 -14
  48. mapillary_tools/serializer/description.py +587 -0
  49. mapillary_tools/serializer/gpx.py +132 -0
  50. mapillary_tools/telemetry.py +6 -5
  51. mapillary_tools/types.py +64 -635
  52. mapillary_tools/upload.py +176 -197
  53. mapillary_tools/upload_api_v4.py +94 -51
  54. mapillary_tools/uploader.py +284 -138
  55. mapillary_tools/utils.py +16 -18
  56. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
  57. mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
  58. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
  59. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
  60. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
  61. mapillary_tools/video_data_extraction/cli_options.py +0 -22
  62. mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
  63. mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
  64. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
  65. mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
  66. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
  67. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
  68. mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
  69. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
  70. mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
  71. mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
  72. mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
  73. mapillary_tools-0.14.0a1.dist-info/RECORD +0 -78
  74. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
  75. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
  76. {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
@@ -1,140 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
- import datetime
5
3
  import logging
6
-
4
+ import sys
7
5
  import typing as T
8
6
  from pathlib import Path
9
7
 
10
- from .. import geo, telemetry, types
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
12
+
13
+ from .. import exceptions, types
11
14
  from . import options
12
- from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
13
- from .geotag_images_from_gpx_file import parse_gpx
14
- from .geotag_videos_from_video import NativeVideoExtractor
15
+ from .base import GeotagVideosFromGeneric
16
+ from .video_extractors.gpx import GPXVideoExtractor
15
17
 
16
18
 
17
19
  LOG = logging.getLogger(__name__)
18
20
 
19
21
 
20
- class GPXVideoExtractor(GenericVideoExtractor):
21
- def __init__(self, video_path: Path, gpx_path: Path):
22
- self.video_path = video_path
23
- self.gpx_path = gpx_path
24
-
25
- def extract(self) -> types.VideoMetadataOrError:
26
- try:
27
- gpx_tracks = parse_gpx(self.gpx_path)
28
- except Exception as ex:
29
- raise RuntimeError(
30
- f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
31
- )
32
-
33
- if 1 < len(gpx_tracks):
34
- LOG.warning(
35
- "Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
36
- len(gpx_tracks),
37
- self.gpx_path,
38
- )
39
-
40
- gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
41
-
42
- native_extractor = NativeVideoExtractor(self.video_path)
43
-
44
- video_metadata_or_error = native_extractor.extract()
45
-
46
- if isinstance(video_metadata_or_error, types.ErrorMetadata):
47
- self._rebase_times(gpx_points)
48
- return types.VideoMetadata(
49
- filename=video_metadata_or_error.filename,
50
- filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
51
- points=gpx_points,
52
- )
53
-
54
- video_metadata = video_metadata_or_error
55
-
56
- offset = self._synx_gpx_by_first_gps_timestamp(
57
- gpx_points, video_metadata.points
58
- )
59
-
60
- self._rebase_times(gpx_points, offset=offset)
61
-
62
- return dataclasses.replace(video_metadata_or_error, points=gpx_points)
63
-
64
- @staticmethod
65
- def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
66
- """
67
- Make point times start from 0
68
- """
69
- if points:
70
- first_timestamp = points[0].time
71
- for p in points:
72
- p.time = (p.time - first_timestamp) + offset
73
- return points
74
-
75
- def _synx_gpx_by_first_gps_timestamp(
76
- self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
77
- ) -> float:
78
- offset: float = 0.0
79
-
80
- if not gpx_points:
81
- return offset
82
-
83
- first_gpx_dt = datetime.datetime.fromtimestamp(
84
- gpx_points[0].time, tz=datetime.timezone.utc
85
- )
86
- LOG.info("First GPX timestamp: %s", first_gpx_dt)
87
-
88
- if not video_gps_points:
89
- LOG.warning(
90
- "Skip GPX synchronization because no GPS found in video %s",
91
- self.video_path,
92
- )
93
- return offset
94
-
95
- first_gps_point = video_gps_points[0]
96
- if isinstance(first_gps_point, telemetry.GPSPoint):
97
- if first_gps_point.epoch_time is not None:
98
- first_gps_dt = datetime.datetime.fromtimestamp(
99
- first_gps_point.epoch_time, tz=datetime.timezone.utc
100
- )
101
- LOG.info("First GPS timestamp: %s", first_gps_dt)
102
- offset = gpx_points[0].time - first_gps_point.epoch_time
103
- if offset:
104
- LOG.warning(
105
- "Found offset between GPX %s and video GPS timestamps %s: %s seconds",
106
- first_gpx_dt,
107
- first_gps_dt,
108
- offset,
109
- )
110
- else:
111
- LOG.info(
112
- "GPX and GPS are perfectly synchronized (all starts from %s)",
113
- first_gpx_dt,
114
- )
115
- else:
116
- LOG.warning(
117
- "Skip GPX synchronization because no GPS epoch time found in video %s",
118
- self.video_path,
119
- )
120
-
121
- return offset
122
-
123
-
124
22
  class GeotagVideosFromGPX(GeotagVideosFromGeneric):
125
23
  def __init__(
126
24
  self,
127
- video_paths: T.Sequence[Path],
128
- option: options.SourcePathOption | None = None,
25
+ source_path: options.SourcePathOption | None = None,
129
26
  num_processes: int | None = None,
130
27
  ):
131
- super().__init__(video_paths, num_processes=num_processes)
132
- if option is None:
133
- option = options.SourcePathOption(pattern="%f.gpx")
134
- self.option = option
135
-
136
- def _generate_image_extractors(self) -> T.Sequence[GPXVideoExtractor]:
137
- return [
138
- GPXVideoExtractor(video_path, self.option.resolve(video_path))
139
- for video_path in self.video_paths
140
- ]
28
+ super().__init__(num_processes=num_processes)
29
+ if source_path is None:
30
+ source_path = options.SourcePathOption(pattern="%g.gpx")
31
+ self.source_path = source_path
32
+
33
+ @override
34
+ def _generate_video_extractors(
35
+ self, video_paths: T.Sequence[Path]
36
+ ) -> T.Sequence[GPXVideoExtractor | types.ErrorMetadata]:
37
+ results: list[GPXVideoExtractor | types.ErrorMetadata] = []
38
+ for video_path in video_paths:
39
+ source_path = self.source_path.resolve(video_path)
40
+ if source_path.is_file():
41
+ results.append(GPXVideoExtractor(video_path, source_path))
42
+ else:
43
+ results.append(
44
+ types.describe_error_metadata(
45
+ exceptions.MapillaryVideoGPSNotFoundError(
46
+ "GPX file not found for video"
47
+ ),
48
+ filename=video_path,
49
+ filetype=types.FileType.VIDEO,
50
+ )
51
+ )
52
+ return results
@@ -1,165 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  import typing as T
4
5
  from pathlib import Path
5
6
 
6
- from .. import blackvue_parser, exceptions, geo, telemetry, types, utils
7
- from ..camm import camm_parser
8
- from ..gpmf import gpmf_gps_filter, gpmf_parser
9
- from ..types import FileType
10
- from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
11
-
12
-
13
- class GoProVideoExtractor(GenericVideoExtractor):
14
- def extract(self) -> types.VideoMetadataOrError:
15
- with self.video_path.open("rb") as fp:
16
- gopro_info = gpmf_parser.extract_gopro_info(fp)
17
-
18
- if gopro_info is None:
19
- raise exceptions.MapillaryVideoGPSNotFoundError(
20
- "No GPS data found from the video"
21
- )
22
-
23
- gps_points = gopro_info.gps
24
- assert gps_points is not None, "must have GPS data extracted"
25
- if not gps_points:
26
- # Instead of raising an exception, return error metadata to tell the file type
27
- ex: exceptions.MapillaryDescriptionError = (
28
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
29
- )
30
- return types.describe_error_metadata(
31
- ex, self.video_path, filetype=FileType.GOPRO
32
- )
33
-
34
- gps_points = T.cast(
35
- T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
36
- )
37
- if not gps_points:
38
- # Instead of raising an exception, return error metadata to tell the file type
39
- ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
40
- return types.describe_error_metadata(
41
- ex, self.video_path, filetype=FileType.GOPRO
42
- )
43
-
44
- video_metadata = types.VideoMetadata(
45
- filename=self.video_path,
46
- filesize=utils.get_file_size(self.video_path),
47
- filetype=FileType.GOPRO,
48
- points=T.cast(T.List[geo.Point], gps_points),
49
- make=gopro_info.make,
50
- model=gopro_info.model,
51
- )
52
-
53
- return video_metadata
54
-
55
-
56
- class CAMMVideoExtractor(GenericVideoExtractor):
57
- def extract(self) -> types.VideoMetadataOrError:
58
- with self.video_path.open("rb") as fp:
59
- camm_info = camm_parser.extract_camm_info(fp)
60
-
61
- if camm_info is None:
62
- raise exceptions.MapillaryVideoGPSNotFoundError(
63
- "No GPS data found from the video"
64
- )
65
-
66
- if not camm_info.gps and not camm_info.mini_gps:
67
- # Instead of raising an exception, return error metadata to tell the file type
68
- ex: exceptions.MapillaryDescriptionError = (
69
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
70
- )
71
- return types.describe_error_metadata(
72
- ex, self.video_path, filetype=FileType.CAMM
73
- )
74
-
75
- return types.VideoMetadata(
76
- filename=self.video_path,
77
- filesize=utils.get_file_size(self.video_path),
78
- filetype=FileType.CAMM,
79
- points=T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps),
80
- make=camm_info.make,
81
- model=camm_info.model,
82
- )
83
-
7
+ if sys.version_info >= (3, 12):
8
+ from typing import override
9
+ else:
10
+ from typing_extensions import override
84
11
 
85
- class BlackVueVideoExtractor(GenericVideoExtractor):
86
- def extract(self) -> types.VideoMetadataOrError:
87
- with self.video_path.open("rb") as fp:
88
- blackvue_info = blackvue_parser.extract_blackvue_info(fp)
89
-
90
- if blackvue_info is None:
91
- raise exceptions.MapillaryVideoGPSNotFoundError(
92
- "No GPS data found from the video"
93
- )
94
-
95
- if not blackvue_info.gps:
96
- # Instead of raising an exception, return error metadata to tell the file type
97
- ex: exceptions.MapillaryDescriptionError = (
98
- exceptions.MapillaryGPXEmptyError("Empty GPS data found")
99
- )
100
- return types.describe_error_metadata(
101
- ex, self.video_path, filetype=FileType.BLACKVUE
102
- )
103
-
104
- video_metadata = types.VideoMetadata(
105
- filename=self.video_path,
106
- filesize=utils.get_file_size(self.video_path),
107
- filetype=FileType.BLACKVUE,
108
- points=blackvue_info.gps or [],
109
- make=blackvue_info.make,
110
- model=blackvue_info.model,
111
- )
112
-
113
- return video_metadata
114
-
115
-
116
- class NativeVideoExtractor(GenericVideoExtractor):
117
- def __init__(self, video_path: Path, filetypes: set[FileType] | None = None):
118
- super().__init__(video_path)
119
- self.filetypes = filetypes
120
-
121
- def extract(self) -> types.VideoMetadataOrError:
122
- ft = self.filetypes
123
- extractor: GenericVideoExtractor
124
-
125
- if ft is None or FileType.VIDEO in ft or FileType.GOPRO in ft:
126
- extractor = GoProVideoExtractor(self.video_path)
127
- try:
128
- return extractor.extract()
129
- except exceptions.MapillaryVideoGPSNotFoundError:
130
- pass
131
-
132
- if ft is None or FileType.VIDEO in ft or FileType.CAMM in ft:
133
- extractor = CAMMVideoExtractor(self.video_path)
134
- try:
135
- return extractor.extract()
136
- except exceptions.MapillaryVideoGPSNotFoundError:
137
- pass
138
-
139
- if ft is None or FileType.VIDEO in ft or FileType.BLACKVUE in ft:
140
- extractor = BlackVueVideoExtractor(self.video_path)
141
- try:
142
- return extractor.extract()
143
- except exceptions.MapillaryVideoGPSNotFoundError:
144
- pass
145
-
146
- raise exceptions.MapillaryVideoGPSNotFoundError(
147
- "No GPS data found from the video"
148
- )
12
+ from ..types import FileType
13
+ from .base import GeotagVideosFromGeneric
14
+ from .video_extractors.native import NativeVideoExtractor
149
15
 
150
16
 
151
17
  class GeotagVideosFromVideo(GeotagVideosFromGeneric):
152
18
  def __init__(
153
19
  self,
154
- video_paths: T.Sequence[Path],
155
20
  filetypes: set[FileType] | None = None,
156
21
  num_processes: int | None = None,
157
22
  ):
158
- super().__init__(video_paths, num_processes=num_processes)
23
+ super().__init__(num_processes=num_processes)
159
24
  self.filetypes = filetypes
160
25
 
161
- def _generate_video_extractors(self) -> T.Sequence[GenericVideoExtractor]:
26
+ @override
27
+ def _generate_video_extractors(
28
+ self, video_paths: T.Sequence[Path]
29
+ ) -> T.Sequence[NativeVideoExtractor]:
162
30
  return [
163
- NativeVideoExtractor(path, filetypes=self.filetypes)
164
- for path in self.video_paths
31
+ NativeVideoExtractor(path, filetypes=self.filetypes) for path in video_paths
165
32
  ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from pathlib import Path
5
+
6
+ from ... import types
7
+
8
+
9
+ class BaseImageExtractor(abc.ABC):
10
+ """
11
+ Extracts metadata from an image file.
12
+ """
13
+
14
+ def __init__(self, image_path: Path):
15
+ self.image_path = image_path
16
+
17
+ def extract(self) -> types.ImageMetadataOrError:
18
+ raise NotImplementedError
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import sys
5
+ import typing as T
6
+ from pathlib import Path
7
+
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
12
+
13
+ from ... import exceptions, exif_read, geo, types, utils
14
+ from .base import BaseImageExtractor
15
+
16
+
17
+ class ImageEXIFExtractor(BaseImageExtractor):
18
+ def __init__(self, image_path: Path, skip_lonlat_error: bool = False):
19
+ super().__init__(image_path)
20
+ self.skip_lonlat_error = skip_lonlat_error
21
+
22
+ @contextlib.contextmanager
23
+ def _exif_context(self) -> T.Generator[exif_read.ExifReadABC, None, None]:
24
+ with self.image_path.open("rb") as fp:
25
+ yield exif_read.ExifRead(fp)
26
+
27
+ @override
28
+ def extract(self) -> types.ImageMetadata:
29
+ with self._exif_context() as exif:
30
+ lonlat = exif.extract_lon_lat()
31
+ if lonlat is None:
32
+ if not self.skip_lonlat_error:
33
+ raise exceptions.MapillaryGeoTaggingError(
34
+ "Unable to extract GPS Longitude or GPS Latitude from the image"
35
+ )
36
+ lonlat = (0.0, 0.0)
37
+ lon, lat = lonlat
38
+
39
+ capture_time = exif.extract_capture_time()
40
+ if capture_time is None:
41
+ raise exceptions.MapillaryGeoTaggingError(
42
+ "Unable to extract timestamp from the image"
43
+ )
44
+
45
+ image_metadata = types.ImageMetadata(
46
+ filename=self.image_path,
47
+ filesize=utils.get_file_size(self.image_path),
48
+ time=geo.as_unix_time(capture_time),
49
+ lat=lat,
50
+ lon=lon,
51
+ alt=exif.extract_altitude(),
52
+ angle=exif.extract_direction(),
53
+ width=exif.extract_width(),
54
+ height=exif.extract_height(),
55
+ MAPOrientation=exif.extract_orientation(),
56
+ MAPDeviceMake=exif.extract_make(),
57
+ MAPDeviceModel=exif.extract_model(),
58
+ )
59
+
60
+ return image_metadata
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import xml.etree.ElementTree as ET
5
+ from pathlib import Path
6
+
7
+ from ... import exiftool_read
8
+ from .exif import ImageEXIFExtractor
9
+
10
+
11
+ class ImageExifToolExtractor(ImageEXIFExtractor):
12
+ def __init__(self, image_path: Path, element: ET.Element):
13
+ super().__init__(image_path)
14
+ self.element = element
15
+
16
+ @contextlib.contextmanager
17
+ def _exif_context(self):
18
+ yield exiftool_read.ExifToolRead(ET.ElementTree(self.element))
@@ -28,6 +28,7 @@ class SourceType(enum.Enum):
28
28
  SOURCE_TYPE_ALIAS: dict[str, SourceType] = {
29
29
  "blackvue_videos": SourceType.BLACKVUE,
30
30
  "gopro_videos": SourceType.GOPRO,
31
+ "exiftool": SourceType.EXIFTOOL_RUNTIME,
31
32
  }
32
33
 
33
34
 
@@ -59,9 +60,13 @@ class SourceOption:
59
60
  elif k == "filetypes":
60
61
  kwargs[k] = {types.FileType(t) for t in v}
61
62
  elif k == "source_path":
62
- kwargs.setdefault("source_path", SourcePathOption()).source_path = v
63
+ kwargs.setdefault(
64
+ "source_path", SourcePathOption(source_path=Path(v))
65
+ ).sourthe_path = Path(v)
63
66
  elif k == "pattern":
64
- kwargs.setdefault("source_path", SourcePathOption()).pattern = v
67
+ kwargs.setdefault(
68
+ "source_path", SourcePathOption(pattern=v)
69
+ ).pattern = v
65
70
  elif k == "interpolation_offset_time":
66
71
  kwargs.setdefault(
67
72
  "interpolation", InterpolationOption()
@@ -84,6 +89,24 @@ class SourcePathOption:
84
89
  raise ValueError("Either pattern or source_path must be provided")
85
90
 
86
91
  def resolve(self, path: Path) -> Path:
92
+ """
93
+ Resolve the source path or pattern against the given path.
94
+
95
+ Examples:
96
+ >>> from pathlib import Path
97
+ >>> opt = SourcePathOption(source_path=Path("/foo/bar.mp4"))
98
+ >>> opt.resolve(Path("/baz/qux.mp4"))
99
+ PosixPath('/foo/bar.mp4')
100
+
101
+ >>> opt = SourcePathOption(pattern="videos/%g_sub%e")
102
+ >>> opt.resolve(Path("/data/video1.mp4"))
103
+ PosixPath('/data/videos/video1_sub.mp4')
104
+
105
+ >>> opt = SourcePathOption(pattern="/abs/path/%f")
106
+ >>> opt.resolve(Path("/tmp/abc.mov"))
107
+ PosixPath('/abs/path/abc.mov')
108
+ """
109
+
87
110
  if self.source_path is not None:
88
111
  return self.source_path
89
112
 
@@ -139,7 +162,7 @@ SourceOptionSchema = {
139
162
  "type": "integer",
140
163
  },
141
164
  "interpolation_offset_time": {
142
- "type": "float",
165
+ "type": "number",
143
166
  },
144
167
  "interpolation_use_gpx_start_time": {
145
168
  "type": "boolean",
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as T
5
+ import xml.etree.ElementTree as ET
6
+ from pathlib import Path
7
+
8
+ import gpxpy
9
+
10
+ from .. import exiftool_read, geo, utils
11
+
12
+ Track = T.List[geo.Point]
13
+ LOG = logging.getLogger(__name__)
14
+
15
+
16
+ def parse_gpx(gpx_file: Path) -> list[Track]:
17
+ with gpx_file.open("r") as f:
18
+ gpx = gpxpy.parse(f)
19
+
20
+ tracks: list[Track] = []
21
+
22
+ for track in gpx.tracks:
23
+ for segment in track.segments:
24
+ tracks.append([])
25
+ for point in segment.points:
26
+ if point.time is not None:
27
+ tracks[-1].append(
28
+ geo.Point(
29
+ time=geo.as_unix_time(point.time),
30
+ lat=point.latitude,
31
+ lon=point.longitude,
32
+ alt=point.elevation,
33
+ angle=None,
34
+ )
35
+ )
36
+
37
+ return tracks
38
+
39
+
40
+ def index_rdf_description_by_path(
41
+ xml_paths: T.Sequence[Path],
42
+ ) -> dict[str, ET.Element]:
43
+ rdf_description_by_path: dict[str, ET.Element] = {}
44
+
45
+ for xml_path in utils.find_xml_files(xml_paths):
46
+ try:
47
+ etree = ET.parse(xml_path)
48
+ except ET.ParseError as ex:
49
+ verbose = LOG.getEffectiveLevel() <= logging.DEBUG
50
+ if verbose:
51
+ LOG.warning("Failed to parse %s", xml_path, exc_info=True)
52
+ else:
53
+ LOG.warning("Failed to parse %s: %s", xml_path, ex)
54
+ continue
55
+
56
+ rdf_description_by_path.update(
57
+ exiftool_read.index_rdf_description_by_path_from_xml_element(
58
+ etree.getroot()
59
+ )
60
+ )
61
+
62
+ return rdf_description_by_path
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ from pathlib import Path
5
+
6
+ from ... import types
7
+
8
+
9
+ class BaseVideoExtractor(abc.ABC):
10
+ """
11
+ Extracts metadata from a video file.
12
+ """
13
+
14
+ def __init__(self, video_path: Path):
15
+ self.video_path = video_path
16
+
17
+ def extract(self) -> types.VideoMetadata:
18
+ raise NotImplementedError
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing as T
5
+ from pathlib import Path
6
+ from xml.etree import ElementTree as ET
7
+
8
+ if sys.version_info >= (3, 12):
9
+ from typing import override
10
+ else:
11
+ from typing_extensions import override
12
+
13
+ from ... import exceptions, exiftool_read_video, geo, telemetry, types, utils
14
+ from ...gpmf import gpmf_gps_filter
15
+ from .base import BaseVideoExtractor
16
+
17
+
18
+ class VideoExifToolExtractor(BaseVideoExtractor):
19
+ def __init__(self, video_path: Path, element: ET.Element):
20
+ super().__init__(video_path)
21
+ self.element = element
22
+
23
+ @override
24
+ def extract(self) -> types.VideoMetadata:
25
+ exif = exiftool_read_video.ExifToolReadVideo(ET.ElementTree(self.element))
26
+
27
+ make = exif.extract_make()
28
+ model = exif.extract_model()
29
+
30
+ is_gopro = make is not None and make.upper() in ["GOPRO"]
31
+
32
+ points = exif.extract_gps_track()
33
+
34
+ # ExifTool has no idea if GPS is not found or found but empty
35
+ if is_gopro:
36
+ if not points:
37
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
38
+
39
+ # ExifTool (since 13.04) converts GPSSpeed for GoPro to km/h, so here we convert it back to m/s
40
+ for p in points:
41
+ if isinstance(p, telemetry.GPSPoint) and p.ground_speed is not None:
42
+ p.ground_speed = p.ground_speed / 3.6
43
+
44
+ if isinstance(points[0], telemetry.GPSPoint):
45
+ points = T.cast(
46
+ T.List[geo.Point],
47
+ gpmf_gps_filter.remove_noisy_points(
48
+ T.cast(T.List[telemetry.GPSPoint], points)
49
+ ),
50
+ )
51
+ if not points:
52
+ raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
53
+
54
+ if not points:
55
+ raise exceptions.MapillaryVideoGPSNotFoundError(
56
+ "No GPS data found from the video"
57
+ )
58
+
59
+ filetype = types.FileType.GOPRO if is_gopro else types.FileType.VIDEO
60
+
61
+ video_metadata = types.VideoMetadata(
62
+ self.video_path,
63
+ filesize=utils.get_file_size(self.video_path),
64
+ filetype=filetype,
65
+ points=points,
66
+ make=make,
67
+ model=model,
68
+ )
69
+
70
+ return video_metadata