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.
- mapillary_tools/__init__.py +1 -1
- mapillary_tools/api_v4.py +5 -4
- mapillary_tools/authenticate.py +9 -9
- mapillary_tools/blackvue_parser.py +79 -22
- mapillary_tools/camm/camm_parser.py +5 -5
- mapillary_tools/commands/__main__.py +1 -2
- mapillary_tools/config.py +41 -18
- mapillary_tools/constants.py +3 -2
- mapillary_tools/exceptions.py +1 -1
- mapillary_tools/exif_read.py +65 -65
- mapillary_tools/exif_write.py +7 -7
- mapillary_tools/exiftool_read.py +23 -46
- mapillary_tools/exiftool_read_video.py +88 -49
- mapillary_tools/exiftool_runner.py +4 -24
- mapillary_tools/ffmpeg.py +417 -242
- mapillary_tools/geo.py +4 -21
- mapillary_tools/geotag/__init__.py +0 -1
- mapillary_tools/geotag/{geotag_from_generic.py → base.py} +34 -50
- mapillary_tools/geotag/factory.py +105 -103
- mapillary_tools/geotag/geotag_images_from_exif.py +15 -51
- mapillary_tools/geotag/geotag_images_from_exiftool.py +118 -63
- mapillary_tools/geotag/geotag_images_from_gpx.py +33 -16
- mapillary_tools/geotag/geotag_images_from_gpx_file.py +2 -34
- mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -3
- mapillary_tools/geotag/geotag_images_from_video.py +51 -14
- mapillary_tools/geotag/geotag_videos_from_exiftool.py +123 -0
- mapillary_tools/geotag/geotag_videos_from_gpx.py +35 -123
- mapillary_tools/geotag/geotag_videos_from_video.py +14 -147
- 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 +26 -3
- mapillary_tools/geotag/utils.py +62 -0
- mapillary_tools/geotag/video_extractors/base.py +18 -0
- mapillary_tools/geotag/video_extractors/exiftool.py +70 -0
- mapillary_tools/geotag/video_extractors/gpx.py +116 -0
- mapillary_tools/geotag/video_extractors/native.py +135 -0
- mapillary_tools/gpmf/gpmf_parser.py +16 -16
- mapillary_tools/gpmf/gps_filter.py +5 -3
- mapillary_tools/history.py +8 -3
- mapillary_tools/mp4/construct_mp4_parser.py +9 -8
- mapillary_tools/mp4/mp4_sample_parser.py +27 -27
- mapillary_tools/mp4/simple_mp4_builder.py +10 -9
- mapillary_tools/mp4/simple_mp4_parser.py +13 -12
- mapillary_tools/process_geotag_properties.py +21 -15
- mapillary_tools/process_sequence_properties.py +49 -49
- mapillary_tools/sample_video.py +15 -14
- mapillary_tools/serializer/description.py +587 -0
- mapillary_tools/serializer/gpx.py +132 -0
- mapillary_tools/telemetry.py +6 -5
- mapillary_tools/types.py +64 -635
- mapillary_tools/upload.py +176 -197
- mapillary_tools/upload_api_v4.py +94 -51
- mapillary_tools/uploader.py +284 -138
- mapillary_tools/utils.py +16 -18
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/METADATA +87 -31
- mapillary_tools-0.14.0b1.dist-info/RECORD +75 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/WHEEL +1 -1
- mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -77
- mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -151
- mapillary_tools/video_data_extraction/cli_options.py +0 -22
- mapillary_tools/video_data_extraction/extract_video_data.py +0 -157
- mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -75
- mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -49
- mapillary_tools/video_data_extraction/extractors/camm_parser.py +0 -62
- mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -74
- mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -52
- mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -58
- mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -108
- 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.14.0a1.dist-info/RECORD +0 -78
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/entry_points.txt +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/licenses/LICENSE +0 -0
- {mapillary_tools-0.14.0a1.dist-info → mapillary_tools-0.14.0b1.dist-info}/top_level.txt +0 -0
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import typing as T
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
import tqdm
|
|
8
|
-
|
|
9
|
-
from .. import exceptions, geo, utils
|
|
10
|
-
from ..gpmf import gpmf_gps_filter
|
|
11
|
-
from ..telemetry import GPSPoint
|
|
12
|
-
from ..types import ErrorMetadata, FileType, VideoMetadata, VideoMetadataOrError
|
|
13
|
-
from . import video_data_parser_factory
|
|
14
|
-
from .cli_options import CliOptions
|
|
15
|
-
from .extractors.base_parser import BaseParser
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
LOG = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class VideoDataExtractor:
|
|
22
|
-
options: CliOptions
|
|
23
|
-
|
|
24
|
-
def __init__(self, options: CliOptions) -> None:
|
|
25
|
-
self.options = options
|
|
26
|
-
|
|
27
|
-
def process(self) -> T.List[VideoMetadataOrError]:
|
|
28
|
-
paths = self.options["paths"]
|
|
29
|
-
self._check_paths(paths)
|
|
30
|
-
video_files = utils.find_videos(paths)
|
|
31
|
-
self._check_sources_cardinality(video_files)
|
|
32
|
-
|
|
33
|
-
map_results = utils.mp_map_maybe(
|
|
34
|
-
self.process_file, video_files, num_processes=self.options["num_processes"]
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
video_metadata_or_errors: list[VideoMetadataOrError] = list(
|
|
38
|
-
tqdm.tqdm(
|
|
39
|
-
map_results,
|
|
40
|
-
desc="Extracting GPS tracks",
|
|
41
|
-
unit="videos",
|
|
42
|
-
disable=LOG.getEffectiveLevel() <= logging.DEBUG,
|
|
43
|
-
total=len(video_files),
|
|
44
|
-
)
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
return video_metadata_or_errors
|
|
48
|
-
|
|
49
|
-
def process_file(self, file: Path) -> VideoMetadataOrError:
|
|
50
|
-
parsers = video_data_parser_factory.make_parsers(file, self.options)
|
|
51
|
-
points: T.Sequence[geo.Point] = []
|
|
52
|
-
make = self.options["device_make"]
|
|
53
|
-
model = self.options["device_model"]
|
|
54
|
-
|
|
55
|
-
ex: T.Optional[Exception]
|
|
56
|
-
for parser in parsers:
|
|
57
|
-
log_vars = {
|
|
58
|
-
"filename": file,
|
|
59
|
-
"parser": parser.parser_label,
|
|
60
|
-
"source": parser.geotag_source_path,
|
|
61
|
-
}
|
|
62
|
-
try:
|
|
63
|
-
if not points:
|
|
64
|
-
points = self._extract_points(parser, log_vars)
|
|
65
|
-
if not model:
|
|
66
|
-
model = parser.extract_model()
|
|
67
|
-
if not make:
|
|
68
|
-
make = parser.extract_make()
|
|
69
|
-
except Exception as e:
|
|
70
|
-
ex = e
|
|
71
|
-
LOG.warning(
|
|
72
|
-
'%(filename)s: Exception for parser %(parser)s while processing source %(source)s: "%(e)s"',
|
|
73
|
-
{**log_vars, "e": e},
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# After trying all parsers, return the points if we found any, otherwise
|
|
77
|
-
# the last exception thrown or a default one.
|
|
78
|
-
# Note that if we have points, we return them, regardless of exceptions
|
|
79
|
-
# with make or model.
|
|
80
|
-
if points:
|
|
81
|
-
video_metadata = VideoMetadata(
|
|
82
|
-
filename=file,
|
|
83
|
-
filetype=FileType.VIDEO,
|
|
84
|
-
filesize=utils.get_file_size(file),
|
|
85
|
-
points=points,
|
|
86
|
-
make=make,
|
|
87
|
-
model=model,
|
|
88
|
-
)
|
|
89
|
-
return video_metadata
|
|
90
|
-
else:
|
|
91
|
-
return ErrorMetadata(
|
|
92
|
-
filename=file,
|
|
93
|
-
error=(
|
|
94
|
-
ex
|
|
95
|
-
if ex
|
|
96
|
-
else exceptions.MapillaryVideoGPSNotFoundError(
|
|
97
|
-
"No GPS data found from the video"
|
|
98
|
-
)
|
|
99
|
-
),
|
|
100
|
-
filetype=FileType.VIDEO,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def _extract_points(
|
|
104
|
-
self, parser: BaseParser, log_vars: T.Dict
|
|
105
|
-
) -> T.Sequence[geo.Point]:
|
|
106
|
-
points = parser.extract_points()
|
|
107
|
-
if points:
|
|
108
|
-
LOG.debug(
|
|
109
|
-
"%(filename)s: %(points)d points extracted by parser %(parser)s from file %(source)s}",
|
|
110
|
-
{**log_vars, "points": len(points)},
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
return self._sanitize_points(points)
|
|
114
|
-
|
|
115
|
-
@staticmethod
|
|
116
|
-
def _check_paths(import_paths: T.Sequence[Path]):
|
|
117
|
-
for path in import_paths:
|
|
118
|
-
if not path.is_file() and not path.is_dir():
|
|
119
|
-
raise exceptions.MapillaryFileNotFoundError(
|
|
120
|
-
f"Import file or directory not found: {path}"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
def _check_sources_cardinality(self, files: T.Sequence[Path]):
|
|
124
|
-
if len(files) > 1:
|
|
125
|
-
for parser_opts in self.options["geotag_sources_options"]:
|
|
126
|
-
pattern = parser_opts.get("pattern")
|
|
127
|
-
if pattern and "%" not in pattern:
|
|
128
|
-
raise exceptions.MapillaryUserError(
|
|
129
|
-
"Multiple video files found: Geotag source pattern for source %s must include filename placeholders",
|
|
130
|
-
parser_opts["source"],
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
@staticmethod
|
|
134
|
-
def _sanitize_points(points: T.Sequence[geo.Point]) -> T.Sequence[geo.Point]:
|
|
135
|
-
"""
|
|
136
|
-
Deduplicates points, when possible removes noisy ones, and checks
|
|
137
|
-
against stationary videos
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
if not points:
|
|
141
|
-
raise exceptions.MapillaryVideoGPSNotFoundError(
|
|
142
|
-
"No GPS data found in the given sources"
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
points = geo.extend_deduplicate_points(points)
|
|
146
|
-
|
|
147
|
-
if all(isinstance(p, GPSPoint) for p in points):
|
|
148
|
-
points = T.cast(
|
|
149
|
-
T.Sequence[geo.Point],
|
|
150
|
-
gpmf_gps_filter.remove_noisy_points(
|
|
151
|
-
T.cast(T.Sequence[GPSPoint], points)
|
|
152
|
-
),
|
|
153
|
-
)
|
|
154
|
-
if not points:
|
|
155
|
-
raise exceptions.MapillaryGPSNoiseError("GPS is too noisy")
|
|
156
|
-
|
|
157
|
-
return points
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import functools
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
import typing as T
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from ... import geo
|
|
9
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
10
|
-
|
|
11
|
-
LOG = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class BaseParser(metaclass=abc.ABCMeta):
|
|
15
|
-
videoPath: Path
|
|
16
|
-
options: CliOptions
|
|
17
|
-
parserOptions: CliParserOptions
|
|
18
|
-
|
|
19
|
-
def __init__(
|
|
20
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
21
|
-
) -> None:
|
|
22
|
-
self.videoPath = video_path
|
|
23
|
-
self.options = options
|
|
24
|
-
self.parserOptions = parser_options
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
@abc.abstractmethod
|
|
28
|
-
def default_source_pattern(self) -> str:
|
|
29
|
-
raise NotImplementedError
|
|
30
|
-
|
|
31
|
-
@property
|
|
32
|
-
@abc.abstractmethod
|
|
33
|
-
def parser_label(self) -> str:
|
|
34
|
-
raise NotImplementedError
|
|
35
|
-
|
|
36
|
-
@abc.abstractmethod
|
|
37
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
38
|
-
raise NotImplementedError
|
|
39
|
-
|
|
40
|
-
@abc.abstractmethod
|
|
41
|
-
def extract_make(self) -> T.Optional[str]:
|
|
42
|
-
raise NotImplementedError
|
|
43
|
-
|
|
44
|
-
@abc.abstractmethod
|
|
45
|
-
def extract_model(self) -> T.Optional[str]:
|
|
46
|
-
raise NotImplementedError
|
|
47
|
-
|
|
48
|
-
@functools.cached_property
|
|
49
|
-
def geotag_source_path(self) -> T.Optional[Path]:
|
|
50
|
-
video_dir = self.videoPath.parent.resolve()
|
|
51
|
-
video_filename = self.videoPath.name
|
|
52
|
-
video_basename, video_ext = os.path.splitext(video_filename)
|
|
53
|
-
pattern = self.parserOptions.get("pattern") or self.default_source_pattern
|
|
54
|
-
|
|
55
|
-
replaced = Path(
|
|
56
|
-
pattern.replace("%f", video_filename)
|
|
57
|
-
.replace("%g", video_basename)
|
|
58
|
-
.replace("%e", video_ext)
|
|
59
|
-
)
|
|
60
|
-
abs_path = (
|
|
61
|
-
replaced if replaced.is_absolute() else Path.joinpath(video_dir, replaced)
|
|
62
|
-
).resolve()
|
|
63
|
-
|
|
64
|
-
return abs_path if abs_path.is_file() else None
|
|
65
|
-
|
|
66
|
-
@staticmethod
|
|
67
|
-
def _rebase_times(points: T.Sequence[geo.Point], offset: float = 0.0):
|
|
68
|
-
"""
|
|
69
|
-
Make point times start from 0
|
|
70
|
-
"""
|
|
71
|
-
if points:
|
|
72
|
-
first_timestamp = points[0].time
|
|
73
|
-
for p in points:
|
|
74
|
-
p.time = (p.time - first_timestamp) + offset
|
|
75
|
-
return points
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import functools
|
|
4
|
-
|
|
5
|
-
import typing as T
|
|
6
|
-
|
|
7
|
-
from ... import blackvue_parser, geo
|
|
8
|
-
from .base_parser import BaseParser
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class BlackVueParser(BaseParser):
|
|
12
|
-
default_source_pattern = "%f"
|
|
13
|
-
must_rebase_times_to_zero = False
|
|
14
|
-
parser_label = "blackvue"
|
|
15
|
-
|
|
16
|
-
pointsFound: bool = False
|
|
17
|
-
|
|
18
|
-
@functools.cached_property
|
|
19
|
-
def extract_blackvue_info(self) -> blackvue_parser.BlackVueInfo | None:
|
|
20
|
-
source_path = self.geotag_source_path
|
|
21
|
-
if not source_path:
|
|
22
|
-
return None
|
|
23
|
-
|
|
24
|
-
with source_path.open("rb") as fp:
|
|
25
|
-
return blackvue_parser.extract_blackvue_info(fp)
|
|
26
|
-
|
|
27
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
28
|
-
blackvue_info = self.extract_blackvue_info
|
|
29
|
-
|
|
30
|
-
if blackvue_info is None:
|
|
31
|
-
return []
|
|
32
|
-
|
|
33
|
-
return blackvue_info.gps or []
|
|
34
|
-
|
|
35
|
-
def extract_make(self) -> str | None:
|
|
36
|
-
blackvue_info = self.extract_blackvue_info
|
|
37
|
-
|
|
38
|
-
if blackvue_info is None:
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
return blackvue_info.make
|
|
42
|
-
|
|
43
|
-
def extract_model(self) -> str | None:
|
|
44
|
-
blackvue_info = self.extract_blackvue_info
|
|
45
|
-
|
|
46
|
-
if blackvue_info is None:
|
|
47
|
-
return None
|
|
48
|
-
|
|
49
|
-
return blackvue_info.model
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import typing as T
|
|
4
|
-
|
|
5
|
-
from ... import geo
|
|
6
|
-
from ...camm import camm_parser
|
|
7
|
-
from ...mp4 import simple_mp4_parser as sparser
|
|
8
|
-
from .base_parser import BaseParser
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CammParser(BaseParser):
|
|
12
|
-
default_source_pattern = "%f"
|
|
13
|
-
must_rebase_times_to_zero = False
|
|
14
|
-
parser_label = "camm"
|
|
15
|
-
|
|
16
|
-
_extracted: bool = False
|
|
17
|
-
_cached_camm_info: camm_parser.CAMMInfo | None = None
|
|
18
|
-
|
|
19
|
-
# TODO: use @functools.cached_property
|
|
20
|
-
def _extract_camm_info(self) -> camm_parser.CAMMInfo | None:
|
|
21
|
-
if self._extracted:
|
|
22
|
-
return self._cached_camm_info
|
|
23
|
-
|
|
24
|
-
self._extracted = True
|
|
25
|
-
|
|
26
|
-
source_path = self.geotag_source_path
|
|
27
|
-
|
|
28
|
-
if source_path is None:
|
|
29
|
-
# source_path not found
|
|
30
|
-
return None
|
|
31
|
-
|
|
32
|
-
with source_path.open("rb") as fp:
|
|
33
|
-
try:
|
|
34
|
-
self._cached_camm_info = camm_parser.extract_camm_info(fp)
|
|
35
|
-
except sparser.ParsingError:
|
|
36
|
-
self._cached_camm_info = None
|
|
37
|
-
|
|
38
|
-
return self._cached_camm_info
|
|
39
|
-
|
|
40
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
41
|
-
camm_info = self._extract_camm_info()
|
|
42
|
-
|
|
43
|
-
if camm_info is None:
|
|
44
|
-
return []
|
|
45
|
-
|
|
46
|
-
return T.cast(T.List[geo.Point], camm_info.gps or camm_info.mini_gps)
|
|
47
|
-
|
|
48
|
-
def extract_make(self) -> str | None:
|
|
49
|
-
camm_info = self._extract_camm_info()
|
|
50
|
-
|
|
51
|
-
if camm_info is None:
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
return camm_info.make
|
|
55
|
-
|
|
56
|
-
def extract_model(self) -> str | None:
|
|
57
|
-
camm_info = self._extract_camm_info()
|
|
58
|
-
|
|
59
|
-
if camm_info is None:
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
return camm_info.model
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import shutil
|
|
2
|
-
import subprocess
|
|
3
|
-
import typing as T
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from ... import constants, exceptions, geo
|
|
7
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
8
|
-
from .base_parser import BaseParser
|
|
9
|
-
from .exiftool_xml_parser import ExiftoolXmlParser
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ExiftoolRuntimeParser(BaseParser):
|
|
13
|
-
"""
|
|
14
|
-
Wrapper around ExiftoolRdfParser that executes exiftool
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
exiftoolXmlParser: ExiftoolXmlParser
|
|
18
|
-
|
|
19
|
-
default_source_pattern = "%f"
|
|
20
|
-
must_rebase_times_to_zero = True
|
|
21
|
-
parser_label = "exiftool_runtime"
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
25
|
-
):
|
|
26
|
-
super().__init__(video_path, options, parser_options)
|
|
27
|
-
if constants.EXIFTOOL_PATH is None:
|
|
28
|
-
exiftool_path = shutil.which("exiftool")
|
|
29
|
-
else:
|
|
30
|
-
exiftool_path = shutil.which(constants.EXIFTOOL_PATH)
|
|
31
|
-
|
|
32
|
-
if not exiftool_path:
|
|
33
|
-
raise exceptions.MapillaryExiftoolNotFoundError(
|
|
34
|
-
"Cannot execute exiftool. Please install it from https://exiftool.org/ or you package manager, or set the environment variable MAPILLARY_TOOLS_EXIFTOOL_PATH"
|
|
35
|
-
)
|
|
36
|
-
if not self.geotag_source_path:
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
# To handle non-latin1 filenames under Windows, we pass the path
|
|
40
|
-
# via stdin. See https://exiftool.org/faq.html#Q18
|
|
41
|
-
stdin = str(self.geotag_source_path)
|
|
42
|
-
args = [
|
|
43
|
-
exiftool_path,
|
|
44
|
-
"-q",
|
|
45
|
-
"-r",
|
|
46
|
-
"-n",
|
|
47
|
-
"-ee",
|
|
48
|
-
"-api",
|
|
49
|
-
"LargeFileSupport=1",
|
|
50
|
-
"-X",
|
|
51
|
-
"-charset",
|
|
52
|
-
"filename=utf8",
|
|
53
|
-
"-@",
|
|
54
|
-
"-",
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
process = subprocess.run(
|
|
58
|
-
args, capture_output=True, text=True, input=stdin, encoding="utf-8"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
self.exiftoolXmlParser = ExiftoolXmlParser(
|
|
62
|
-
video_path, options, parser_options, process.stdout
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
66
|
-
return self.exiftoolXmlParser.extract_points() if self.exiftoolXmlParser else []
|
|
67
|
-
|
|
68
|
-
def extract_make(self) -> T.Optional[str]:
|
|
69
|
-
return self.exiftoolXmlParser.extract_make() if self.exiftoolXmlParser else None
|
|
70
|
-
|
|
71
|
-
def extract_model(self) -> T.Optional[str]:
|
|
72
|
-
return (
|
|
73
|
-
self.exiftoolXmlParser.extract_model() if self.exiftoolXmlParser else None
|
|
74
|
-
)
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
import xml.etree.ElementTree as ET
|
|
3
|
-
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from ... import geo
|
|
7
|
-
from ...exiftool_read import _DESCRIPTION_TAG, EXIFTOOL_NAMESPACES
|
|
8
|
-
from ...exiftool_read_video import ExifToolReadVideo
|
|
9
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
10
|
-
from .base_parser import BaseParser
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ExiftoolXmlParser(BaseParser):
|
|
14
|
-
default_source_pattern = "%g.xml"
|
|
15
|
-
parser_label = "exiftool_xml"
|
|
16
|
-
|
|
17
|
-
exifToolReadVideo: T.Optional[ExifToolReadVideo] = None
|
|
18
|
-
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
video_path: Path,
|
|
22
|
-
options: CliOptions,
|
|
23
|
-
parser_options: CliParserOptions,
|
|
24
|
-
xml_content: T.Optional[str] = None,
|
|
25
|
-
) -> None:
|
|
26
|
-
super().__init__(video_path, options, parser_options)
|
|
27
|
-
|
|
28
|
-
if xml_content:
|
|
29
|
-
etree = ET.fromstring(xml_content)
|
|
30
|
-
else:
|
|
31
|
-
xml_path = self.geotag_source_path
|
|
32
|
-
if not xml_path:
|
|
33
|
-
return
|
|
34
|
-
etree = ET.parse(xml_path).getroot()
|
|
35
|
-
|
|
36
|
-
element = next(etree.iterfind(_DESCRIPTION_TAG, namespaces=EXIFTOOL_NAMESPACES))
|
|
37
|
-
self.exifToolReadVideo = ExifToolReadVideo(ET.ElementTree(element))
|
|
38
|
-
|
|
39
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
40
|
-
gps_points = (
|
|
41
|
-
self.exifToolReadVideo.extract_gps_track() if self.exifToolReadVideo else []
|
|
42
|
-
)
|
|
43
|
-
self._rebase_times(gps_points)
|
|
44
|
-
return gps_points
|
|
45
|
-
|
|
46
|
-
def extract_make(self) -> T.Optional[str]:
|
|
47
|
-
return self.exifToolReadVideo.extract_make() if self.exifToolReadVideo else None
|
|
48
|
-
|
|
49
|
-
def extract_model(self) -> T.Optional[str]:
|
|
50
|
-
return (
|
|
51
|
-
self.exifToolReadVideo.extract_model() if self.exifToolReadVideo else None
|
|
52
|
-
)
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import typing as T
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from ... import geo
|
|
5
|
-
from ..cli_options import CliOptions, CliParserOptions
|
|
6
|
-
from .base_parser import BaseParser
|
|
7
|
-
from .blackvue_parser import BlackVueParser
|
|
8
|
-
from .camm_parser import CammParser
|
|
9
|
-
from .gopro_parser import GoProParser
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class GenericVideoParser(BaseParser):
|
|
13
|
-
"""
|
|
14
|
-
Wrapper around the three native video parsers. It will try to execute them
|
|
15
|
-
in the order camm-gopro-blackvue, like the previous implementation
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
parsers: T.Sequence[BaseParser] = []
|
|
19
|
-
|
|
20
|
-
default_source_pattern = "%f"
|
|
21
|
-
must_rebase_times_to_zero = False
|
|
22
|
-
parser_label = "video"
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self, video_path: Path, options: CliOptions, parser_options: CliParserOptions
|
|
26
|
-
) -> None:
|
|
27
|
-
super().__init__(video_path, options, parser_options)
|
|
28
|
-
camm_parser = CammParser(video_path, options, parser_options)
|
|
29
|
-
gopro_parser = GoProParser(video_path, options, parser_options)
|
|
30
|
-
blackvue_parser = BlackVueParser(video_path, options, parser_options)
|
|
31
|
-
self.parsers = [camm_parser, gopro_parser, blackvue_parser]
|
|
32
|
-
|
|
33
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
34
|
-
for parser in self.parsers:
|
|
35
|
-
points = parser.extract_points()
|
|
36
|
-
if points:
|
|
37
|
-
return points
|
|
38
|
-
return []
|
|
39
|
-
|
|
40
|
-
def extract_make(self) -> T.Optional[str]:
|
|
41
|
-
for parser in self.parsers:
|
|
42
|
-
make = parser.extract_make()
|
|
43
|
-
if make:
|
|
44
|
-
return make
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
def extract_model(self) -> T.Optional[str]:
|
|
48
|
-
for parser in self.parsers:
|
|
49
|
-
model = parser.extract_model()
|
|
50
|
-
if model:
|
|
51
|
-
return model
|
|
52
|
-
return None
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import typing as T
|
|
4
|
-
|
|
5
|
-
from ... import geo
|
|
6
|
-
from ...gpmf import gpmf_parser
|
|
7
|
-
from ...mp4 import simple_mp4_parser as sparser
|
|
8
|
-
from .base_parser import BaseParser
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class GoProParser(BaseParser):
|
|
12
|
-
default_source_pattern = "%f"
|
|
13
|
-
must_rebase_times_to_zero = False
|
|
14
|
-
parser_label = "gopro"
|
|
15
|
-
|
|
16
|
-
_extracted: bool = False
|
|
17
|
-
_cached_gopro_info: gpmf_parser.GoProInfo | None = None
|
|
18
|
-
|
|
19
|
-
def _extract_gopro_info(self) -> gpmf_parser.GoProInfo | None:
|
|
20
|
-
if self._extracted:
|
|
21
|
-
return self._cached_gopro_info
|
|
22
|
-
|
|
23
|
-
self._extracted = True
|
|
24
|
-
|
|
25
|
-
source_path = self.geotag_source_path
|
|
26
|
-
|
|
27
|
-
if source_path is None:
|
|
28
|
-
# source_path not found
|
|
29
|
-
return None
|
|
30
|
-
|
|
31
|
-
with source_path.open("rb") as fp:
|
|
32
|
-
try:
|
|
33
|
-
self._cached_gopro_info = gpmf_parser.extract_gopro_info(fp)
|
|
34
|
-
except sparser.ParsingError:
|
|
35
|
-
self._cached_gopro_info = None
|
|
36
|
-
|
|
37
|
-
return self._cached_gopro_info
|
|
38
|
-
|
|
39
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
40
|
-
gopro_info = self._extract_gopro_info()
|
|
41
|
-
if gopro_info is None:
|
|
42
|
-
return []
|
|
43
|
-
|
|
44
|
-
return T.cast(T.Sequence[geo.Point], gopro_info.gps)
|
|
45
|
-
|
|
46
|
-
def extract_make(self) -> str | None:
|
|
47
|
-
gopro_info = self._extract_gopro_info()
|
|
48
|
-
if gopro_info is None:
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
return gopro_info.make
|
|
52
|
-
|
|
53
|
-
def extract_model(self) -> str | None:
|
|
54
|
-
gopro_info = self._extract_gopro_info()
|
|
55
|
-
if gopro_info is None:
|
|
56
|
-
return None
|
|
57
|
-
|
|
58
|
-
return gopro_info.model
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import logging
|
|
3
|
-
import typing as T
|
|
4
|
-
|
|
5
|
-
from ... import geo, telemetry
|
|
6
|
-
from ...geotag import geotag_images_from_gpx_file
|
|
7
|
-
from .base_parser import BaseParser
|
|
8
|
-
from .generic_video_parser import GenericVideoParser
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
LOG = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class GpxParser(BaseParser):
|
|
15
|
-
default_source_pattern = "%g.gpx"
|
|
16
|
-
parser_label = "gpx"
|
|
17
|
-
|
|
18
|
-
def extract_points(self) -> T.Sequence[geo.Point]:
|
|
19
|
-
path = self.geotag_source_path
|
|
20
|
-
if not path:
|
|
21
|
-
return []
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
gpx_tracks = geotag_images_from_gpx_file.parse_gpx(path)
|
|
25
|
-
except Exception as ex:
|
|
26
|
-
raise RuntimeError(
|
|
27
|
-
f"Error parsing GPX {path}: {ex.__class__.__name__}: {ex}"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
if 1 < len(gpx_tracks):
|
|
31
|
-
LOG.warning(
|
|
32
|
-
"Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
|
|
33
|
-
len(gpx_tracks),
|
|
34
|
-
self.videoPath,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
gpx_points: T.Sequence[geo.Point] = sum(gpx_tracks, [])
|
|
38
|
-
if not gpx_points:
|
|
39
|
-
return gpx_points
|
|
40
|
-
|
|
41
|
-
offset = self._synx_gpx_by_first_gps_timestamp(gpx_points)
|
|
42
|
-
|
|
43
|
-
self._rebase_times(gpx_points, offset=offset)
|
|
44
|
-
|
|
45
|
-
return gpx_points
|
|
46
|
-
|
|
47
|
-
def _synx_gpx_by_first_gps_timestamp(
|
|
48
|
-
self, gpx_points: T.Sequence[geo.Point]
|
|
49
|
-
) -> float:
|
|
50
|
-
offset: float = 0.0
|
|
51
|
-
|
|
52
|
-
if not gpx_points:
|
|
53
|
-
return offset
|
|
54
|
-
|
|
55
|
-
first_gpx_dt = datetime.datetime.fromtimestamp(
|
|
56
|
-
gpx_points[0].time, tz=datetime.timezone.utc
|
|
57
|
-
)
|
|
58
|
-
LOG.info("First GPX timestamp: %s", first_gpx_dt)
|
|
59
|
-
|
|
60
|
-
# Extract first GPS timestamp (if found) for synchronization
|
|
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:
|
|
66
|
-
LOG.warning(
|
|
67
|
-
"Skip GPX synchronization because no GPS found in video %s",
|
|
68
|
-
self.videoPath,
|
|
69
|
-
)
|
|
70
|
-
return offset
|
|
71
|
-
|
|
72
|
-
first_gps_point = gps_points[0]
|
|
73
|
-
if isinstance(first_gps_point, telemetry.GPSPoint):
|
|
74
|
-
if first_gps_point.epoch_time is not None:
|
|
75
|
-
first_gps_dt = datetime.datetime.fromtimestamp(
|
|
76
|
-
first_gps_point.epoch_time, tz=datetime.timezone.utc
|
|
77
|
-
)
|
|
78
|
-
LOG.info("First GPS timestamp: %s", first_gps_dt)
|
|
79
|
-
offset = gpx_points[0].time - first_gps_point.epoch_time
|
|
80
|
-
if offset:
|
|
81
|
-
LOG.warning(
|
|
82
|
-
"Found offset between GPX %s and video GPS timestamps %s: %s seconds",
|
|
83
|
-
first_gpx_dt,
|
|
84
|
-
first_gps_dt,
|
|
85
|
-
offset,
|
|
86
|
-
)
|
|
87
|
-
else:
|
|
88
|
-
LOG.info(
|
|
89
|
-
"GPX and GPS are perfectly synchronized (all starts from %s)",
|
|
90
|
-
first_gpx_dt,
|
|
91
|
-
)
|
|
92
|
-
else:
|
|
93
|
-
LOG.warning(
|
|
94
|
-
"Skip GPX synchronization because no GPS epoch time found in video %s",
|
|
95
|
-
self.videoPath,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
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()
|