mapillary-tools 0.12.0__tar.gz → 0.13.0__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.12.0/mapillary_tools.egg-info → mapillary_tools-0.13.0}/PKG-INFO +11 -4
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/README.md +1 -1
- mapillary_tools-0.13.0/mapillary_tools/__init__.py +1 -0
- mapillary_tools-0.13.0/mapillary_tools/api_v4.py +151 -0
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/camm}/camm_builder.py +122 -62
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/camm}/camm_parser.py +120 -84
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/__init__.py +0 -1
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/__main__.py +0 -6
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/process.py +0 -50
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/upload.py +1 -26
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/constants.py +2 -2
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/exiftool_read_video.py +13 -11
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/ffmpeg.py +2 -2
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geo.py +0 -54
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/blackvue_parser.py +4 -4
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_exif.py +2 -1
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_exiftool_both_image_and_video.py +0 -1
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_videos_from_exiftool_video.py +5 -3
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_videos_from_video.py +13 -14
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/gpmf_gps_filter.py +9 -10
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/gpmf_parser.py +318 -78
- mapillary_tools-0.13.0/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/mp4}/construct_mp4_parser.py +32 -16
- mapillary_tools-0.13.0/mapillary_tools/mp4/mp4_sample_parser.py +322 -0
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/mp4}/simple_mp4_builder.py +64 -38
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/process_geotag_properties.py +25 -19
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/process_sequence_properties.py +6 -6
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/sample_video.py +17 -16
- mapillary_tools-0.13.0/mapillary_tools/telemetry.py +59 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/types.py +18 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/upload.py +75 -233
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/upload_api_v4.py +8 -16
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/utils.py +9 -16
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/cli_options.py +0 -1
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extract_video_data.py +13 -31
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/base_parser.py +13 -11
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/blackvue_parser.py +5 -4
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/camm_parser.py +7 -6
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/exiftool_runtime_parser.py +4 -9
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/exiftool_xml_parser.py +9 -11
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/generic_video_parser.py +6 -11
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/gopro_parser.py +11 -4
- mapillary_tools-0.13.0/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +71 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/video_data_extraction/extractors/nmea_parser.py +3 -3
- mapillary_tools-0.13.0/mapillary_tools/video_data_extraction/video_data_parser_factory.py +39 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0/mapillary_tools.egg-info}/PKG-INFO +11 -4
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools.egg-info/SOURCES.txt +9 -11
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools.egg-info/requires.txt +0 -3
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/requirements.txt +0 -1
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/schema/image_description_schema.json +14 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/setup.py +2 -0
- mapillary_tools-0.12.0/mapillary_tools/__init__.py +0 -1
- mapillary_tools-0.12.0/mapillary_tools/api_v4.py +0 -68
- mapillary_tools-0.12.0/mapillary_tools/commands/upload_blackvue.py +0 -33
- mapillary_tools-0.12.0/mapillary_tools/commands/upload_camm.py +0 -33
- mapillary_tools-0.12.0/mapillary_tools/commands/upload_zip.py +0 -33
- mapillary_tools-0.12.0/mapillary_tools/geotag/mp4_sample_parser.py +0 -426
- mapillary_tools-0.12.0/mapillary_tools/process_import_meta_properties.py +0 -76
- mapillary_tools-0.12.0/mapillary_tools/video_data_extraction/extractors/gpx_parser.py +0 -29
- mapillary_tools-0.12.0/mapillary_tools/video_data_extraction/video_data_parser_factory.py +0 -46
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/LICENSE +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/MANIFEST.in +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/authenticate.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/authenticate.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/process_and_upload.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/sample_video.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/video_process_and_upload.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/config.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/exceptions.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/exif_read.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/exif_write.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/exiftool_read.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/__init__.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_from_generic.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/gps_filter.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/geotag/utils.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/history.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/mp4}/io_utils.py +0 -0
- {mapillary_tools-0.12.0/mapillary_tools/geotag → mapillary_tools-0.13.0/mapillary_tools/mp4}/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools/uploader.py +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.12.0 → mapillary_tools-0.13.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: mapillary_tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Mapillary Image/Video Import Pipeline
|
|
5
5
|
Home-page: https://github.com/mapillary/mapillary_tools
|
|
6
6
|
Author: Mapillary
|
|
@@ -18,7 +18,14 @@ Requires-Dist: requests[socks]<3.0.0,>=2.20.0
|
|
|
18
18
|
Requires-Dist: tqdm<5.0,>=4.0
|
|
19
19
|
Requires-Dist: typing_extensions
|
|
20
20
|
Requires-Dist: jsonschema~=4.17.3
|
|
21
|
-
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
22
29
|
|
|
23
30
|
<p align="center">
|
|
24
31
|
<a href="https://github.com/mapillary/mapillary_tools/">
|
|
@@ -82,7 +89,7 @@ mapillary_tools supports JPG/JEPG images (.jpg, .jpeg), with the following EXIF
|
|
|
82
89
|
mapillary_tools supports videos (.mp4, .360) that contain any of the following telemetry structures:
|
|
83
90
|
|
|
84
91
|
- [GPMF](https://github.com/gopro/gpmf-parser): mostly GoPro videos
|
|
85
|
-
- [GoPro HERO series](https://gopro.com/en/us/shop/cameras/hero11-black/CHDHX-111-master.html) (from 5 to
|
|
92
|
+
- [GoPro HERO series](https://gopro.com/en/us/shop/cameras/hero11-black/CHDHX-111-master.html) (from 5 to 13)
|
|
86
93
|
- [GoPro MAX](https://gopro.com/en/us/shop/cameras/max/CHDHZ-202-master.html)
|
|
87
94
|
- [CAMM](https://developers.google.com/streetview/publish/camm-spec): an open-standard telemetry spec supported by a number of cameras
|
|
88
95
|
- [Insta360 Pro2](https://www.insta360.com/product/insta360-pro2/)
|
|
@@ -60,7 +60,7 @@ mapillary_tools supports JPG/JEPG images (.jpg, .jpeg), with the following EXIF
|
|
|
60
60
|
mapillary_tools supports videos (.mp4, .360) that contain any of the following telemetry structures:
|
|
61
61
|
|
|
62
62
|
- [GPMF](https://github.com/gopro/gpmf-parser): mostly GoPro videos
|
|
63
|
-
- [GoPro HERO series](https://gopro.com/en/us/shop/cameras/hero11-black/CHDHX-111-master.html) (from 5 to
|
|
63
|
+
- [GoPro HERO series](https://gopro.com/en/us/shop/cameras/hero11-black/CHDHX-111-master.html) (from 5 to 13)
|
|
64
64
|
- [GoPro MAX](https://gopro.com/en/us/shop/cameras/max/CHDHZ-202-master.html)
|
|
65
65
|
- [CAMM](https://developers.google.com/streetview/publish/camm-spec): an open-standard telemetry spec supported by a number of cameras
|
|
66
66
|
- [Insta360 Pro2](https://www.insta360.com/product/insta360-pro2/)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.13.0"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import ssl
|
|
4
|
+
import typing as T
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
|
|
9
|
+
LOG = logging.getLogger(__name__)
|
|
10
|
+
MAPILLARY_CLIENT_TOKEN = os.getenv(
|
|
11
|
+
"MAPILLARY_CLIENT_TOKEN", "MLY|5675152195860640|6b02c72e6e3c801e5603ab0495623282"
|
|
12
|
+
)
|
|
13
|
+
MAPILLARY_GRAPH_API_ENDPOINT = os.getenv(
|
|
14
|
+
"MAPILLARY_GRAPH_API_ENDPOINT", "https://graph.mapillary.com"
|
|
15
|
+
)
|
|
16
|
+
REQUESTS_TIMEOUT = 60 # 1 minutes
|
|
17
|
+
USE_SYSTEM_CERTS: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HTTPSystemCertsAdapter(HTTPAdapter):
|
|
21
|
+
"""
|
|
22
|
+
This adapter uses the system's certificate store instead of the certifi module.
|
|
23
|
+
|
|
24
|
+
The implementation is based on the project https://pypi.org/project/pip-system-certs/,
|
|
25
|
+
which has a system-wide effect.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def init_poolmanager(self, *args, **kwargs):
|
|
29
|
+
ssl_context = ssl.create_default_context()
|
|
30
|
+
ssl_context.load_default_certs()
|
|
31
|
+
kwargs["ssl_context"] = ssl_context
|
|
32
|
+
|
|
33
|
+
super().init_poolmanager(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def cert_verify(self, *args, **kwargs):
|
|
36
|
+
super().cert_verify(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
# By default Python requests uses the ca_certs from the certifi module
|
|
39
|
+
# But we want to use the certificate store instead.
|
|
40
|
+
# By clearing the ca_certs variable we force it to fall back on that behaviour (handled in urllib3)
|
|
41
|
+
if "conn" in kwargs:
|
|
42
|
+
conn = kwargs["conn"]
|
|
43
|
+
else:
|
|
44
|
+
conn = args[0]
|
|
45
|
+
|
|
46
|
+
conn.ca_certs = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def request_post(
|
|
50
|
+
url: str,
|
|
51
|
+
data: T.Optional[T.Any] = None,
|
|
52
|
+
json: T.Optional[dict] = None,
|
|
53
|
+
**kwargs,
|
|
54
|
+
) -> requests.Response:
|
|
55
|
+
global USE_SYSTEM_CERTS
|
|
56
|
+
|
|
57
|
+
if USE_SYSTEM_CERTS:
|
|
58
|
+
with requests.Session() as session:
|
|
59
|
+
session.mount("https://", HTTPSystemCertsAdapter())
|
|
60
|
+
return session.post(url, data=data, json=json, **kwargs)
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
try:
|
|
64
|
+
return requests.post(url, data=data, json=json, **kwargs)
|
|
65
|
+
except requests.exceptions.SSLError as ex:
|
|
66
|
+
if "SSLCertVerificationError" not in str(ex):
|
|
67
|
+
raise ex
|
|
68
|
+
USE_SYSTEM_CERTS = True
|
|
69
|
+
# HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
|
|
70
|
+
LOG.warning(
|
|
71
|
+
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
72
|
+
)
|
|
73
|
+
with requests.Session() as session:
|
|
74
|
+
session.mount("https://", HTTPSystemCertsAdapter())
|
|
75
|
+
return session.post(url, data=data, json=json, **kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def request_get(
|
|
79
|
+
url: str,
|
|
80
|
+
params: T.Optional[dict] = None,
|
|
81
|
+
**kwargs,
|
|
82
|
+
) -> requests.Response:
|
|
83
|
+
global USE_SYSTEM_CERTS
|
|
84
|
+
|
|
85
|
+
if USE_SYSTEM_CERTS:
|
|
86
|
+
with requests.Session() as session:
|
|
87
|
+
session.mount("https://", HTTPSystemCertsAdapter())
|
|
88
|
+
return session.get(url, params=params, **kwargs)
|
|
89
|
+
else:
|
|
90
|
+
try:
|
|
91
|
+
return requests.get(url, params=params, **kwargs)
|
|
92
|
+
except requests.exceptions.SSLError as ex:
|
|
93
|
+
if "SSLCertVerificationError" not in str(ex):
|
|
94
|
+
raise ex
|
|
95
|
+
USE_SYSTEM_CERTS = True
|
|
96
|
+
# HTTPSConnectionPool(host='graph.mapillary.com', port=443): Max retries exceeded with url: /login (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1018)')))
|
|
97
|
+
LOG.warning(
|
|
98
|
+
"SSL error occurred, falling back to system SSL certificates: %s", ex
|
|
99
|
+
)
|
|
100
|
+
with requests.Session() as session:
|
|
101
|
+
session.mount("https://", HTTPSystemCertsAdapter())
|
|
102
|
+
return session.get(url, params=params, **kwargs)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_upload_token(email: str, password: str) -> requests.Response:
|
|
106
|
+
resp = request_post(
|
|
107
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/login",
|
|
108
|
+
params={"access_token": MAPILLARY_CLIENT_TOKEN},
|
|
109
|
+
json={"email": email, "password": password, "locale": "en_US"},
|
|
110
|
+
timeout=REQUESTS_TIMEOUT,
|
|
111
|
+
)
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
return resp
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def fetch_organization(
|
|
117
|
+
user_access_token: str, organization_id: T.Union[int, str]
|
|
118
|
+
) -> requests.Response:
|
|
119
|
+
resp = request_get(
|
|
120
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/{organization_id}",
|
|
121
|
+
params={
|
|
122
|
+
"fields": ",".join(["slug", "description", "name"]),
|
|
123
|
+
},
|
|
124
|
+
headers={
|
|
125
|
+
"Authorization": f"OAuth {user_access_token}",
|
|
126
|
+
},
|
|
127
|
+
timeout=REQUESTS_TIMEOUT,
|
|
128
|
+
)
|
|
129
|
+
resp.raise_for_status()
|
|
130
|
+
return resp
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
ActionType = T.Literal[
|
|
134
|
+
"upload_started_upload", "upload_finished_upload", "upload_failed_upload"
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def log_event(action_type: ActionType, properties: T.Dict) -> requests.Response:
|
|
139
|
+
resp = request_post(
|
|
140
|
+
f"{MAPILLARY_GRAPH_API_ENDPOINT}/logging",
|
|
141
|
+
json={
|
|
142
|
+
"action_type": action_type,
|
|
143
|
+
"properties": properties,
|
|
144
|
+
},
|
|
145
|
+
headers={
|
|
146
|
+
"Authorization": f"OAuth {MAPILLARY_CLIENT_TOKEN}",
|
|
147
|
+
},
|
|
148
|
+
timeout=REQUESTS_TIMEOUT,
|
|
149
|
+
)
|
|
150
|
+
resp.raise_for_status()
|
|
151
|
+
return resp
|
|
@@ -1,61 +1,98 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import typing as T
|
|
3
3
|
|
|
4
|
-
from .. import geo, types
|
|
5
|
-
|
|
6
|
-
from . import (
|
|
7
|
-
camm_parser,
|
|
4
|
+
from .. import geo, telemetry, types
|
|
5
|
+
from ..mp4 import (
|
|
8
6
|
construct_mp4_parser as cparser,
|
|
9
7
|
mp4_sample_parser as sample_parser,
|
|
10
8
|
simple_mp4_builder as builder,
|
|
11
9
|
)
|
|
12
|
-
from .simple_mp4_builder import BoxDict
|
|
13
10
|
|
|
11
|
+
from . import camm_parser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TelemetryMeasurement = T.Union[
|
|
15
|
+
geo.Point,
|
|
16
|
+
telemetry.TelemetryMeasurement,
|
|
17
|
+
]
|
|
14
18
|
|
|
15
|
-
def build_camm_sample(point: geo.Point) -> bytes:
|
|
16
|
-
return camm_parser.CAMMSampleData.build(
|
|
17
|
-
{
|
|
18
|
-
"type": camm_parser.CAMMType.MIN_GPS.value,
|
|
19
|
-
"data": [
|
|
20
|
-
point.lat,
|
|
21
|
-
point.lon,
|
|
22
|
-
-1.0 if point.alt is None else point.alt,
|
|
23
|
-
],
|
|
24
|
-
}
|
|
25
|
-
)
|
|
26
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)}")
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
|
|
72
|
+
def _create_edit_list_from_points(
|
|
29
73
|
point_segments: T.Sequence[T.Sequence[geo.Point]],
|
|
30
74
|
movie_timescale: int,
|
|
31
75
|
media_timescale: int,
|
|
32
|
-
) -> BoxDict:
|
|
76
|
+
) -> builder.BoxDict:
|
|
33
77
|
entries: T.List[T.Dict] = []
|
|
34
78
|
|
|
35
|
-
|
|
36
|
-
if not points:
|
|
37
|
-
entries = [
|
|
38
|
-
{
|
|
39
|
-
"media_time": 0,
|
|
40
|
-
"segment_duration": 0,
|
|
41
|
-
"media_rate_integer": 1,
|
|
42
|
-
"media_rate_fraction": 0,
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
break
|
|
79
|
+
non_empty_point_segments = [points for points in point_segments if points]
|
|
46
80
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
for idx, points in enumerate(non_empty_point_segments):
|
|
82
|
+
assert 0 <= points[0].time, (
|
|
83
|
+
f"expect non-negative point time but got {points[0]}"
|
|
84
|
+
)
|
|
85
|
+
assert points[0].time <= points[-1].time, (
|
|
86
|
+
f"expect points to be sorted but got first point {points[0]} and last point {points[-1]}"
|
|
87
|
+
)
|
|
53
88
|
|
|
54
89
|
if idx == 0:
|
|
55
90
|
if 0 < points[0].time:
|
|
56
91
|
segment_duration = int(points[0].time * movie_timescale)
|
|
92
|
+
# put an empty edit list entry to skip the initial gap
|
|
57
93
|
entries.append(
|
|
58
94
|
{
|
|
95
|
+
# If this field is set to –1, it is an empty edit
|
|
59
96
|
"media_time": -1,
|
|
60
97
|
"segment_duration": segment_duration,
|
|
61
98
|
"media_rate_integer": 1,
|
|
@@ -63,7 +100,6 @@ def _create_edit_list(
|
|
|
63
100
|
}
|
|
64
101
|
)
|
|
65
102
|
else:
|
|
66
|
-
assert point_segments[-1][-1].time <= points[0].time
|
|
67
103
|
media_time = int(points[0].time * media_timescale)
|
|
68
104
|
segment_duration = int((points[-1].time - points[0].time) * movie_timescale)
|
|
69
105
|
entries.append(
|
|
@@ -83,19 +119,31 @@ def _create_edit_list(
|
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
|
|
86
|
-
def
|
|
87
|
-
points: T.Sequence[geo.Point],
|
|
122
|
+
def _multiplex(
|
|
123
|
+
points: T.Sequence[geo.Point],
|
|
124
|
+
measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
|
|
125
|
+
) -> T.List[TelemetryMeasurement]:
|
|
126
|
+
mutiplexed: T.List[TelemetryMeasurement] = [*points, *(measurements or [])]
|
|
127
|
+
mutiplexed.sort(key=lambda m: m.time)
|
|
128
|
+
|
|
129
|
+
return mutiplexed
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def convert_telemetry_to_raw_samples(
|
|
133
|
+
measurements: T.Sequence[TelemetryMeasurement],
|
|
134
|
+
timescale: int,
|
|
88
135
|
) -> T.Generator[sample_parser.RawSample, None, None]:
|
|
89
|
-
for idx,
|
|
90
|
-
camm_sample_data =
|
|
136
|
+
for idx, measurement in enumerate(measurements):
|
|
137
|
+
camm_sample_data = _build_camm_sample(measurement)
|
|
91
138
|
|
|
92
|
-
if idx + 1 < len(
|
|
93
|
-
timedelta = int((
|
|
139
|
+
if idx + 1 < len(measurements):
|
|
140
|
+
timedelta = int((measurements[idx + 1].time - measurement.time) * timescale)
|
|
94
141
|
else:
|
|
95
142
|
timedelta = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
143
|
+
|
|
144
|
+
assert 0 <= timedelta <= builder.UINT32_MAX, (
|
|
145
|
+
f"expected timedelta {timedelta} between {measurements[idx]} and {measurements[idx + 1]} with timescale {timescale} to be <= UINT32_MAX"
|
|
146
|
+
)
|
|
99
147
|
|
|
100
148
|
yield sample_parser.RawSample(
|
|
101
149
|
# will update later
|
|
@@ -114,7 +162,9 @@ _STBLChildrenBuilderConstruct = cparser.Box32ConstructBuilder(
|
|
|
114
162
|
)
|
|
115
163
|
|
|
116
164
|
|
|
117
|
-
def _create_camm_stbl(
|
|
165
|
+
def _create_camm_stbl(
|
|
166
|
+
raw_samples: T.Iterable[sample_parser.RawSample],
|
|
167
|
+
) -> builder.BoxDict:
|
|
118
168
|
descriptions = [
|
|
119
169
|
{
|
|
120
170
|
"format": b"camm",
|
|
@@ -135,10 +185,10 @@ def _create_camm_stbl(raw_samples: T.Iterable[sample_parser.RawSample]) -> BoxDi
|
|
|
135
185
|
def create_camm_trak(
|
|
136
186
|
raw_samples: T.Sequence[sample_parser.RawSample],
|
|
137
187
|
media_timescale: int,
|
|
138
|
-
) -> BoxDict:
|
|
188
|
+
) -> builder.BoxDict:
|
|
139
189
|
stbl = _create_camm_stbl(raw_samples)
|
|
140
190
|
|
|
141
|
-
hdlr: BoxDict = {
|
|
191
|
+
hdlr: builder.BoxDict = {
|
|
142
192
|
"type": b"hdlr",
|
|
143
193
|
"data": {
|
|
144
194
|
"handler_type": b"camm",
|
|
@@ -150,7 +200,7 @@ def create_camm_trak(
|
|
|
150
200
|
assert media_timescale <= builder.UINT64_MAX
|
|
151
201
|
|
|
152
202
|
# Media Header Box
|
|
153
|
-
mdhd: BoxDict = {
|
|
203
|
+
mdhd: builder.BoxDict = {
|
|
154
204
|
"type": b"mdhd",
|
|
155
205
|
"data": {
|
|
156
206
|
# use 64-bit version
|
|
@@ -166,7 +216,7 @@ def create_camm_trak(
|
|
|
166
216
|
},
|
|
167
217
|
}
|
|
168
218
|
|
|
169
|
-
dinf: BoxDict = {
|
|
219
|
+
dinf: builder.BoxDict = {
|
|
170
220
|
"type": b"dinf",
|
|
171
221
|
"data": [
|
|
172
222
|
# self reference dref box
|
|
@@ -187,7 +237,7 @@ def create_camm_trak(
|
|
|
187
237
|
],
|
|
188
238
|
}
|
|
189
239
|
|
|
190
|
-
minf: BoxDict = {
|
|
240
|
+
minf: builder.BoxDict = {
|
|
191
241
|
"type": b"minf",
|
|
192
242
|
"data": [
|
|
193
243
|
dinf,
|
|
@@ -195,7 +245,7 @@ def create_camm_trak(
|
|
|
195
245
|
],
|
|
196
246
|
}
|
|
197
247
|
|
|
198
|
-
tkhd: BoxDict = {
|
|
248
|
+
tkhd: builder.BoxDict = {
|
|
199
249
|
"type": b"tkhd",
|
|
200
250
|
"data": {
|
|
201
251
|
# use 32-bit version of the box
|
|
@@ -213,7 +263,7 @@ def create_camm_trak(
|
|
|
213
263
|
},
|
|
214
264
|
}
|
|
215
265
|
|
|
216
|
-
mdia: BoxDict = {
|
|
266
|
+
mdia: builder.BoxDict = {
|
|
217
267
|
"type": b"mdia",
|
|
218
268
|
"data": [
|
|
219
269
|
mdhd,
|
|
@@ -231,23 +281,31 @@ def create_camm_trak(
|
|
|
231
281
|
}
|
|
232
282
|
|
|
233
283
|
|
|
234
|
-
def camm_sample_generator2(
|
|
284
|
+
def camm_sample_generator2(
|
|
285
|
+
video_metadata: types.VideoMetadata,
|
|
286
|
+
telemetry_measurements: T.Optional[T.List[telemetry.TelemetryMeasurement]] = None,
|
|
287
|
+
):
|
|
235
288
|
def _f(
|
|
236
289
|
fp: T.BinaryIO,
|
|
237
|
-
moov_children: T.List[BoxDict],
|
|
290
|
+
moov_children: T.List[builder.BoxDict],
|
|
238
291
|
) -> T.Generator[io.IOBase, None, None]:
|
|
239
292
|
movie_timescale = builder.find_movie_timescale(moov_children)
|
|
240
293
|
# make sure the precision of timedeltas not lower than 0.001 (1ms)
|
|
241
294
|
media_timescale = max(1000, movie_timescale)
|
|
295
|
+
|
|
296
|
+
# points with negative time are skipped
|
|
297
|
+
# TODO: interpolate first point at time == 0
|
|
298
|
+
# TODO: measurements with negative times should be skipped too
|
|
299
|
+
points = [point for point in video_metadata.points if point.time >= 0]
|
|
300
|
+
|
|
301
|
+
measurements = _multiplex(points, telemetry_measurements)
|
|
242
302
|
camm_samples = list(
|
|
243
|
-
|
|
303
|
+
convert_telemetry_to_raw_samples(measurements, media_timescale)
|
|
244
304
|
)
|
|
245
305
|
camm_trak = create_camm_trak(camm_samples, media_timescale)
|
|
246
|
-
elst =
|
|
247
|
-
[video_metadata.points], movie_timescale, media_timescale
|
|
248
|
-
)
|
|
306
|
+
elst = _create_edit_list_from_points([points], movie_timescale, media_timescale)
|
|
249
307
|
if T.cast(T.Dict, elst["data"])["entries"]:
|
|
250
|
-
T.cast(T.List[BoxDict], camm_trak["data"]).append(
|
|
308
|
+
T.cast(T.List[builder.BoxDict], camm_trak["data"]).append(
|
|
251
309
|
{
|
|
252
310
|
"type": b"edts",
|
|
253
311
|
"data": [elst],
|
|
@@ -255,7 +313,7 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
|
|
|
255
313
|
)
|
|
256
314
|
moov_children.append(camm_trak)
|
|
257
315
|
|
|
258
|
-
udta_data: T.List[BoxDict] = []
|
|
316
|
+
udta_data: T.List[builder.BoxDict] = []
|
|
259
317
|
if video_metadata.make:
|
|
260
318
|
udta_data.append(
|
|
261
319
|
{
|
|
@@ -279,6 +337,8 @@ def camm_sample_generator2(video_metadata: types.VideoMetadata):
|
|
|
279
337
|
)
|
|
280
338
|
|
|
281
339
|
# if yield, the moov_children will not be modified
|
|
282
|
-
return (
|
|
340
|
+
return (
|
|
341
|
+
io.BytesIO(_build_camm_sample(measurement)) for measurement in measurements
|
|
342
|
+
)
|
|
283
343
|
|
|
284
344
|
return _f
|