99
1010import construct as C
1111
12- from .. import geo
13- from ..mp4 import mp4_sample_parser as sample_parser , simple_mp4_parser as sparser
12+ from .. import geo , telemetry
13+ from ..mp4 import simple_mp4_parser as sparser
14+ from ..mp4 .mp4_sample_parser import MovieBoxParser , Sample , TrackBoxParser
1415
1516
1617LOG = logging .getLogger (__name__ )
1718
1819
20+ TelemetryMeasurement = T .Union [
21+ geo .Point ,
22+ telemetry .AccelerationData ,
23+ telemetry .GyroscopeData ,
24+ telemetry .MagnetometerData ,
25+ ]
26+
27+
1928# Camera Motion Metadata Spec https://developers.google.com/streetview/publish/camm-spec
2029class CAMMType (Enum ):
2130 ANGLE_AXIS = 0
@@ -75,9 +84,9 @@ class CAMMType(Enum):
7584)
7685
7786
78- def _parse_point_from_sample (
79- fp : T .BinaryIO , sample : sample_parser . Sample
80- ) -> T .Optional [geo . Point ]:
87+ def _parse_telemetry_from_sample (
88+ fp : T .BinaryIO , sample : Sample
89+ ) -> T .Optional [TelemetryMeasurement ]:
8190 fp .seek (sample .raw_sample .offset , io .SEEK_SET )
8291 data = fp .read (sample .raw_sample .size )
8392 box = CAMMSampleData .parse (data )
@@ -99,12 +108,34 @@ def _parse_point_from_sample(
99108 alt = box .data .altitude ,
100109 angle = None ,
101110 )
111+ elif box .type == CAMMType .ACCELERATION .value :
112+ return telemetry .AccelerationData (
113+ time = sample .exact_time ,
114+ x = box .data [0 ],
115+ y = box .data [1 ],
116+ z = box .data [2 ],
117+ )
118+ elif box .type == CAMMType .GYRO .value :
119+ return telemetry .GyroscopeData (
120+ time = sample .exact_time ,
121+ x = box .data [0 ],
122+ y = box .data [1 ],
123+ z = box .data [2 ],
124+ )
125+ elif box .type == CAMMType .MAGNETIC_FIELD .value :
126+ return telemetry .MagnetometerData (
127+ time = sample .exact_time ,
128+ x = box .data [0 ],
129+ y = box .data [1 ],
130+ z = box .data [2 ],
131+ )
102132 return None
103133
104134
105- def filter_points_by_elst (
106- points : T .Iterable [geo .Point ], elst : T .Sequence [T .Tuple [float , float ]]
107- ) -> T .Generator [geo .Point , None , None ]:
135+ def _filter_telemetry_by_elst_segments (
136+ measurements : T .Iterable [TelemetryMeasurement ],
137+ elst : T .Sequence [T .Tuple [float , float ]],
138+ ) -> T .Generator [TelemetryMeasurement , None , None ]:
108139 empty_elst = [entry for entry in elst if entry [0 ] == - 1 ]
109140 if empty_elst :
110141 offset = empty_elst [- 1 ][1 ]
@@ -114,20 +145,26 @@ def filter_points_by_elst(
114145 elst = [entry for entry in elst if entry [0 ] != - 1 ]
115146
116147 if not elst :
117- for p in points :
118- yield dataclasses .replace (p , time = p .time + offset )
148+ for m in measurements :
149+ if dataclasses .is_dataclass (m ):
150+ yield dataclasses .replace (m , time = m .time + offset )
151+ else :
152+ m ._replace (time = m .time + offset )
119153 return
120154
121155 elst .sort (key = lambda entry : entry [0 ])
122156 elst_idx = 0
123- for p in points :
157+ for m in measurements :
124158 if len (elst ) <= elst_idx :
125159 break
126160 media_time , duration = elst [elst_idx ]
127- if p .time < media_time :
161+ if m .time < media_time :
128162 pass
129- elif p .time <= media_time + duration :
130- yield dataclasses .replace (p , time = p .time + offset )
163+ elif m .time <= media_time + duration :
164+ if dataclasses .is_dataclass (m ):
165+ yield dataclasses .replace (m , time = m .time + offset )
166+ else :
167+ m ._replace (time = m .time + offset )
131168 else :
132169 elst_idx += 1
133170
@@ -148,46 +185,84 @@ def _is_camm_description(description: T.Dict) -> bool:
148185 return description ["format" ] == b"camm"
149186
150187
188+ def _contains_camm_description (track : TrackBoxParser ) -> bool :
189+ descriptions = track .extract_sample_descriptions ()
190+ return any (_is_camm_description (d ) for d in descriptions )
191+
192+
193+ def _filter_telemetry_by_track_elst (
194+ moov : MovieBoxParser ,
195+ track : TrackBoxParser ,
196+ measurements : T .Iterable [TelemetryMeasurement ],
197+ ) -> T .List [TelemetryMeasurement ]:
198+ elst_boxdata = track .extract_elst_boxdata ()
199+
200+ if elst_boxdata is not None :
201+ elst_entries = elst_boxdata ["entries" ]
202+ if elst_entries :
203+ # media_timescale
204+ mdhd_boxdata = track .extract_mdhd_boxdata ()
205+ media_timescale = mdhd_boxdata ["timescale" ]
206+
207+ # movie_timescale
208+ mvhd_boxdata = moov .extract_mvhd_boxdata ()
209+ movie_timescale = mvhd_boxdata ["timescale" ]
210+
211+ segments = [
212+ elst_entry_to_seconds (
213+ entry ,
214+ movie_timescale = movie_timescale ,
215+ media_timescale = media_timescale ,
216+ )
217+ for entry in elst_entries
218+ ]
219+
220+ return list (_filter_telemetry_by_elst_segments (measurements , segments ))
221+
222+ return list (measurements )
223+
224+
151225def extract_points (fp : T .BinaryIO ) -> T .Optional [T .List [geo .Point ]]:
152226 """
153227 Return a list of points (could be empty) if it is a valid CAMM video,
154228 otherwise None
155229 """
156230
157- points = None
231+ moov = MovieBoxParser . parse_stream ( fp )
158232
159- moov = sample_parser .MovieBoxParser .parse_stream (fp )
160233 for track in moov .extract_tracks ():
161- descriptions = track .extract_sample_descriptions ()
162- if any (_is_camm_description (d ) for d in descriptions ):
163- maybe_points = (
164- _parse_point_from_sample (fp , sample )
234+ if _contains_camm_description (track ):
235+ maybe_measurements = (
236+ _parse_telemetry_from_sample (fp , sample )
165237 for sample in track .extract_samples ()
166238 if _is_camm_description (sample .description )
167239 )
168- points = [p for p in maybe_points if p is not None ]
169- if points :
170- elst_boxdata = track .extract_elst_boxdata ()
171- if elst_boxdata is not None :
172- elst_entries = elst_boxdata ["entries" ]
173- if elst_entries :
174- # media_timescale
175- mdhd_boxdata = track .extract_mdhd_boxdata ()
176- media_timescale = mdhd_boxdata ["timescale" ]
177- # movie_timescale
178- mvhd_boxdata = moov .extract_mvhd_boxdata ()
179- movie_timescale = mvhd_boxdata ["timescale" ]
180- segments = [
181- elst_entry_to_seconds (
182- entry ,
183- movie_timescale = movie_timescale ,
184- media_timescale = media_timescale ,
185- )
186- for entry in elst_entries
187- ]
188- points = list (filter_points_by_elst (points , segments ))
240+ points = [m for m in maybe_measurements if isinstance (m , geo .Point )]
189241
190- return points
242+ return T .cast (
243+ T .List [geo .Point ], _filter_telemetry_by_track_elst (moov , track , points )
244+ )
245+
246+ return None
247+
248+
249+ def extract_telemetry_data (fp : T .BinaryIO ) -> T .Optional [T .List [TelemetryMeasurement ]]:
250+ moov = MovieBoxParser .parse_stream (fp )
251+
252+ for track in moov .extract_tracks ():
253+ if _contains_camm_description (track ):
254+ maybe_measurements = (
255+ _parse_telemetry_from_sample (fp , sample )
256+ for sample in track .extract_samples ()
257+ if _is_camm_description (sample .description )
258+ )
259+ measurements = [m for m in maybe_measurements if m is not None ]
260+
261+ measurements = _filter_telemetry_by_track_elst (moov , track , measurements )
262+
263+ return measurements
264+
265+ return None
191266
192267
193268def parse_gpx (path : pathlib .Path ) -> T .List [geo .Point ]:
0 commit comments