mapillary-tools 0.13.0__tar.gz → 0.13.1__tar.gz
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-0.13.0/mapillary_tools.egg-info → mapillary_tools-0.13.1}/PKG-INFO +1 -1
- mapillary_tools-0.13.1/mapillary_tools/__init__.py +1 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/camm/camm_builder.py +14 -62
- mapillary_tools-0.13.1/mapillary_tools/camm/camm_parser.py +561 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +7 -1
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gpmf_parser.py +28 -5
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/telemetry.py +20 -8
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/upload.py +2 -3
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/camm_parser.py +8 -12
- mapillary_tools-0.13.1/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +108 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1/mapillary_tools.egg-info}/PKG-INFO +1 -1
- mapillary_tools-0.13.0/mapillary_tools/__init__.py +0 -1
- mapillary_tools-0.13.0/mapillary_tools/camm/camm_parser.py +0 -342
- mapillary_tools-0.13.0/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -71
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/LICENSE +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/MANIFEST.in +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/README.md +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/api_v4.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/authenticate.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/__init__.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/__main__.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/authenticate.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/process.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/process_and_upload.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/sample_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/upload.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/video_process_and_upload.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/config.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/constants.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exceptions.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exif_read.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exif_write.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exiftool_read.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/exiftool_read_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/ffmpeg.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geo.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/__init__.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/blackvue_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_from_generic.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/gps_filter.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/geotag/utils.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/history.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/construct_mp4_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/io_utils.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/process_geotag_properties.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/process_sequence_properties.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/sample_video.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/types.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/upload_api_v4.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/uploader.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/utils.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/cli_options.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extract_video_data.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/base_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/SOURCES.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/requires.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/requirements.txt +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/schema/image_description_schema.json +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/setup.cfg +0 -0
- {mapillary_tools-0.13.0 → mapillary_tools-0.13.1}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.13.1"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import typing as T
|
|
3
3
|
|
|
4
|
-
from .. import geo,
|
|
4
|
+
from .. import geo, types
|
|
5
5
|
from ..mp4 import (
|
|
6
6
|
construct_mp4_parser as cparser,
|
|
7
7
|
mp4_sample_parser as sample_parser,
|
|
@@ -11,62 +11,11 @@ from ..mp4 import (
|
|
|
11
11
|
from . import camm_parser
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
TelemetryMeasurement
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _build_camm_sample(measurement: TelemetryMeasurement) -> bytes:
|
|
21
|
-
if isinstance(measurement, geo.Point):
|
|
22
|
-
return camm_parser.CAMMSampleData.build(
|
|
23
|
-
{
|
|
24
|
-
"type": camm_parser.CAMMType.MIN_GPS.value,
|
|
25
|
-
"data": [
|
|
26
|
-
measurement.lat,
|
|
27
|
-
measurement.lon,
|
|
28
|
-
-1.0 if measurement.alt is None else measurement.alt,
|
|
29
|
-
],
|
|
30
|
-
}
|
|
31
|
-
)
|
|
32
|
-
elif isinstance(measurement, telemetry.AccelerationData):
|
|
33
|
-
# Accelerometer reading in meters/second^2 along XYZ axes of the camera.
|
|
34
|
-
return camm_parser.CAMMSampleData.build(
|
|
35
|
-
{
|
|
36
|
-
"type": camm_parser.CAMMType.ACCELERATION.value,
|
|
37
|
-
"data": [
|
|
38
|
-
measurement.x,
|
|
39
|
-
measurement.y,
|
|
40
|
-
measurement.z,
|
|
41
|
-
],
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
elif isinstance(measurement, telemetry.GyroscopeData):
|
|
45
|
-
# Gyroscope signal in radians/seconds around XYZ axes of the camera. Rotation is positive in the counterclockwise direction.
|
|
46
|
-
return camm_parser.CAMMSampleData.build(
|
|
47
|
-
{
|
|
48
|
-
"type": camm_parser.CAMMType.GYRO.value,
|
|
49
|
-
"data": [
|
|
50
|
-
measurement.x,
|
|
51
|
-
measurement.y,
|
|
52
|
-
measurement.z,
|
|
53
|
-
],
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
elif isinstance(measurement, telemetry.MagnetometerData):
|
|
57
|
-
# Ambient magnetic field.
|
|
58
|
-
return camm_parser.CAMMSampleData.build(
|
|
59
|
-
{
|
|
60
|
-
"type": camm_parser.CAMMType.MAGNETIC_FIELD.value,
|
|
61
|
-
"data": [
|
|
62
|
-
measurement.x,
|
|
63
|
-
measurement.y,
|
|
64
|
-
measurement.z,
|
|
65
|
-
],
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
|
-
else:
|
|
69
|
-
raise ValueError(f"unexpected measurement type {type(measurement)}")
|
|
14
|
+
def _build_camm_sample(measurement: camm_parser.TelemetryMeasurement) -> bytes:
|
|
15
|
+
for sample_entry_cls in camm_parser.SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.values():
|
|
16
|
+
if sample_entry_cls.serializable(measurement):
|
|
17
|
+
return sample_entry_cls.serialize(measurement)
|
|
18
|
+
raise ValueError(f"Unsupported measurement type {type(measurement)}")
|
|
70
19
|
|
|
71
20
|
|
|
72
21
|
def _create_edit_list_from_points(
|
|
@@ -121,16 +70,19 @@ def _create_edit_list_from_points(
|
|
|
121
70
|
|
|
122
71
|
def _multiplex(
|
|
123
72
|
points: T.Sequence[geo.Point],
|
|
124
|
-
measurements: T.Optional[T.List[
|
|
125
|
-
) -> T.List[TelemetryMeasurement]:
|
|
126
|
-
mutiplexed: T.List[TelemetryMeasurement] = [
|
|
73
|
+
measurements: T.Optional[T.List[camm_parser.TelemetryMeasurement]] = None,
|
|
74
|
+
) -> T.List[camm_parser.TelemetryMeasurement]:
|
|
75
|
+
mutiplexed: T.List[camm_parser.TelemetryMeasurement] = [
|
|
76
|
+
*points,
|
|
77
|
+
*(measurements or []),
|
|
78
|
+
]
|
|
127
79
|
mutiplexed.sort(key=lambda m: m.time)
|
|
128
80
|
|
|
129
81
|
return mutiplexed
|
|
130
82
|
|
|
131
83
|
|
|
132
84
|
def convert_telemetry_to_raw_samples(
|
|
133
|
-
measurements: T.Sequence[TelemetryMeasurement],
|
|
85
|
+
measurements: T.Sequence[camm_parser.TelemetryMeasurement],
|
|
134
86
|
timescale: int,
|
|
135
87
|
) -> T.Generator[sample_parser.RawSample, None, None]:
|
|
136
88
|
for idx, measurement in enumerate(measurements):
|
|
@@ -283,7 +235,7 @@ def create_camm_trak(
|
|
|
283
235
|
|
|
284
236
|
def camm_sample_generator2(
|
|
285
237
|
video_metadata: types.VideoMetadata,
|
|
286
|
-
telemetry_measurements: T.Optional[T.List[
|
|
238
|
+
telemetry_measurements: T.Optional[T.List[camm_parser.TelemetryMeasurement]] = None,
|
|
287
239
|
):
|
|
288
240
|
def _f(
|
|
289
241
|
fp: T.BinaryIO,
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# pyre-ignore-all-errors[5, 11, 16, 21, 24, 58]
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import dataclasses
|
|
5
|
+
import io
|
|
6
|
+
import logging
|
|
7
|
+
import pathlib
|
|
8
|
+
import typing as T
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
import construct as C
|
|
12
|
+
|
|
13
|
+
from .. import geo, telemetry
|
|
14
|
+
from ..mp4 import simple_mp4_parser as sparser
|
|
15
|
+
from ..mp4.mp4_sample_parser import MovieBoxParser, Sample, TrackBoxParser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
LOG = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
TelemetryMeasurement = T.Union[
|
|
22
|
+
geo.Point,
|
|
23
|
+
telemetry.TelemetryMeasurement,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Camera Motion Metadata Spec https://developers.google.com/streetview/publish/camm-spec
|
|
28
|
+
class CAMMType(Enum):
|
|
29
|
+
ANGLE_AXIS = 0
|
|
30
|
+
EXPOSURE_TIME = 1
|
|
31
|
+
GYRO = 2
|
|
32
|
+
ACCELERATION = 3
|
|
33
|
+
POSITION = 4
|
|
34
|
+
MIN_GPS = 5
|
|
35
|
+
GPS = 6
|
|
36
|
+
MAGNETIC_FIELD = 7
|
|
37
|
+
|
|
38
|
+
# Mapillary extensions are offset by 1024
|
|
39
|
+
# GoPro GPS is not compatible with CAMMType.GPS,
|
|
40
|
+
# so we use a new type to represent it
|
|
41
|
+
MLY_GOPRO_GPS = 1024 + 6
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# All fields are little-endian
|
|
45
|
+
Float = C.Float32l
|
|
46
|
+
Double = C.Float64l
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
TTelemetry = T.TypeVar("TTelemetry", bound=TelemetryMeasurement)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CAMMSampleEntry(abc.ABC, T.Generic[TTelemetry]):
|
|
53
|
+
camm_type: CAMMType
|
|
54
|
+
|
|
55
|
+
construct: C.Struct
|
|
56
|
+
|
|
57
|
+
telemetry_cls: T.Type[TTelemetry]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def serializable(cls, data: T.Any, throw: bool = False) -> bool:
|
|
61
|
+
# Use "is" for exact type match, instead of isinstance
|
|
62
|
+
if type(data) is cls.telemetry_cls:
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
if throw:
|
|
66
|
+
raise TypeError(
|
|
67
|
+
f"{cls} can not serialize {type(data)}: expect {cls.telemetry_cls}"
|
|
68
|
+
)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
@abc.abstractmethod
|
|
73
|
+
def serialize(cls, data: TTelemetry) -> bytes:
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
@abc.abstractmethod
|
|
78
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> TTelemetry:
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MinGPSSampleEntry(CAMMSampleEntry):
|
|
83
|
+
camm_type = CAMMType.MIN_GPS
|
|
84
|
+
|
|
85
|
+
construct = Double[3] # type: ignore
|
|
86
|
+
|
|
87
|
+
telemetry_cls = geo.Point
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> geo.Point:
|
|
91
|
+
return geo.Point(
|
|
92
|
+
time=sample.exact_time,
|
|
93
|
+
lat=data[0],
|
|
94
|
+
lon=data[1],
|
|
95
|
+
alt=data[2],
|
|
96
|
+
angle=None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def serialize(cls, data: geo.Point) -> bytes:
|
|
101
|
+
cls.serializable(data, throw=True)
|
|
102
|
+
|
|
103
|
+
return CAMMSampleData.build(
|
|
104
|
+
{
|
|
105
|
+
"type": cls.camm_type.value,
|
|
106
|
+
"data": [
|
|
107
|
+
data.lat,
|
|
108
|
+
data.lon,
|
|
109
|
+
-1.0 if data.alt is None else data.alt,
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class GPSSampleEntry(CAMMSampleEntry):
|
|
116
|
+
camm_type: CAMMType = CAMMType.GPS
|
|
117
|
+
|
|
118
|
+
construct = C.Struct(
|
|
119
|
+
"time_gps_epoch" / Double, # type: ignore
|
|
120
|
+
"gps_fix_type" / C.Int32sl, # type: ignore
|
|
121
|
+
"latitude" / Double, # type: ignore
|
|
122
|
+
"longitude" / Double, # type: ignore
|
|
123
|
+
"altitude" / Float, # type: ignore
|
|
124
|
+
"horizontal_accuracy" / Float, # type: ignore
|
|
125
|
+
"vertical_accuracy" / Float, # type: ignore
|
|
126
|
+
"velocity_east" / Float, # type: ignore
|
|
127
|
+
"velocity_north" / Float, # type: ignore
|
|
128
|
+
"velocity_up" / Float, # type: ignore
|
|
129
|
+
"speed_accuracy" / Float, # type: ignore
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
telemetry_cls = telemetry.CAMMGPSPoint
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.CAMMGPSPoint:
|
|
136
|
+
return telemetry.CAMMGPSPoint(
|
|
137
|
+
time=sample.exact_time,
|
|
138
|
+
lat=data.latitude,
|
|
139
|
+
lon=data.longitude,
|
|
140
|
+
alt=data.altitude,
|
|
141
|
+
angle=None,
|
|
142
|
+
time_gps_epoch=data.time_gps_epoch,
|
|
143
|
+
gps_fix_type=data.gps_fix_type,
|
|
144
|
+
horizontal_accuracy=data.horizontal_accuracy,
|
|
145
|
+
vertical_accuracy=data.vertical_accuracy,
|
|
146
|
+
velocity_east=data.velocity_east,
|
|
147
|
+
velocity_north=data.velocity_north,
|
|
148
|
+
velocity_up=data.velocity_up,
|
|
149
|
+
speed_accuracy=data.speed_accuracy,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def serialize(cls, data: telemetry.CAMMGPSPoint) -> bytes:
|
|
154
|
+
cls.serializable(data, throw=True)
|
|
155
|
+
|
|
156
|
+
return CAMMSampleData.build(
|
|
157
|
+
{
|
|
158
|
+
"type": cls.camm_type.value,
|
|
159
|
+
"data": {
|
|
160
|
+
"time_gps_epoch": data.time_gps_epoch,
|
|
161
|
+
"gps_fix_type": data.gps_fix_type,
|
|
162
|
+
"latitude": data.lat,
|
|
163
|
+
"longitude": data.lon,
|
|
164
|
+
"altitude": -1.0 if data.alt is None else data.alt,
|
|
165
|
+
"horizontal_accuracy": data.horizontal_accuracy,
|
|
166
|
+
"vertical_accuracy": data.vertical_accuracy,
|
|
167
|
+
"velocity_east": data.velocity_east,
|
|
168
|
+
"velocity_north": data.velocity_north,
|
|
169
|
+
"velocity_up": data.velocity_up,
|
|
170
|
+
"speed_accuracy": data.speed_accuracy,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GoProGPSSampleEntry(CAMMSampleEntry):
|
|
177
|
+
camm_type: CAMMType = CAMMType.MLY_GOPRO_GPS
|
|
178
|
+
|
|
179
|
+
construct = C.Struct(
|
|
180
|
+
"latitude" / Double, # type: ignore
|
|
181
|
+
"longitude" / Double, # type: ignore
|
|
182
|
+
"altitude" / Float, # type: ignore
|
|
183
|
+
"epoch_time" / Double, # type: ignore
|
|
184
|
+
"fix" / C.Int32sl, # type: ignore
|
|
185
|
+
"precision" / Float, # type: ignore
|
|
186
|
+
"ground_speed" / Float, # type: ignore
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
telemetry_cls = telemetry.GPSPoint
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GPSPoint:
|
|
193
|
+
return telemetry.GPSPoint(
|
|
194
|
+
time=sample.exact_time,
|
|
195
|
+
lat=data.latitude,
|
|
196
|
+
lon=data.longitude,
|
|
197
|
+
alt=data.altitude,
|
|
198
|
+
angle=None,
|
|
199
|
+
epoch_time=data.epoch_time,
|
|
200
|
+
fix=telemetry.GPSFix(data.fix),
|
|
201
|
+
precision=data.precision,
|
|
202
|
+
ground_speed=data.ground_speed,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def serialize(cls, data: telemetry.GPSPoint) -> bytes:
|
|
207
|
+
cls.serializable(data, throw=True)
|
|
208
|
+
|
|
209
|
+
if data.fix is None:
|
|
210
|
+
gps_fix = telemetry.GPSFix.NO_FIX.value
|
|
211
|
+
else:
|
|
212
|
+
gps_fix = data.fix.value
|
|
213
|
+
|
|
214
|
+
return CAMMSampleData.build(
|
|
215
|
+
{
|
|
216
|
+
"type": cls.camm_type.value,
|
|
217
|
+
"data": {
|
|
218
|
+
"latitude": data.lat,
|
|
219
|
+
"longitude": data.lon,
|
|
220
|
+
"altitude": -1.0 if data.alt is None else data.alt,
|
|
221
|
+
"epoch_time": data.epoch_time,
|
|
222
|
+
"fix": gps_fix,
|
|
223
|
+
"precision": data.precision,
|
|
224
|
+
"ground_speed": data.ground_speed,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class AccelerationSampleEntry(CAMMSampleEntry):
|
|
231
|
+
camm_type: CAMMType = CAMMType.ACCELERATION
|
|
232
|
+
|
|
233
|
+
construct: C.Struct = Float[3] # type: ignore
|
|
234
|
+
|
|
235
|
+
telemetry_cls = telemetry.AccelerationData
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.AccelerationData:
|
|
239
|
+
return telemetry.AccelerationData(
|
|
240
|
+
time=sample.exact_time,
|
|
241
|
+
x=data[0],
|
|
242
|
+
y=data[1],
|
|
243
|
+
z=data[2],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def serialize(cls, data: telemetry.AccelerationData) -> bytes:
|
|
248
|
+
cls.serializable(data, throw=True)
|
|
249
|
+
|
|
250
|
+
return CAMMSampleData.build(
|
|
251
|
+
{
|
|
252
|
+
"type": cls.camm_type.value,
|
|
253
|
+
"data": [data.x, data.y, data.z],
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class GyroscopeSampleEntry(CAMMSampleEntry):
|
|
259
|
+
camm_type: CAMMType = CAMMType.GYRO
|
|
260
|
+
|
|
261
|
+
construct: C.Struct = Float[3] # type: ignore
|
|
262
|
+
|
|
263
|
+
telemetry_cls = telemetry.GyroscopeData
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.GyroscopeData:
|
|
267
|
+
return telemetry.GyroscopeData(
|
|
268
|
+
time=sample.exact_time,
|
|
269
|
+
x=data[0],
|
|
270
|
+
y=data[1],
|
|
271
|
+
z=data[2],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
def serialize(cls, data: telemetry.GyroscopeData) -> bytes:
|
|
276
|
+
cls.serializable(data)
|
|
277
|
+
|
|
278
|
+
return CAMMSampleData.build(
|
|
279
|
+
{
|
|
280
|
+
"type": cls.camm_type.value,
|
|
281
|
+
"data": [data.x, data.y, data.z],
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class MagnetometerSampleEntry(CAMMSampleEntry):
|
|
287
|
+
camm_type: CAMMType = CAMMType.MAGNETIC_FIELD
|
|
288
|
+
|
|
289
|
+
construct: C.Struct = Float[3] # type: ignore
|
|
290
|
+
|
|
291
|
+
telemetry_cls = telemetry.MagnetometerData
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def deserialize(cls, sample: Sample, data: T.Any) -> telemetry.MagnetometerData:
|
|
295
|
+
return telemetry.MagnetometerData(
|
|
296
|
+
time=sample.exact_time,
|
|
297
|
+
x=data[0],
|
|
298
|
+
y=data[1],
|
|
299
|
+
z=data[2],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def serialize(cls, data: telemetry.MagnetometerData) -> bytes:
|
|
304
|
+
cls.serializable(data)
|
|
305
|
+
|
|
306
|
+
return CAMMSampleData.build(
|
|
307
|
+
{
|
|
308
|
+
"type": cls.camm_type.value,
|
|
309
|
+
"data": [data.x, data.y, data.z],
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
SAMPLE_ENTRY_CLS_BY_CAMM_TYPE = {
|
|
315
|
+
sample_entry_cls.camm_type: sample_entry_cls
|
|
316
|
+
for sample_entry_cls in CAMMSampleEntry.__subclasses__()
|
|
317
|
+
}
|
|
318
|
+
assert len(SAMPLE_ENTRY_CLS_BY_CAMM_TYPE) == 6, SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.keys()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
_SWITCH: T.Dict[int, C.Struct] = {
|
|
322
|
+
# angle_axis
|
|
323
|
+
CAMMType.ANGLE_AXIS.value: Float[3], # type: ignore
|
|
324
|
+
CAMMType.EXPOSURE_TIME.value: C.Struct(
|
|
325
|
+
"pixel_exposure_time" / C.Int32sl, # type: ignore
|
|
326
|
+
"rolling_shutter_skew_time" / C.Int32sl, # type: ignore
|
|
327
|
+
),
|
|
328
|
+
# position
|
|
329
|
+
CAMMType.POSITION.value: Float[3], # type: ignore
|
|
330
|
+
**{t.value: cls.construct for t, cls in SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.items()},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
CAMMSampleData = C.Struct(
|
|
334
|
+
C.Padding(2),
|
|
335
|
+
"type" / C.Int16ul,
|
|
336
|
+
"data" / C.Switch(C.this.type, _SWITCH),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _parse_telemetry_from_sample(
|
|
341
|
+
fp: T.BinaryIO, sample: Sample
|
|
342
|
+
) -> T.Optional[TelemetryMeasurement]:
|
|
343
|
+
fp.seek(sample.raw_sample.offset, io.SEEK_SET)
|
|
344
|
+
data = fp.read(sample.raw_sample.size)
|
|
345
|
+
box = CAMMSampleData.parse(data)
|
|
346
|
+
|
|
347
|
+
camm_type = CAMMType(box.type) # type: ignore
|
|
348
|
+
SampleKlass = SAMPLE_ENTRY_CLS_BY_CAMM_TYPE.get(camm_type)
|
|
349
|
+
if SampleKlass is None:
|
|
350
|
+
return None
|
|
351
|
+
return SampleKlass.deserialize(sample, box.data)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _filter_telemetry_by_elst_segments(
|
|
355
|
+
measurements: T.Iterable[TelemetryMeasurement],
|
|
356
|
+
elst: T.Sequence[T.Tuple[float, float]],
|
|
357
|
+
) -> T.Generator[TelemetryMeasurement, None, None]:
|
|
358
|
+
empty_elst = [entry for entry in elst if entry[0] == -1]
|
|
359
|
+
if empty_elst:
|
|
360
|
+
offset = empty_elst[-1][1]
|
|
361
|
+
else:
|
|
362
|
+
offset = 0
|
|
363
|
+
|
|
364
|
+
elst = [entry for entry in elst if entry[0] != -1]
|
|
365
|
+
|
|
366
|
+
if not elst:
|
|
367
|
+
for m in measurements:
|
|
368
|
+
yield dataclasses.replace(m, time=m.time + offset)
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
elst.sort(key=lambda entry: entry[0])
|
|
372
|
+
elst_idx = 0
|
|
373
|
+
for m in measurements:
|
|
374
|
+
if len(elst) <= elst_idx:
|
|
375
|
+
break
|
|
376
|
+
media_time, duration = elst[elst_idx]
|
|
377
|
+
if m.time < media_time:
|
|
378
|
+
pass
|
|
379
|
+
elif m.time <= media_time + duration:
|
|
380
|
+
yield dataclasses.replace(m, time=m.time + offset)
|
|
381
|
+
else:
|
|
382
|
+
elst_idx += 1
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def elst_entry_to_seconds(
|
|
386
|
+
entry: T.Dict, movie_timescale: int, media_timescale: int
|
|
387
|
+
) -> T.Tuple[float, float]:
|
|
388
|
+
assert movie_timescale > 0, "expected positive movie_timescale"
|
|
389
|
+
assert media_timescale > 0, "expected positive media_timescale"
|
|
390
|
+
media_time, duration = entry["media_time"], entry["segment_duration"]
|
|
391
|
+
if media_time != -1:
|
|
392
|
+
media_time = media_time / media_timescale
|
|
393
|
+
duration = duration / movie_timescale
|
|
394
|
+
return (media_time, duration)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _is_camm_description(description: T.Dict) -> bool:
|
|
398
|
+
return description["format"] == b"camm"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _contains_camm_description(track: TrackBoxParser) -> bool:
|
|
402
|
+
descriptions = track.extract_sample_descriptions()
|
|
403
|
+
return any(_is_camm_description(d) for d in descriptions)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _filter_telemetry_by_track_elst(
|
|
407
|
+
moov: MovieBoxParser,
|
|
408
|
+
track: TrackBoxParser,
|
|
409
|
+
measurements: T.Iterable[TelemetryMeasurement],
|
|
410
|
+
) -> T.List[TelemetryMeasurement]:
|
|
411
|
+
elst_boxdata = track.extract_elst_boxdata()
|
|
412
|
+
|
|
413
|
+
if elst_boxdata is not None:
|
|
414
|
+
elst_entries = elst_boxdata["entries"]
|
|
415
|
+
if elst_entries:
|
|
416
|
+
# media_timescale
|
|
417
|
+
mdhd_boxdata = track.extract_mdhd_boxdata()
|
|
418
|
+
media_timescale = mdhd_boxdata["timescale"]
|
|
419
|
+
|
|
420
|
+
# movie_timescale
|
|
421
|
+
mvhd_boxdata = moov.extract_mvhd_boxdata()
|
|
422
|
+
movie_timescale = mvhd_boxdata["timescale"]
|
|
423
|
+
|
|
424
|
+
segments = [
|
|
425
|
+
elst_entry_to_seconds(
|
|
426
|
+
entry,
|
|
427
|
+
movie_timescale=movie_timescale,
|
|
428
|
+
media_timescale=media_timescale,
|
|
429
|
+
)
|
|
430
|
+
for entry in elst_entries
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
return list(_filter_telemetry_by_elst_segments(measurements, segments))
|
|
434
|
+
|
|
435
|
+
return list(measurements)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def extract_points(fp: T.BinaryIO) -> T.Optional[T.List[geo.Point]]:
|
|
439
|
+
"""
|
|
440
|
+
Return a list of points (could be empty) if it is a valid CAMM video,
|
|
441
|
+
otherwise None
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
moov = MovieBoxParser.parse_stream(fp)
|
|
445
|
+
|
|
446
|
+
for track in moov.extract_tracks():
|
|
447
|
+
if _contains_camm_description(track):
|
|
448
|
+
maybe_measurements = (
|
|
449
|
+
_parse_telemetry_from_sample(fp, sample)
|
|
450
|
+
for sample in track.extract_samples()
|
|
451
|
+
if _is_camm_description(sample.description)
|
|
452
|
+
)
|
|
453
|
+
points = [m for m in maybe_measurements if isinstance(m, geo.Point)]
|
|
454
|
+
|
|
455
|
+
return T.cast(
|
|
456
|
+
T.List[geo.Point], _filter_telemetry_by_track_elst(moov, track, points)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def extract_telemetry_data(fp: T.BinaryIO) -> T.Optional[T.List[TelemetryMeasurement]]:
|
|
463
|
+
moov = MovieBoxParser.parse_stream(fp)
|
|
464
|
+
|
|
465
|
+
for track in moov.extract_tracks():
|
|
466
|
+
if _contains_camm_description(track):
|
|
467
|
+
maybe_measurements = (
|
|
468
|
+
_parse_telemetry_from_sample(fp, sample)
|
|
469
|
+
for sample in track.extract_samples()
|
|
470
|
+
if _is_camm_description(sample.description)
|
|
471
|
+
)
|
|
472
|
+
measurements = [m for m in maybe_measurements if m is not None]
|
|
473
|
+
|
|
474
|
+
measurements = _filter_telemetry_by_track_elst(moov, track, measurements)
|
|
475
|
+
|
|
476
|
+
return measurements
|
|
477
|
+
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def parse_gpx(path: pathlib.Path) -> T.List[geo.Point]:
|
|
482
|
+
with path.open("rb") as fp:
|
|
483
|
+
points = extract_points(fp)
|
|
484
|
+
if points is None:
|
|
485
|
+
return []
|
|
486
|
+
return points
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
MakeOrModel = C.Struct(
|
|
490
|
+
"size" / C.Int16ub,
|
|
491
|
+
C.Padding(2),
|
|
492
|
+
"data" / C.FixedSized(C.this.size, C.GreedyBytes),
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _decode_quietly(data: bytes, h: sparser.Header) -> str:
|
|
497
|
+
try:
|
|
498
|
+
return data.decode("utf-8")
|
|
499
|
+
except UnicodeDecodeError:
|
|
500
|
+
LOG.warning("Failed to decode %s: %s", h, data[:512])
|
|
501
|
+
return ""
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _parse_quietly(data: bytes, h: sparser.Header) -> bytes:
|
|
505
|
+
try:
|
|
506
|
+
parsed = MakeOrModel.parse(data)
|
|
507
|
+
except C.ConstructError:
|
|
508
|
+
LOG.warning("Failed to parse %s: %s", h, data[:512])
|
|
509
|
+
return b""
|
|
510
|
+
return parsed["data"]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def extract_camera_make_and_model(fp: T.BinaryIO) -> T.Tuple[str, str]:
|
|
514
|
+
header_and_stream = sparser.parse_path(
|
|
515
|
+
fp,
|
|
516
|
+
[
|
|
517
|
+
b"moov",
|
|
518
|
+
b"udta",
|
|
519
|
+
[
|
|
520
|
+
# Insta360 Titan
|
|
521
|
+
b"\xa9mak",
|
|
522
|
+
b"\xa9mod",
|
|
523
|
+
# RICHO THETA V
|
|
524
|
+
b"@mod",
|
|
525
|
+
b"@mak",
|
|
526
|
+
# RICHO THETA V
|
|
527
|
+
b"manu",
|
|
528
|
+
b"modl",
|
|
529
|
+
],
|
|
530
|
+
],
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
make: T.Optional[str] = None
|
|
534
|
+
model: T.Optional[str] = None
|
|
535
|
+
|
|
536
|
+
try:
|
|
537
|
+
for h, s in header_and_stream:
|
|
538
|
+
data = s.read(h.maxsize)
|
|
539
|
+
if h.type == b"\xa9mak":
|
|
540
|
+
make_data = _parse_quietly(data, h)
|
|
541
|
+
make_data = make_data.rstrip(b"\x00")
|
|
542
|
+
make = _decode_quietly(make_data, h)
|
|
543
|
+
elif h.type == b"\xa9mod":
|
|
544
|
+
model_data = _parse_quietly(data, h)
|
|
545
|
+
model_data = model_data.rstrip(b"\x00")
|
|
546
|
+
model = _decode_quietly(model_data, h)
|
|
547
|
+
elif h.type in [b"@mak", b"manu"]:
|
|
548
|
+
make = _decode_quietly(data, h)
|
|
549
|
+
elif h.type in [b"@mod", b"modl"]:
|
|
550
|
+
model = _decode_quietly(data, h)
|
|
551
|
+
# quit when both found
|
|
552
|
+
if make and model:
|
|
553
|
+
break
|
|
554
|
+
except sparser.ParsingError:
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
if make:
|
|
558
|
+
make = make.strip()
|
|
559
|
+
if model:
|
|
560
|
+
model = model.strip()
|
|
561
|
+
return make or "", model or ""
|
|
@@ -25,7 +25,13 @@ class GeotagImagesFromGPXFile(GeotagImagesFromGeneric):
|
|
|
25
25
|
num_processes: T.Optional[int] = None,
|
|
26
26
|
):
|
|
27
27
|
super().__init__()
|
|
28
|
-
|
|
28
|
+
try:
|
|
29
|
+
tracks = parse_gpx(source_path)
|
|
30
|
+
except Exception as ex:
|
|
31
|
+
raise RuntimeError(
|
|
32
|
+
f"Error parsing GPX {source_path}: {ex.__class__.__name__}: {ex}"
|
|
33
|
+
)
|
|
34
|
+
|
|
29
35
|
if 1 < len(tracks):
|
|
30
36
|
LOG.warning(
|
|
31
37
|
"Found %s tracks in the GPX file %s. Will merge points in all the tracks as a single track for interpolation",
|