mapillary-tools 0.13.3__py3-none-any.whl → 0.14.0a1__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 (64) hide show
  1. mapillary_tools/__init__.py +1 -1
  2. mapillary_tools/api_v4.py +106 -7
  3. mapillary_tools/authenticate.py +325 -64
  4. mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
  5. mapillary_tools/camm/camm_builder.py +55 -97
  6. mapillary_tools/camm/camm_parser.py +425 -177
  7. mapillary_tools/commands/__main__.py +2 -0
  8. mapillary_tools/commands/authenticate.py +8 -1
  9. mapillary_tools/commands/process.py +27 -51
  10. mapillary_tools/commands/process_and_upload.py +18 -5
  11. mapillary_tools/commands/sample_video.py +2 -3
  12. mapillary_tools/commands/upload.py +18 -9
  13. mapillary_tools/commands/video_process_and_upload.py +19 -5
  14. mapillary_tools/config.py +28 -12
  15. mapillary_tools/constants.py +46 -4
  16. mapillary_tools/exceptions.py +34 -35
  17. mapillary_tools/exif_read.py +158 -53
  18. mapillary_tools/exiftool_read.py +19 -5
  19. mapillary_tools/exiftool_read_video.py +12 -1
  20. mapillary_tools/exiftool_runner.py +77 -0
  21. mapillary_tools/geo.py +148 -107
  22. mapillary_tools/geotag/factory.py +298 -0
  23. mapillary_tools/geotag/geotag_from_generic.py +152 -11
  24. mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
  25. mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
  26. mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
  27. mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
  28. mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
  29. mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
  30. mapillary_tools/geotag/geotag_images_from_video.py +46 -46
  31. mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
  32. mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
  33. mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
  34. mapillary_tools/geotag/options.py +159 -0
  35. mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
  36. mapillary_tools/history.py +3 -11
  37. mapillary_tools/mp4/io_utils.py +0 -1
  38. mapillary_tools/mp4/mp4_sample_parser.py +11 -3
  39. mapillary_tools/mp4/simple_mp4_parser.py +0 -10
  40. mapillary_tools/process_geotag_properties.py +151 -386
  41. mapillary_tools/process_sequence_properties.py +554 -202
  42. mapillary_tools/sample_video.py +8 -15
  43. mapillary_tools/telemetry.py +24 -12
  44. mapillary_tools/types.py +80 -22
  45. mapillary_tools/upload.py +311 -261
  46. mapillary_tools/upload_api_v4.py +55 -95
  47. mapillary_tools/uploader.py +396 -254
  48. mapillary_tools/utils.py +26 -0
  49. mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
  50. mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
  51. mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
  52. mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
  53. mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
  54. mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
  55. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
  56. mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
  57. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
  58. mapillary_tools/geotag/utils.py +0 -26
  59. mapillary_tools-0.13.3.dist-info/RECORD +0 -75
  60. /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
  61. /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
  62. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
  63. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
  64. {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/top_level.txt +0 -0
@@ -1,52 +1,46 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import typing as T
3
5
  import xml.etree.ElementTree as ET
4
- from multiprocessing import Pool
5
6
  from pathlib import Path
6
7
 
7
- from tqdm import tqdm
8
-
9
- from .. import exceptions, exiftool_read, geo, types, utils
8
+ from .. import constants, exceptions, exiftool_read, geo, types, utils
10
9
  from ..exiftool_read_video import ExifToolReadVideo
10
+ from ..exiftool_runner import ExiftoolRunner
11
+ from ..gpmf import gpmf_gps_filter
11
12
  from ..telemetry import GPSPoint
12
- from . import gpmf_gps_filter, utils as video_utils
13
- from .geotag_from_generic import GeotagVideosFromGeneric
13
+ from .geotag_from_generic import GenericVideoExtractor, GeotagVideosFromGeneric
14
14
 
15
15
  LOG = logging.getLogger(__name__)
16
- _DESCRIPTION_TAG = "rdf:Description"
17
16
 
18
17
 
19
- class GeotagVideosFromExifToolVideo(GeotagVideosFromGeneric):
20
- def __init__(
21
- self,
22
- video_paths: T.Sequence[Path],
23
- xml_path: Path,
24
- num_processes: T.Optional[int] = None,
25
- ):
26
- self.video_paths = video_paths
27
- self.xml_path = xml_path
28
- self.num_processes = num_processes
29
- super().__init__()
18
+ class VideoExifToolExtractor(GenericVideoExtractor):
19
+ def __init__(self, video_path: Path, element: ET.Element):
20
+ super().__init__(video_path)
21
+ self.element = element
30
22
 
31
- @staticmethod
32
- def geotag_video(element: ET.Element) -> types.VideoMetadataOrError:
33
- video_path = exiftool_read.find_rdf_description_path(element)
34
- assert video_path is not None, "must find the path from the element"
23
+ def extract(self) -> types.VideoMetadataOrError:
24
+ exif = ExifToolReadVideo(ET.ElementTree(self.element))
35
25
 
36
- try:
37
- exif = ExifToolReadVideo(ET.ElementTree(element))
26
+ make = exif.extract_make()
27
+ model = exif.extract_model()
28
+
29
+ is_gopro = make is not None and make.upper() in ["GOPRO"]
38
30
 
39
- points = exif.extract_gps_track()
31
+ points = exif.extract_gps_track()
40
32
 
33
+ # ExifTool has no idea if GPS is not found or found but empty
34
+ if is_gopro:
41
35
  if not points:
42
- raise exceptions.MapillaryVideoGPSNotFoundError(
43
- "No GPS data found from the video"
44
- )
36
+ raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
45
37
 
46
- points = geo.extend_deduplicate_points(points)
47
- assert points, "must have at least one point"
38
+ # ExifTool (since 13.04) converts GPSSpeed for GoPro to km/h, so here we convert it back to m/s
39
+ for p in points:
40
+ if isinstance(p, GPSPoint) and p.ground_speed is not None:
41
+ p.ground_speed = p.ground_speed / 3.6
48
42
 
49
- if all(isinstance(p, GPSPoint) for p in points):
43
+ if isinstance(points[0], GPSPoint):
50
44
  points = T.cast(
51
45
  T.List[geo.Point],
52
46
  gpmf_gps_filter.remove_noisy_points(
@@ -56,90 +50,102 @@ class GeotagVideosFromExifToolVideo(GeotagVideosFromGeneric):
56
50
  if not points:
57
51
  raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
58
52
 
59
- stationary = video_utils.is_video_stationary(
60
- geo.get_max_distance_from_start([(p.lat, p.lon) for p in points])
53
+ if not points:
54
+ raise exceptions.MapillaryVideoGPSNotFoundError(
55
+ "No GPS data found from the video"
61
56
  )
62
57
 
63
- if stationary:
64
- raise exceptions.MapillaryStationaryVideoError("Stationary video")
65
-
66
- video_metadata = types.VideoMetadata(
67
- video_path,
68
- md5sum=None,
69
- filesize=utils.get_file_size(video_path),
70
- filetype=types.FileType.VIDEO,
71
- points=points,
72
- make=exif.extract_make(),
73
- model=exif.extract_model(),
74
- )
58
+ filetype = types.FileType.GOPRO if is_gopro else types.FileType.VIDEO
75
59
 
76
- LOG.debug("Calculating MD5 checksum for %s", str(video_metadata.filename))
60
+ video_metadata = types.VideoMetadata(
61
+ self.video_path,
62
+ filesize=utils.get_file_size(self.video_path),
63
+ filetype=filetype,
64
+ points=points,
65
+ make=make,
66
+ model=model,
67
+ )
77
68
 
78
- video_metadata.update_md5sum()
69
+ return video_metadata
79
70
 
80
- except Exception as ex:
81
- if not isinstance(ex, exceptions.MapillaryDescriptionError):
82
- LOG.warning(
83
- "Failed to geotag video %s: %s",
84
- video_path,
85
- str(ex),
86
- exc_info=LOG.getEffectiveLevel() <= logging.DEBUG,
87
- )
88
- return types.describe_error_metadata(
89
- ex, video_path, filetype=types.FileType.VIDEO
90
- )
91
71
 
92
- return video_metadata
72
+ class GeotagVideosFromExifToolVideo(GeotagVideosFromGeneric):
73
+ def __init__(
74
+ self,
75
+ video_paths: T.Sequence[Path],
76
+ xml_path: Path,
77
+ num_processes: int | None = None,
78
+ ):
79
+ super().__init__(video_paths, num_processes=num_processes)
80
+ self.xml_path = xml_path
93
81
 
94
- def to_description(self) -> T.List[types.VideoMetadataOrError]:
82
+ def _generate_video_extractors(
83
+ self,
84
+ ) -> T.Sequence[GenericVideoExtractor | types.ErrorMetadata]:
95
85
  rdf_description_by_path = exiftool_read.index_rdf_description_by_path(
96
86
  [self.xml_path]
97
87
  )
98
88
 
99
- error_metadatas: T.List[types.ErrorMetadata] = []
100
- rdf_descriptions: T.List[ET.Element] = []
89
+ results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
90
+
101
91
  for path in self.video_paths:
102
92
  rdf_description = rdf_description_by_path.get(
103
93
  exiftool_read.canonical_path(path)
104
94
  )
105
95
  if rdf_description is None:
106
96
  exc = exceptions.MapillaryEXIFNotFoundError(
107
- f"The {_DESCRIPTION_TAG} XML element for the video not found"
97
+ f"The {exiftool_read._DESCRIPTION_TAG} XML element for the video not found"
108
98
  )
109
- error_metadatas.append(
99
+ results.append(
110
100
  types.describe_error_metadata(
111
101
  exc, path, filetype=types.FileType.VIDEO
112
102
  )
113
103
  )
114
104
  else:
115
- rdf_descriptions.append(rdf_description)
116
-
117
- if self.num_processes is None:
118
- num_processes = self.num_processes
119
- disable_multiprocessing = False
120
- else:
121
- num_processes = max(self.num_processes, 1)
122
- disable_multiprocessing = self.num_processes <= 0
123
-
124
- with Pool(processes=num_processes) as pool:
125
- video_metadatas_iter: T.Iterator[types.VideoMetadataOrError]
126
- if disable_multiprocessing:
127
- video_metadatas_iter = map(
128
- GeotagVideosFromExifToolVideo.geotag_video, rdf_descriptions
129
- )
130
- else:
131
- video_metadatas_iter = pool.imap(
132
- GeotagVideosFromExifToolVideo.geotag_video,
133
- rdf_descriptions,
105
+ results.append(VideoExifToolExtractor(path, rdf_description))
106
+
107
+ return results
108
+
109
+
110
+ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
111
+ def _generate_video_extractors(
112
+ self,
113
+ ) -> T.Sequence[GenericVideoExtractor | types.ErrorMetadata]:
114
+ runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
115
+
116
+ LOG.debug(
117
+ "Extracting XML from %d videos with exiftool command: %s",
118
+ len(self.video_paths),
119
+ " ".join(runner._build_args_read_stdin()),
120
+ )
121
+
122
+ try:
123
+ xml = runner.extract_xml(self.video_paths)
124
+ except FileNotFoundError as ex:
125
+ raise exceptions.MapillaryExiftoolNotFoundError(ex) from ex
126
+
127
+ rdf_description_by_path = (
128
+ exiftool_read.index_rdf_description_by_path_from_xml_element(
129
+ ET.fromstring(xml)
130
+ )
131
+ )
132
+
133
+ results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
134
+
135
+ for path in self.video_paths:
136
+ rdf_description = rdf_description_by_path.get(
137
+ exiftool_read.canonical_path(path)
138
+ )
139
+ if rdf_description is None:
140
+ exc = exceptions.MapillaryEXIFNotFoundError(
141
+ f"The {exiftool_read._DESCRIPTION_TAG} XML element for the video not found"
134
142
  )
135
- video_metadata_or_errors = list(
136
- tqdm(
137
- video_metadatas_iter,
138
- desc="Extracting GPS tracks from ExifTool XML",
139
- unit="videos",
140
- disable=LOG.getEffectiveLevel() <= logging.DEBUG,
141
- total=len(self.video_paths),
143
+ results.append(
144
+ types.describe_error_metadata(
145
+ exc, path, filetype=types.FileType.VIDEO
146
+ )
142
147
  )
143
- )
148
+ else:
149
+ results.append(VideoExifToolExtractor(path, rdf_description))
144
150
 
145
- return error_metadatas + video_metadata_or_errors
151
+ return results
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import datetime
5
+ import logging
6
+
7
+ import typing as T
8
+ from pathlib import Path
9
+
10
+ from .. import geo, telemetry, types
11
+ 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
+
16
+
17
+ LOG = logging.getLogger(__name__)
18
+
19
+
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
+ class GeotagVideosFromGPX(GeotagVideosFromGeneric):
125
+ def __init__(
126
+ self,
127
+ video_paths: T.Sequence[Path],
128
+ option: options.SourcePathOption | None = None,
129
+ num_processes: int | None = None,
130
+ ):
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
+ ]