mapillary-tools 0.14.0a2__py3-none-any.whl → 0.14.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 +66 -262
- mapillary_tools/authenticate.py +54 -46
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/commands/__main__.py +15 -16
- mapillary_tools/commands/upload.py +33 -4
- mapillary_tools/config.py +38 -17
- mapillary_tools/constants.py +127 -43
- mapillary_tools/exceptions.py +4 -0
- mapillary_tools/exif_read.py +2 -1
- mapillary_tools/exif_write.py +3 -1
- mapillary_tools/exiftool_read_video.py +52 -15
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +406 -232
- mapillary_tools/geo.py +16 -0
- mapillary_tools/geotag/__init__.py +0 -0
- mapillary_tools/geotag/base.py +8 -4
- mapillary_tools/geotag/factory.py +106 -89
- mapillary_tools/geotag/geotag_images_from_exiftool.py +27 -20
- mapillary_tools/geotag/geotag_images_from_gpx.py +7 -6
- mapillary_tools/geotag/geotag_images_from_video.py +35 -0
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +61 -14
- mapillary_tools/geotag/geotag_videos_from_gpx.py +22 -9
- mapillary_tools/geotag/options.py +25 -3
- mapillary_tools/geotag/utils.py +9 -12
- mapillary_tools/geotag/video_extractors/base.py +1 -1
- mapillary_tools/geotag/video_extractors/exiftool.py +1 -1
- mapillary_tools/geotag/video_extractors/gpx.py +61 -70
- mapillary_tools/geotag/video_extractors/native.py +34 -31
- mapillary_tools/history.py +128 -8
- mapillary_tools/http.py +211 -0
- mapillary_tools/mp4/construct_mp4_parser.py +8 -2
- mapillary_tools/process_geotag_properties.py +47 -35
- mapillary_tools/process_sequence_properties.py +340 -325
- mapillary_tools/sample_video.py +8 -8
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/types.py +44 -610
- mapillary_tools/upload.py +327 -352
- mapillary_tools/upload_api_v4.py +125 -72
- mapillary_tools/uploader.py +797 -216
- mapillary_tools/utils.py +57 -5
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/METADATA +91 -34
- mapillary_tools-0.14.1.dist-info/RECORD +76 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/WHEEL +1 -1
- mapillary_tools-0.14.0a2.dist-info/RECORD +0 -72
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a2.dist-info → mapillary_tools-0.14.1.dist-info}/top_level.txt +0 -0
|
@@ -13,6 +13,7 @@ else:
|
|
|
13
13
|
|
|
14
14
|
from .. import constants, exceptions, exiftool_read, types
|
|
15
15
|
from ..exiftool_runner import ExiftoolRunner
|
|
16
|
+
from . import options
|
|
16
17
|
from .base import GeotagVideosFromGeneric
|
|
17
18
|
from .utils import index_rdf_description_by_path
|
|
18
19
|
from .video_extractors.exiftool import VideoExifToolExtractor
|
|
@@ -22,18 +23,14 @@ LOG = logging.getLogger(__name__)
|
|
|
22
23
|
|
|
23
24
|
class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
24
25
|
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
xml_path: Path,
|
|
27
|
-
num_processes: int | None = None,
|
|
26
|
+
self, source_path: options.SourcePathOption, num_processes: int | None = None
|
|
28
27
|
):
|
|
29
28
|
super().__init__(num_processes=num_processes)
|
|
30
|
-
self.
|
|
29
|
+
self.source_path = source_path
|
|
31
30
|
|
|
32
31
|
@classmethod
|
|
33
|
-
def
|
|
34
|
-
cls,
|
|
35
|
-
rdf_by_path: dict[str, ET.Element],
|
|
36
|
-
video_paths: T.Iterable[Path],
|
|
32
|
+
def build_video_extractors_from_etree(
|
|
33
|
+
cls, rdf_by_path: dict[str, ET.Element], video_paths: T.Iterable[Path]
|
|
37
34
|
) -> list[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
38
35
|
results: list[VideoExifToolExtractor | types.ErrorMetadata] = []
|
|
39
36
|
|
|
@@ -57,8 +54,49 @@ class GeotagVideosFromExifToolXML(GeotagVideosFromGeneric):
|
|
|
57
54
|
def _generate_video_extractors(
|
|
58
55
|
self, video_paths: T.Sequence[Path]
|
|
59
56
|
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
60
|
-
rdf_by_path =
|
|
61
|
-
return self.
|
|
57
|
+
rdf_by_path = self.find_rdf_by_path(self.source_path, video_paths)
|
|
58
|
+
return self.build_video_extractors_from_etree(rdf_by_path, video_paths)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def find_rdf_by_path(
|
|
62
|
+
cls, option: options.SourcePathOption, paths: T.Iterable[Path]
|
|
63
|
+
) -> dict[str, ET.Element]:
|
|
64
|
+
# Find RDF descriptions by path in RDF description
|
|
65
|
+
# Sources are matched based on the paths in "rdf:about" in XML elements
|
|
66
|
+
# {"source_path": "/path/to/exiftool.xml"}
|
|
67
|
+
# {"source_path": "/path/to/exiftool_xmls/"}
|
|
68
|
+
if option.source_path is not None:
|
|
69
|
+
return index_rdf_description_by_path([option.source_path])
|
|
70
|
+
|
|
71
|
+
# Find RDF descriptions by pattern matching
|
|
72
|
+
# i.e. "video.mp4" matches "/path/to/video.xml" regardless of "rdf:about"
|
|
73
|
+
# {"pattern": "/path/to/%g.xml"}
|
|
74
|
+
if option.pattern is not None:
|
|
75
|
+
rdf_by_path = {}
|
|
76
|
+
for path in paths:
|
|
77
|
+
canonical_path = exiftool_read.canonical_path(path)
|
|
78
|
+
|
|
79
|
+
# Skip non-existent resolved source paths to avoid verbose warnings
|
|
80
|
+
resolved_source_path = option.resolve(path)
|
|
81
|
+
if not resolved_source_path.exists():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
rdf_by_about = index_rdf_description_by_path([resolved_source_path])
|
|
85
|
+
if not rdf_by_about:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
rdf = rdf_by_about.get(canonical_path)
|
|
89
|
+
if rdf is None:
|
|
90
|
+
about, rdf = list(rdf_by_about.items())[0]
|
|
91
|
+
if len(rdf_by_about) > 1:
|
|
92
|
+
LOG.warning(
|
|
93
|
+
f"Found {len(rdf_by_about)} RDFs in the XML source {resolved_source_path}. Using the first RDF (with rdf:about={about}) for {path}"
|
|
94
|
+
)
|
|
95
|
+
rdf_by_path[canonical_path] = rdf
|
|
96
|
+
|
|
97
|
+
return rdf_by_path
|
|
98
|
+
|
|
99
|
+
raise AssertionError("Either source_path or pattern must be provided")
|
|
62
100
|
|
|
63
101
|
|
|
64
102
|
class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
@@ -66,7 +104,10 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
66
104
|
def _generate_video_extractors(
|
|
67
105
|
self, video_paths: T.Sequence[Path]
|
|
68
106
|
) -> T.Sequence[VideoExifToolExtractor | types.ErrorMetadata]:
|
|
69
|
-
|
|
107
|
+
if constants.EXIFTOOL_PATH is None:
|
|
108
|
+
runner = ExiftoolRunner()
|
|
109
|
+
else:
|
|
110
|
+
runner = ExiftoolRunner(constants.EXIFTOOL_PATH)
|
|
70
111
|
|
|
71
112
|
LOG.debug(
|
|
72
113
|
"Extracting XML from %d videos with ExifTool command: %s",
|
|
@@ -76,7 +117,13 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
76
117
|
try:
|
|
77
118
|
xml = runner.extract_xml(video_paths)
|
|
78
119
|
except FileNotFoundError as ex:
|
|
79
|
-
|
|
120
|
+
exiftool_ex = exceptions.MapillaryExiftoolNotFoundError(ex)
|
|
121
|
+
return [
|
|
122
|
+
types.describe_error_metadata(
|
|
123
|
+
exiftool_ex, video_path, filetype=types.FileType.VIDEO
|
|
124
|
+
)
|
|
125
|
+
for video_path in video_paths
|
|
126
|
+
]
|
|
80
127
|
|
|
81
128
|
try:
|
|
82
129
|
xml_element = ET.fromstring(xml)
|
|
@@ -84,7 +131,7 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
84
131
|
LOG.warning(
|
|
85
132
|
"Failed to parse ExifTool XML: %s",
|
|
86
133
|
str(ex),
|
|
87
|
-
exc_info=LOG.
|
|
134
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
88
135
|
)
|
|
89
136
|
rdf_by_path = {}
|
|
90
137
|
else:
|
|
@@ -92,6 +139,6 @@ class GeotagVideosFromExifToolRunner(GeotagVideosFromGeneric):
|
|
|
92
139
|
xml_element
|
|
93
140
|
)
|
|
94
141
|
|
|
95
|
-
return GeotagVideosFromExifToolXML.
|
|
142
|
+
return GeotagVideosFromExifToolXML.build_video_extractors_from_etree(
|
|
96
143
|
rdf_by_path, video_paths
|
|
97
144
|
)
|
|
@@ -10,6 +10,7 @@ if sys.version_info >= (3, 12):
|
|
|
10
10
|
else:
|
|
11
11
|
from typing_extensions import override
|
|
12
12
|
|
|
13
|
+
from .. import exceptions, types
|
|
13
14
|
from . import options
|
|
14
15
|
from .base import GeotagVideosFromGeneric
|
|
15
16
|
from .video_extractors.gpx import GPXVideoExtractor
|
|
@@ -21,19 +22,31 @@ LOG = logging.getLogger(__name__)
|
|
|
21
22
|
class GeotagVideosFromGPX(GeotagVideosFromGeneric):
|
|
22
23
|
def __init__(
|
|
23
24
|
self,
|
|
24
|
-
|
|
25
|
+
source_path: options.SourcePathOption | None = None,
|
|
25
26
|
num_processes: int | None = None,
|
|
26
27
|
):
|
|
27
28
|
super().__init__(num_processes=num_processes)
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
self.
|
|
29
|
+
if source_path is None:
|
|
30
|
+
source_path = options.SourcePathOption(pattern="%g.gpx")
|
|
31
|
+
self.source_path = source_path
|
|
31
32
|
|
|
32
33
|
@override
|
|
33
34
|
def _generate_video_extractors(
|
|
34
35
|
self, video_paths: T.Sequence[Path]
|
|
35
|
-
) -> T.Sequence[GPXVideoExtractor]:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
@@ -60,9 +60,13 @@ class SourceOption:
|
|
|
60
60
|
elif k == "filetypes":
|
|
61
61
|
kwargs[k] = {types.FileType(t) for t in v}
|
|
62
62
|
elif k == "source_path":
|
|
63
|
-
kwargs.setdefault(
|
|
63
|
+
kwargs.setdefault(
|
|
64
|
+
"source_path", SourcePathOption(source_path=Path(v))
|
|
65
|
+
).sourthe_path = Path(v)
|
|
64
66
|
elif k == "pattern":
|
|
65
|
-
kwargs.setdefault(
|
|
67
|
+
kwargs.setdefault(
|
|
68
|
+
"source_path", SourcePathOption(pattern=v)
|
|
69
|
+
).pattern = v
|
|
66
70
|
elif k == "interpolation_offset_time":
|
|
67
71
|
kwargs.setdefault(
|
|
68
72
|
"interpolation", InterpolationOption()
|
|
@@ -85,6 +89,24 @@ class SourcePathOption:
|
|
|
85
89
|
raise ValueError("Either pattern or source_path must be provided")
|
|
86
90
|
|
|
87
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
|
+
|
|
88
110
|
if self.source_path is not None:
|
|
89
111
|
return self.source_path
|
|
90
112
|
|
|
@@ -140,7 +162,7 @@ SourceOptionSchema = {
|
|
|
140
162
|
"type": "integer",
|
|
141
163
|
},
|
|
142
164
|
"interpolation_offset_time": {
|
|
143
|
-
"type": "
|
|
165
|
+
"type": "number",
|
|
144
166
|
},
|
|
145
167
|
"interpolation_use_gpx_start_time": {
|
|
146
168
|
"type": "boolean",
|
mapillary_tools/geotag/utils.py
CHANGED
|
@@ -37,26 +37,23 @@ def parse_gpx(gpx_file: Path) -> list[Track]:
|
|
|
37
37
|
return tracks
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def index_rdf_description_by_path(
|
|
41
|
-
|
|
42
|
-
) -> dict[str, ET.Element]:
|
|
43
|
-
rdf_description_by_path: dict[str, ET.Element] = {}
|
|
40
|
+
def index_rdf_description_by_path(xml_paths: T.Sequence[Path]) -> dict[str, ET.Element]:
|
|
41
|
+
rdf_by_path: dict[str, ET.Element] = {}
|
|
44
42
|
|
|
45
43
|
for xml_path in utils.find_xml_files(xml_paths):
|
|
46
44
|
try:
|
|
47
45
|
etree = ET.parse(xml_path)
|
|
48
|
-
except
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
LOG.
|
|
52
|
-
|
|
53
|
-
LOG.warning("Failed to parse %s: %s", xml_path, ex)
|
|
46
|
+
except Exception as ex:
|
|
47
|
+
LOG.warning(
|
|
48
|
+
f"Failed to parse {xml_path}: {ex}",
|
|
49
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
50
|
+
)
|
|
54
51
|
continue
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
rdf_by_path.update(
|
|
57
54
|
exiftool_read.index_rdf_description_by_path_from_xml_element(
|
|
58
55
|
etree.getroot()
|
|
59
56
|
)
|
|
60
57
|
)
|
|
61
58
|
|
|
62
|
-
return
|
|
59
|
+
return rdf_by_path
|
|
@@ -21,7 +21,7 @@ class VideoExifToolExtractor(BaseVideoExtractor):
|
|
|
21
21
|
self.element = element
|
|
22
22
|
|
|
23
23
|
@override
|
|
24
|
-
def extract(self) -> types.
|
|
24
|
+
def extract(self) -> types.VideoMetadata:
|
|
25
25
|
exif = exiftool_read_video.ExifToolReadVideo(ET.ElementTree(self.element))
|
|
26
26
|
|
|
27
27
|
make = exif.extract_make()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
-
import
|
|
4
|
+
import enum
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
7
|
import typing as T
|
|
@@ -12,7 +12,7 @@ if sys.version_info >= (3, 12):
|
|
|
12
12
|
else:
|
|
13
13
|
from typing_extensions import override
|
|
14
14
|
|
|
15
|
-
from ... import geo, telemetry, types
|
|
15
|
+
from ... import exceptions, geo, telemetry, types, utils
|
|
16
16
|
from ..utils import parse_gpx
|
|
17
17
|
from .base import BaseVideoExtractor
|
|
18
18
|
from .native import NativeVideoExtractor
|
|
@@ -21,106 +21,97 @@ from .native import NativeVideoExtractor
|
|
|
21
21
|
LOG = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class SyncMode(enum.Enum):
|
|
25
|
+
# Sync by video GPS timestamps if found, otherwise rebase
|
|
26
|
+
SYNC = "sync"
|
|
27
|
+
# Sync by video GPS timestamps, and throw if not found
|
|
28
|
+
STRICT_SYNC = "strict_sync"
|
|
29
|
+
# Rebase all GPX timestamps to start from 0
|
|
30
|
+
REBASE = "rebase"
|
|
31
|
+
|
|
32
|
+
|
|
24
33
|
class GPXVideoExtractor(BaseVideoExtractor):
|
|
25
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self, video_path: Path, gpx_path: Path, sync_mode: SyncMode = SyncMode.SYNC
|
|
36
|
+
):
|
|
26
37
|
self.video_path = video_path
|
|
27
38
|
self.gpx_path = gpx_path
|
|
39
|
+
self.sync_mode = sync_mode
|
|
28
40
|
|
|
29
41
|
@override
|
|
30
|
-
def extract(self) -> types.
|
|
31
|
-
|
|
32
|
-
gpx_tracks = parse_gpx(self.gpx_path)
|
|
33
|
-
except Exception as ex:
|
|
34
|
-
raise RuntimeError(
|
|
35
|
-
f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
|
|
36
|
-
)
|
|
42
|
+
def extract(self) -> types.VideoMetadata:
|
|
43
|
+
gpx_tracks = parse_gpx(self.gpx_path)
|
|
37
44
|
|
|
38
45
|
if 1 < len(gpx_tracks):
|
|
39
46
|
LOG.warning(
|
|
40
|
-
"Found
|
|
41
|
-
len(gpx_tracks),
|
|
42
|
-
self.gpx_path,
|
|
47
|
+
f"Found {len(gpx_tracks)} tracks in the GPX file {self.gpx_path}. Will merge points in all the tracks as a single track for interpolation"
|
|
43
48
|
)
|
|
44
49
|
|
|
45
50
|
gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
|
|
46
51
|
|
|
47
52
|
native_extractor = NativeVideoExtractor(self.video_path)
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
try:
|
|
55
|
+
native_video_metadata = native_extractor.extract()
|
|
56
|
+
except exceptions.MapillaryVideoGPSNotFoundError as ex:
|
|
57
|
+
if self.sync_mode is SyncMode.STRICT_SYNC:
|
|
58
|
+
raise ex
|
|
52
59
|
self._rebase_times(gpx_points)
|
|
53
60
|
return types.VideoMetadata(
|
|
54
|
-
filename=
|
|
55
|
-
|
|
61
|
+
filename=self.video_path,
|
|
62
|
+
filesize=utils.get_file_size(self.video_path),
|
|
63
|
+
filetype=types.FileType.VIDEO,
|
|
56
64
|
points=gpx_points,
|
|
57
65
|
)
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
gpx_points,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
self._rebase_times(gpx_points, offset=offset)
|
|
67
|
+
if self.sync_mode is SyncMode.REBASE:
|
|
68
|
+
self._rebase_times(gpx_points)
|
|
69
|
+
else:
|
|
70
|
+
offset = self._gpx_offset(gpx_points, native_video_metadata.points)
|
|
71
|
+
self._rebase_times(gpx_points, offset=offset)
|
|
66
72
|
|
|
67
|
-
return dataclasses.replace(
|
|
73
|
+
return dataclasses.replace(native_video_metadata, points=gpx_points)
|
|
68
74
|
|
|
69
|
-
@
|
|
70
|
-
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
75
|
+
@classmethod
|
|
76
|
+
def _rebase_times(cls, points: T.Sequence[geo.Point], offset: float = 0.0) -> None:
|
|
71
77
|
"""
|
|
72
|
-
|
|
78
|
+
Rebase point times to start from **offset**
|
|
73
79
|
"""
|
|
74
80
|
if points:
|
|
75
81
|
first_timestamp = points[0].time
|
|
76
82
|
for p in points:
|
|
77
83
|
p.time = (p.time - first_timestamp) + offset
|
|
78
|
-
return points
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
@classmethod
|
|
86
|
+
def _gpx_offset(
|
|
87
|
+
cls, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
|
|
82
88
|
) -> float:
|
|
89
|
+
"""
|
|
90
|
+
Calculate the offset that needs to be applied to the GPX points to sync with the video GPS points.
|
|
91
|
+
|
|
92
|
+
>>> gpx_points = [geo.Point(time=5, lat=1, lon=1, alt=None, angle=None)]
|
|
93
|
+
>>> GPXVideoExtractor._gpx_offset(gpx_points, gpx_points)
|
|
94
|
+
0.0
|
|
95
|
+
>>> GPXVideoExtractor._gpx_offset(gpx_points, [])
|
|
96
|
+
0.0
|
|
97
|
+
>>> GPXVideoExtractor._gpx_offset([], gpx_points)
|
|
98
|
+
0.0
|
|
99
|
+
"""
|
|
83
100
|
offset: float = 0.0
|
|
84
101
|
|
|
85
|
-
if not gpx_points:
|
|
86
|
-
return offset
|
|
87
|
-
|
|
88
|
-
first_gpx_dt = datetime.datetime.fromtimestamp(
|
|
89
|
-
gpx_points[0].time, tz=datetime.timezone.utc
|
|
90
|
-
)
|
|
91
|
-
LOG.info("First GPX timestamp: %s", first_gpx_dt)
|
|
92
|
-
|
|
93
|
-
if not video_gps_points:
|
|
94
|
-
LOG.warning(
|
|
95
|
-
"Skip GPX synchronization because no GPS found in video %s",
|
|
96
|
-
self.video_path,
|
|
97
|
-
)
|
|
102
|
+
if not gpx_points or not video_gps_points:
|
|
98
103
|
return offset
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
first_gpx_dt,
|
|
112
|
-
first_gps_dt,
|
|
113
|
-
offset,
|
|
114
|
-
)
|
|
115
|
-
else:
|
|
116
|
-
LOG.info(
|
|
117
|
-
"GPX and GPS are perfectly synchronized (all starts from %s)",
|
|
118
|
-
first_gpx_dt,
|
|
119
|
-
)
|
|
120
|
-
else:
|
|
121
|
-
LOG.warning(
|
|
122
|
-
"Skip GPX synchronization because no GPS epoch time found in video %s",
|
|
123
|
-
self.video_path,
|
|
124
|
-
)
|
|
105
|
+
gps_epoch_time: float | None = None
|
|
106
|
+
gps_point = video_gps_points[0]
|
|
107
|
+
if isinstance(gps_point, telemetry.GPSPoint):
|
|
108
|
+
if gps_point.epoch_time is not None:
|
|
109
|
+
gps_epoch_time = gps_point.epoch_time
|
|
110
|
+
elif isinstance(gps_point, telemetry.CAMMGPSPoint):
|
|
111
|
+
if gps_point.time_gps_epoch is not None:
|
|
112
|
+
gps_epoch_time = gps_point.time_gps_epoch
|
|
113
|
+
|
|
114
|
+
if gps_epoch_time is not None:
|
|
115
|
+
offset = gpx_points[0].time - gps_epoch_time
|
|
125
116
|
|
|
126
117
|
return offset
|
|
@@ -12,12 +12,13 @@ else:
|
|
|
12
12
|
from ... import blackvue_parser, exceptions, geo, telemetry, types, utils
|
|
13
13
|
from ...camm import camm_parser
|
|
14
14
|
from ...gpmf import gpmf_gps_filter, gpmf_parser
|
|
15
|
+
from ...mp4 import construct_mp4_parser, simple_mp4_parser
|
|
15
16
|
from .base import BaseVideoExtractor
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class GoProVideoExtractor(BaseVideoExtractor):
|
|
19
20
|
@override
|
|
20
|
-
def extract(self) -> types.
|
|
21
|
+
def extract(self) -> types.VideoMetadata:
|
|
21
22
|
with self.video_path.open("rb") as fp:
|
|
22
23
|
gopro_info = gpmf_parser.extract_gopro_info(fp)
|
|
23
24
|
|
|
@@ -29,23 +30,13 @@ class GoProVideoExtractor(BaseVideoExtractor):
|
|
|
29
30
|
gps_points = gopro_info.gps
|
|
30
31
|
assert gps_points is not None, "must have GPS data extracted"
|
|
31
32
|
if not gps_points:
|
|
32
|
-
|
|
33
|
-
ex: exceptions.MapillaryDescriptionError = (
|
|
34
|
-
exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
35
|
-
)
|
|
36
|
-
return types.describe_error_metadata(
|
|
37
|
-
ex, self.video_path, filetype=types.FileType.GOPRO
|
|
38
|
-
)
|
|
33
|
+
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
39
34
|
|
|
40
35
|
gps_points = T.cast(
|
|
41
36
|
T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
|
|
42
37
|
)
|
|
43
38
|
if not gps_points:
|
|
44
|
-
|
|
45
|
-
ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
46
|
-
return types.describe_error_metadata(
|
|
47
|
-
ex, self.video_path, filetype=types.FileType.GOPRO
|
|
48
|
-
)
|
|
39
|
+
raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
49
40
|
|
|
50
41
|
video_metadata = types.VideoMetadata(
|
|
51
42
|
filename=self.video_path,
|
|
@@ -61,7 +52,7 @@ class GoProVideoExtractor(BaseVideoExtractor):
|
|
|
61
52
|
|
|
62
53
|
class CAMMVideoExtractor(BaseVideoExtractor):
|
|
63
54
|
@override
|
|
64
|
-
def extract(self) -> types.
|
|
55
|
+
def extract(self) -> types.VideoMetadata:
|
|
65
56
|
with self.video_path.open("rb") as fp:
|
|
66
57
|
camm_info = camm_parser.extract_camm_info(fp)
|
|
67
58
|
|
|
@@ -71,13 +62,7 @@ class CAMMVideoExtractor(BaseVideoExtractor):
|
|
|
71
62
|
)
|
|
72
63
|
|
|
73
64
|
if not camm_info.gps and not camm_info.mini_gps:
|
|
74
|
-
|
|
75
|
-
ex: exceptions.MapillaryDescriptionError = (
|
|
76
|
-
exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
77
|
-
)
|
|
78
|
-
return types.describe_error_metadata(
|
|
79
|
-
ex, self.video_path, filetype=types.FileType.CAMM
|
|
80
|
-
)
|
|
65
|
+
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
81
66
|
|
|
82
67
|
return types.VideoMetadata(
|
|
83
68
|
filename=self.video_path,
|
|
@@ -91,7 +76,7 @@ class CAMMVideoExtractor(BaseVideoExtractor):
|
|
|
91
76
|
|
|
92
77
|
class BlackVueVideoExtractor(BaseVideoExtractor):
|
|
93
78
|
@override
|
|
94
|
-
def extract(self) -> types.
|
|
79
|
+
def extract(self) -> types.VideoMetadata:
|
|
95
80
|
with self.video_path.open("rb") as fp:
|
|
96
81
|
blackvue_info = blackvue_parser.extract_blackvue_info(fp)
|
|
97
82
|
|
|
@@ -101,19 +86,13 @@ class BlackVueVideoExtractor(BaseVideoExtractor):
|
|
|
101
86
|
)
|
|
102
87
|
|
|
103
88
|
if not blackvue_info.gps:
|
|
104
|
-
|
|
105
|
-
ex: exceptions.MapillaryDescriptionError = (
|
|
106
|
-
exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
107
|
-
)
|
|
108
|
-
return types.describe_error_metadata(
|
|
109
|
-
ex, self.video_path, filetype=types.FileType.BLACKVUE
|
|
110
|
-
)
|
|
89
|
+
raise exceptions.MapillaryGPXEmptyError("Empty GPS data found")
|
|
111
90
|
|
|
112
91
|
video_metadata = types.VideoMetadata(
|
|
113
92
|
filename=self.video_path,
|
|
114
93
|
filesize=utils.get_file_size(self.video_path),
|
|
115
94
|
filetype=types.FileType.BLACKVUE,
|
|
116
|
-
points=blackvue_info.gps
|
|
95
|
+
points=blackvue_info.gps,
|
|
117
96
|
make=blackvue_info.make,
|
|
118
97
|
model=blackvue_info.model,
|
|
119
98
|
)
|
|
@@ -127,7 +106,7 @@ class NativeVideoExtractor(BaseVideoExtractor):
|
|
|
127
106
|
self.filetypes = filetypes
|
|
128
107
|
|
|
129
108
|
@override
|
|
130
|
-
def extract(self) -> types.
|
|
109
|
+
def extract(self) -> types.VideoMetadata:
|
|
131
110
|
ft = self.filetypes
|
|
132
111
|
extractor: BaseVideoExtractor
|
|
133
112
|
|
|
@@ -135,6 +114,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
|
|
|
135
114
|
extractor = GoProVideoExtractor(self.video_path)
|
|
136
115
|
try:
|
|
137
116
|
return extractor.extract()
|
|
117
|
+
except simple_mp4_parser.BoxNotFoundError as ex:
|
|
118
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
119
|
+
f"Invalid video: {ex}"
|
|
120
|
+
) from ex
|
|
121
|
+
except construct_mp4_parser.BoxNotFoundError as ex:
|
|
122
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
123
|
+
f"Invalid video: {ex}"
|
|
124
|
+
) from ex
|
|
138
125
|
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
139
126
|
pass
|
|
140
127
|
|
|
@@ -142,6 +129,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
|
|
|
142
129
|
extractor = CAMMVideoExtractor(self.video_path)
|
|
143
130
|
try:
|
|
144
131
|
return extractor.extract()
|
|
132
|
+
except simple_mp4_parser.BoxNotFoundError as ex:
|
|
133
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
134
|
+
f"Invalid video: {ex}"
|
|
135
|
+
) from ex
|
|
136
|
+
except construct_mp4_parser.BoxNotFoundError as ex:
|
|
137
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
138
|
+
f"Invalid video: {ex}"
|
|
139
|
+
) from ex
|
|
145
140
|
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
146
141
|
pass
|
|
147
142
|
|
|
@@ -149,6 +144,14 @@ class NativeVideoExtractor(BaseVideoExtractor):
|
|
|
149
144
|
extractor = BlackVueVideoExtractor(self.video_path)
|
|
150
145
|
try:
|
|
151
146
|
return extractor.extract()
|
|
147
|
+
except simple_mp4_parser.BoxNotFoundError as ex:
|
|
148
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
149
|
+
f"Invalid video: {ex}"
|
|
150
|
+
) from ex
|
|
151
|
+
except construct_mp4_parser.BoxNotFoundError as ex:
|
|
152
|
+
raise exceptions.MapillaryInvalidVideoError(
|
|
153
|
+
f"Invalid video: {ex}"
|
|
154
|
+
) from ex
|
|
152
155
|
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
153
156
|
pass
|
|
154
157
|
|