mapillary-tools 0.13.3a1__py3-none-any.whl → 0.14.0a2__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 +237 -16
- 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 +429 -181
- mapillary_tools/commands/__main__.py +12 -6
- mapillary_tools/commands/authenticate.py +8 -1
- mapillary_tools/commands/process.py +27 -51
- mapillary_tools/commands/process_and_upload.py +19 -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 +31 -13
- mapillary_tools/constants.py +47 -6
- mapillary_tools/exceptions.py +34 -35
- mapillary_tools/exif_read.py +221 -116
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +33 -42
- mapillary_tools/exiftool_read_video.py +46 -33
- mapillary_tools/exiftool_runner.py +77 -0
- mapillary_tools/ffmpeg.py +24 -23
- mapillary_tools/geo.py +144 -120
- mapillary_tools/geotag/base.py +147 -0
- mapillary_tools/geotag/factory.py +291 -0
- mapillary_tools/geotag/geotag_images_from_exif.py +14 -131
- mapillary_tools/geotag/geotag_images_from_exiftool.py +126 -82
- mapillary_tools/geotag/geotag_images_from_gpx.py +53 -118
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +13 -126
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +4 -5
- mapillary_tools/geotag/geotag_images_from_video.py +53 -51
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +97 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +39 -0
- mapillary_tools/geotag/geotag_videos_from_video.py +20 -185
- mapillary_tools/geotag/image_extractors/base.py +18 -0
- mapillary_tools/geotag/image_extractors/exif.py +60 -0
- mapillary_tools/geotag/image_extractors/exiftool.py +18 -0
- mapillary_tools/geotag/options.py +160 -0
- mapillary_tools/geotag/utils.py +52 -16
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py} +57 -39
- mapillary_tools/geotag/video_extractors/native.py +157 -0
- mapillary_tools/{geotag → gpmf}/gpmf_parser.py +205 -182
- mapillary_tools/{geotag → gpmf}/gps_filter.py +5 -3
- mapillary_tools/history.py +7 -13
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/io_utils.py +0 -1
- mapillary_tools/mp4/mp4_sample_parser.py +36 -28
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -22
- mapillary_tools/process_geotag_properties.py +155 -392
- mapillary_tools/process_sequence_properties.py +562 -208
- mapillary_tools/sample_video.py +13 -20
- mapillary_tools/telemetry.py +26 -13
- mapillary_tools/types.py +111 -58
- mapillary_tools/upload.py +316 -298
- mapillary_tools/upload_api_v4.py +55 -122
- mapillary_tools/uploader.py +396 -254
- mapillary_tools/utils.py +42 -18
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/METADATA +3 -2
- mapillary_tools-0.14.0a2.dist-info/RECORD +72 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/geotag_from_generic.py +0 -22
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -93
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -145
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -176
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -34
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -38
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -71
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -53
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -43
- mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -24
- mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -39
- mapillary_tools-0.13.3a1.dist-info/RECORD +0 -75
- /mapillary_tools/{geotag → gpmf}/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info/licenses}/LICENSE +0 -0
- {mapillary_tools-0.13.3a1.dist-info → mapillary_tools-0.14.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
import json
|
|
6
|
+
import typing as T
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import jsonschema
|
|
10
|
+
|
|
11
|
+
from .. import types
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SourceType(enum.Enum):
|
|
15
|
+
NATIVE = "native"
|
|
16
|
+
GPX = "gpx"
|
|
17
|
+
NMEA = "nmea"
|
|
18
|
+
EXIFTOOL_XML = "exiftool_xml"
|
|
19
|
+
EXIFTOOL_RUNTIME = "exiftool_runtime"
|
|
20
|
+
|
|
21
|
+
# Legacy source types for images
|
|
22
|
+
GOPRO = "gopro"
|
|
23
|
+
BLACKVUE = "blackvue"
|
|
24
|
+
CAMM = "camm"
|
|
25
|
+
EXIF = "exif"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SOURCE_TYPE_ALIAS: dict[str, SourceType] = {
|
|
29
|
+
"blackvue_videos": SourceType.BLACKVUE,
|
|
30
|
+
"gopro_videos": SourceType.GOPRO,
|
|
31
|
+
"exiftool": SourceType.EXIFTOOL_RUNTIME,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class SourceOption:
|
|
37
|
+
# Type of the source
|
|
38
|
+
source: SourceType
|
|
39
|
+
|
|
40
|
+
# Filter by these filetypes
|
|
41
|
+
filetypes: set[types.FileType] | None = None
|
|
42
|
+
|
|
43
|
+
num_processes: int | None = None
|
|
44
|
+
|
|
45
|
+
source_path: SourcePathOption | None = None
|
|
46
|
+
|
|
47
|
+
interpolation: InterpolationOption | None = None
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_dict(cls, data: dict[str, T.Any]) -> SourceOption:
|
|
51
|
+
validate_option(data)
|
|
52
|
+
|
|
53
|
+
kwargs: dict[str, T.Any] = {}
|
|
54
|
+
for k, v in data.items():
|
|
55
|
+
# None values are considered as absent and should be ignored
|
|
56
|
+
if v is None:
|
|
57
|
+
continue
|
|
58
|
+
if k == "source":
|
|
59
|
+
kwargs[k] = SourceType(SOURCE_TYPE_ALIAS.get(v, v))
|
|
60
|
+
elif k == "filetypes":
|
|
61
|
+
kwargs[k] = {types.FileType(t) for t in v}
|
|
62
|
+
elif k == "source_path":
|
|
63
|
+
kwargs.setdefault("source_path", SourcePathOption()).source_path = v
|
|
64
|
+
elif k == "pattern":
|
|
65
|
+
kwargs.setdefault("source_path", SourcePathOption()).pattern = v
|
|
66
|
+
elif k == "interpolation_offset_time":
|
|
67
|
+
kwargs.setdefault(
|
|
68
|
+
"interpolation", InterpolationOption()
|
|
69
|
+
).offset_time = v
|
|
70
|
+
elif k == "interpolation_use_gpx_start_time":
|
|
71
|
+
kwargs.setdefault(
|
|
72
|
+
"interpolation", InterpolationOption()
|
|
73
|
+
).use_gpx_start_time = v
|
|
74
|
+
|
|
75
|
+
return cls(**kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclasses.dataclass
|
|
79
|
+
class SourcePathOption:
|
|
80
|
+
pattern: str | None = None
|
|
81
|
+
source_path: Path | None = None
|
|
82
|
+
|
|
83
|
+
def __post_init__(self):
|
|
84
|
+
if self.source_path is None and self.pattern is None:
|
|
85
|
+
raise ValueError("Either pattern or source_path must be provided")
|
|
86
|
+
|
|
87
|
+
def resolve(self, path: Path) -> Path:
|
|
88
|
+
if self.source_path is not None:
|
|
89
|
+
return self.source_path
|
|
90
|
+
|
|
91
|
+
assert self.pattern is not None, (
|
|
92
|
+
"either pattern or source_path must be provided"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# %f: the full video filename (foo.mp4)
|
|
96
|
+
# %g: the video filename without extension (foo)
|
|
97
|
+
# %e: the video filename extension (.mp4)
|
|
98
|
+
replaced = Path(
|
|
99
|
+
self.pattern.replace("%f", path.name)
|
|
100
|
+
.replace("%g", path.stem)
|
|
101
|
+
.replace("%e", path.suffix)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
abs_path = (
|
|
105
|
+
replaced
|
|
106
|
+
if replaced.is_absolute()
|
|
107
|
+
else Path.joinpath(path.parent.resolve(), replaced)
|
|
108
|
+
).resolve()
|
|
109
|
+
|
|
110
|
+
return abs_path
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass
|
|
114
|
+
class InterpolationOption:
|
|
115
|
+
offset_time: float = 0.0
|
|
116
|
+
use_gpx_start_time: bool = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
SourceOptionSchema = {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"source": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"enum": [s.value for s in SourceType] + list(SOURCE_TYPE_ALIAS.keys()),
|
|
125
|
+
},
|
|
126
|
+
"filetypes": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"enum": [t.value for t in types.FileType],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"source_path": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
},
|
|
136
|
+
"pattern": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
},
|
|
139
|
+
"num_processes": {
|
|
140
|
+
"type": "integer",
|
|
141
|
+
},
|
|
142
|
+
"interpolation_offset_time": {
|
|
143
|
+
"type": "float",
|
|
144
|
+
},
|
|
145
|
+
"interpolation_use_gpx_start_time": {
|
|
146
|
+
"type": "boolean",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
"required": ["source"],
|
|
150
|
+
"additionalProperties": False,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def validate_option(instance):
|
|
155
|
+
jsonschema.validate(instance=instance, schema=SourceOptionSchema)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
# python -m mapillary_tools.geotag.options > schema/geotag_source_option.json
|
|
160
|
+
print(json.dumps(SourceOptionSchema, indent=4))
|
mapillary_tools/geotag/utils.py
CHANGED
|
@@ -1,26 +1,62 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
2
4
|
import typing as T
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
from pathlib import Path
|
|
3
7
|
|
|
4
8
|
import gpxpy
|
|
5
|
-
import gpxpy.gpx
|
|
6
9
|
|
|
7
|
-
from .. import geo
|
|
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)
|
|
8
19
|
|
|
20
|
+
tracks: list[Track] = []
|
|
9
21
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
)
|
|
13
36
|
|
|
37
|
+
return tracks
|
|
14
38
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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()
|
|
24
59
|
)
|
|
25
60
|
)
|
|
26
|
-
|
|
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.VideoMetadataOrError:
|
|
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.VideoMetadataOrError:
|
|
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
|
mapillary_tools/{video_data_extraction/extractors/gpx_parser.py → geotag/video_extractors/gpx.py}
RENAMED
|
@@ -1,51 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
1
4
|
import datetime
|
|
2
5
|
import logging
|
|
6
|
+
import sys
|
|
3
7
|
import typing as T
|
|
8
|
+
from pathlib import Path
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from
|
|
10
|
+
if sys.version_info >= (3, 12):
|
|
11
|
+
from typing import override
|
|
12
|
+
else:
|
|
13
|
+
from typing_extensions import override
|
|
9
14
|
|
|
15
|
+
from ... import geo, telemetry, types
|
|
16
|
+
from ..utils import parse_gpx
|
|
17
|
+
from .base import BaseVideoExtractor
|
|
18
|
+
from .native import NativeVideoExtractor
|
|
10
19
|
|
|
11
|
-
LOG = logging.getLogger(__name__)
|
|
12
20
|
|
|
21
|
+
LOG = logging.getLogger(__name__)
|
|
13
22
|
|
|
14
|
-
class GpxParser(BaseParser):
|
|
15
|
-
default_source_pattern = "%g.gpx"
|
|
16
|
-
parser_label = "gpx"
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
class GPXVideoExtractor(BaseVideoExtractor):
|
|
25
|
+
def __init__(self, video_path: Path, gpx_path: Path):
|
|
26
|
+
self.video_path = video_path
|
|
27
|
+
self.gpx_path = gpx_path
|
|
22
28
|
|
|
29
|
+
@override
|
|
30
|
+
def extract(self) -> types.VideoMetadataOrError:
|
|
23
31
|
try:
|
|
24
|
-
gpx_tracks =
|
|
32
|
+
gpx_tracks = parse_gpx(self.gpx_path)
|
|
25
33
|
except Exception as ex:
|
|
26
34
|
raise RuntimeError(
|
|
27
|
-
f"Error parsing GPX {
|
|
35
|
+
f"Error parsing GPX {self.gpx_path}: {ex.__class__.__name__}: {ex}"
|
|
28
36
|
)
|
|
29
37
|
|
|
30
38
|
if 1 < len(gpx_tracks):
|
|
31
39
|
LOG.warning(
|
|
32
40
|
"Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
|
|
33
41
|
len(gpx_tracks),
|
|
34
|
-
self.
|
|
42
|
+
self.gpx_path,
|
|
35
43
|
)
|
|
36
44
|
|
|
37
45
|
gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
|
|
38
|
-
if not gpx_points:
|
|
39
|
-
return gpx_points
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
native_extractor = NativeVideoExtractor(self.video_path)
|
|
48
|
+
|
|
49
|
+
video_metadata_or_error = native_extractor.extract()
|
|
50
|
+
|
|
51
|
+
if isinstance(video_metadata_or_error, types.ErrorMetadata):
|
|
52
|
+
self._rebase_times(gpx_points)
|
|
53
|
+
return types.VideoMetadata(
|
|
54
|
+
filename=video_metadata_or_error.filename,
|
|
55
|
+
filetype=video_metadata_or_error.filetype or types.FileType.VIDEO,
|
|
56
|
+
points=gpx_points,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
video_metadata = video_metadata_or_error
|
|
60
|
+
|
|
61
|
+
offset = self._synx_gpx_by_first_gps_timestamp(
|
|
62
|
+
gpx_points, video_metadata.points
|
|
63
|
+
)
|
|
42
64
|
|
|
43
65
|
self._rebase_times(gpx_points, offset=offset)
|
|
44
66
|
|
|
45
|
-
return gpx_points
|
|
67
|
+
return dataclasses.replace(video_metadata_or_error, points=gpx_points)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
71
|
+
"""
|
|
72
|
+
Make point times start from 0
|
|
73
|
+
"""
|
|
74
|
+
if points:
|
|
75
|
+
first_timestamp = points[0].time
|
|
76
|
+
for p in points:
|
|
77
|
+
p.time = (p.time - first_timestamp) + offset
|
|
78
|
+
return points
|
|
46
79
|
|
|
47
80
|
def _synx_gpx_by_first_gps_timestamp(
|
|
48
|
-
self, gpx_points: T.Sequence[geo.Point]
|
|
81
|
+
self, gpx_points: T.Sequence[geo.Point], video_gps_points: T.Sequence[geo.Point]
|
|
49
82
|
) -> float:
|
|
50
83
|
offset: float = 0.0
|
|
51
84
|
|
|
@@ -57,19 +90,14 @@ class GpxParser(BaseParser):
|
|
|
57
90
|
)
|
|
58
91
|
LOG.info("First GPX timestamp: %s", first_gpx_dt)
|
|
59
92
|
|
|
60
|
-
|
|
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:
|
|
93
|
+
if not video_gps_points:
|
|
66
94
|
LOG.warning(
|
|
67
95
|
"Skip GPX synchronization because no GPS found in video %s",
|
|
68
|
-
self.
|
|
96
|
+
self.video_path,
|
|
69
97
|
)
|
|
70
98
|
return offset
|
|
71
99
|
|
|
72
|
-
first_gps_point =
|
|
100
|
+
first_gps_point = video_gps_points[0]
|
|
73
101
|
if isinstance(first_gps_point, telemetry.GPSPoint):
|
|
74
102
|
if first_gps_point.epoch_time is not None:
|
|
75
103
|
first_gps_dt = datetime.datetime.fromtimestamp(
|
|
@@ -92,17 +120,7 @@ class GpxParser(BaseParser):
|
|
|
92
120
|
else:
|
|
93
121
|
LOG.warning(
|
|
94
122
|
"Skip GPX synchronization because no GPS epoch time found in video %s",
|
|
95
|
-
self.
|
|
123
|
+
self.video_path,
|
|
96
124
|
)
|
|
97
125
|
|
|
98
126
|
return offset
|
|
99
|
-
|
|
100
|
-
def extract_make(self) -> T.Optional[str]:
|
|
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()
|
|
104
|
-
|
|
105
|
-
def extract_model(self) -> T.Optional[str]:
|
|
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()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import typing as T
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
if sys.version_info >= (3, 12):
|
|
8
|
+
from typing import override
|
|
9
|
+
else:
|
|
10
|
+
from typing_extensions import override
|
|
11
|
+
|
|
12
|
+
from ... import blackvue_parser, exceptions, geo, telemetry, types, utils
|
|
13
|
+
from ...camm import camm_parser
|
|
14
|
+
from ...gpmf import gpmf_gps_filter, gpmf_parser
|
|
15
|
+
from .base import BaseVideoExtractor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GoProVideoExtractor(BaseVideoExtractor):
|
|
19
|
+
@override
|
|
20
|
+
def extract(self) -> types.VideoMetadataOrError:
|
|
21
|
+
with self.video_path.open("rb") as fp:
|
|
22
|
+
gopro_info = gpmf_parser.extract_gopro_info(fp)
|
|
23
|
+
|
|
24
|
+
if gopro_info is None:
|
|
25
|
+
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
26
|
+
"No GPS data found from the video"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
gps_points = gopro_info.gps
|
|
30
|
+
assert gps_points is not None, "must have GPS data extracted"
|
|
31
|
+
if not gps_points:
|
|
32
|
+
# Instead of raising an exception, return error metadata to tell the file type
|
|
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
|
+
)
|
|
39
|
+
|
|
40
|
+
gps_points = T.cast(
|
|
41
|
+
T.List[telemetry.GPSPoint], gpmf_gps_filter.remove_noisy_points(gps_points)
|
|
42
|
+
)
|
|
43
|
+
if not gps_points:
|
|
44
|
+
# Instead of raising an exception, return error metadata to tell the file type
|
|
45
|
+
ex = exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
46
|
+
return types.describe_error_metadata(
|
|
47
|
+
ex, self.video_path, filetype=types.FileType.GOPRO
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
video_metadata = types.VideoMetadata(
|
|
51
|
+
filename=self.video_path,
|
|
52
|
+
filesize=utils.get_file_size(self.video_path),
|
|
53
|
+
filetype=types.FileType.GOPRO,
|
|
54
|
+
points=T.cast(T.List[geo.Point], gps_points),
|
|
55
|
+
make=gopro_info.make,
|
|
56
|
+
model=gopro_info.model,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return video_metadata
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CAMMVideoExtractor(BaseVideoExtractor):
|
|
63
|
+
@override
|
|
64
|
+
def extract(self) -> types.VideoMetadataOrError:
|
|
65
|
+
with self.video_path.open("rb") as fp:
|
|
66
|
+
camm_info = camm_parser.extract_camm_info(fp)
|
|
67
|
+
|
|
68
|
+
if camm_info is None:
|
|
69
|
+
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
70
|
+
"No GPS data found from the video"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not camm_info.gps and not camm_info.mini_gps:
|
|
74
|
+
# Instead of raising an exception, return error metadata to tell the file type
|
|
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
|
+
)
|
|
81
|
+
|
|
82
|
+
return types.VideoMetadata(
|
|
83
|
+
filename=self.video_path,
|
|
84
|
+
filesize=utils.get_file_size(self.video_path),
|
|
85
|
+
filetype=types.FileType.CAMM,
|
|
86
|
+
points=T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps),
|
|
87
|
+
make=camm_info.make,
|
|
88
|
+
model=camm_info.model,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BlackVueVideoExtractor(BaseVideoExtractor):
|
|
93
|
+
@override
|
|
94
|
+
def extract(self) -> types.VideoMetadataOrError:
|
|
95
|
+
with self.video_path.open("rb") as fp:
|
|
96
|
+
blackvue_info = blackvue_parser.extract_blackvue_info(fp)
|
|
97
|
+
|
|
98
|
+
if blackvue_info is None:
|
|
99
|
+
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
100
|
+
"No GPS data found from the video"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not blackvue_info.gps:
|
|
104
|
+
# Instead of raising an exception, return error metadata to tell the file type
|
|
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
|
+
)
|
|
111
|
+
|
|
112
|
+
video_metadata = types.VideoMetadata(
|
|
113
|
+
filename=self.video_path,
|
|
114
|
+
filesize=utils.get_file_size(self.video_path),
|
|
115
|
+
filetype=types.FileType.BLACKVUE,
|
|
116
|
+
points=blackvue_info.gps or [],
|
|
117
|
+
make=blackvue_info.make,
|
|
118
|
+
model=blackvue_info.model,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return video_metadata
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class NativeVideoExtractor(BaseVideoExtractor):
|
|
125
|
+
def __init__(self, video_path: Path, filetypes: set[types.FileType] | None = None):
|
|
126
|
+
super().__init__(video_path)
|
|
127
|
+
self.filetypes = filetypes
|
|
128
|
+
|
|
129
|
+
@override
|
|
130
|
+
def extract(self) -> types.VideoMetadataOrError:
|
|
131
|
+
ft = self.filetypes
|
|
132
|
+
extractor: BaseVideoExtractor
|
|
133
|
+
|
|
134
|
+
if ft is None or types.FileType.VIDEO in ft or types.FileType.GOPRO in ft:
|
|
135
|
+
extractor = GoProVideoExtractor(self.video_path)
|
|
136
|
+
try:
|
|
137
|
+
return extractor.extract()
|
|
138
|
+
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
if ft is None or types.FileType.VIDEO in ft or types.FileType.CAMM in ft:
|
|
142
|
+
extractor = CAMMVideoExtractor(self.video_path)
|
|
143
|
+
try:
|
|
144
|
+
return extractor.extract()
|
|
145
|
+
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
if ft is None or types.FileType.VIDEO in ft or types.FileType.BLACKVUE in ft:
|
|
149
|
+
extractor = BlackVueVideoExtractor(self.video_path)
|
|
150
|
+
try:
|
|
151
|
+
return extractor.extract()
|
|
152
|
+
except exceptions.MapillaryVideoGPSNotFoundError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
156
|
+
"No GPS data found from the video"
|
|
157
|
+
)
|