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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +106 -7
- mapillary_tools/authenticate.py +325 -64
- mapillary_tools/{geotag/blackvue_parser.py → blackvue_parser.py} +74 -54
- mapillary_tools/camm/camm_builder.py +55 -97
- mapillary_tools/camm/camm_parser.py +425 -177
- mapillary_tools/commands/__main__.py +2 -0
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +18 -5
- mapillary_tools/commands/sample_video.py +2 -3
- mapillary_tools/commands/upload.py +18 -9
- mapillary_tools/commands/video_process_and_upload.py +19 -5
- mapillary_tools/config.py +28 -12
- mapillary_tools/constants.py +46 -4
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +158 -53
- mapillary_tools/exiftool_read.py +19 -5
- mapillary_tools/exiftool_read_video.py +12 -1
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/geo.py +148 -107
- mapillary_tools/geotag/factory.py +298 -0
- mapillary_tools/geotag/geotag_from_generic.py +152 -11
- mapillary_tools/geotag/geotag_images_from_exif.py +43 -124
- mapillary_tools/geotag/geotag_images_from_exiftool.py +66 -70
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +32 -48
- mapillary_tools/geotag/geotag_images_from_gpx.py +41 -116
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +15 -96
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -2
- mapillary_tools/geotag/geotag_images_from_video.py +46 -46
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +98 -92
- mapillary_tools/geotag/geotag_videos_from_gpx.py +140 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +149 -181
- mapillary_tools/geotag/options.py +159 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +194 -171
- mapillary_tools/history.py +3 -11
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +11 -3
- mapillary_tools/mp4/simple_mp4_parser.py +0 -10
- mapillary_tools/process_geotag_properties.py +151 -386
- mapillary_tools/process_sequence_properties.py +554 -202
- mapillary_tools/sample_video.py +8 -15
- mapillary_tools/telemetry.py +24 -12
- mapillary_tools/types.py +80 -22
- mapillary_tools/upload.py +311 -261
- mapillary_tools/upload_api_v4.py +55 -95
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +26 -0
- mapillary_tools/video_data_extraction/extract_video_data.py +17 -36
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +34 -19
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +41 -17
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -1
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +1 -2
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +37 -22
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a1.dist-info/RECORD +78 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/utils.py +0 -26
- mapillary_tools-0.13.3.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- /mapillary_tools/{geotag → gpmf}/gps_filter.py +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3.dist-info → mapillary_tools-0.14.0a1.dist-info/licenses}/LICENSE +0 -0
- {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
|
|
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
|
|
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
|
|
20
|
-
def __init__(
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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.
|
|
43
|
-
"No GPS data found from the video"
|
|
44
|
-
)
|
|
36
|
+
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
45
37
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
53
|
+
if not points:
|
|
54
|
+
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
55
|
+
"No GPS data found from the video"
|
|
61
56
|
)
|
|
62
57
|
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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
|
+
]
|