22import os
33import cv2
44import numpy as np
5+ from typing import Optional
56from scipy .spatial .transform import Rotation
67import spatialmp4 as sm
78import rerun as rr
89import rerun .blueprint as rrb
910
1011
1112def pico_pose_to_open3d (extrinsic ):
13+ extrinsic = np .array (extrinsic , dtype = np .float64 , copy = True )
1214 convert = np .array ([
1315 [0 , 0 , 1 , 0 ],
1416 [1 , 0 , 0 , 0 ],
@@ -19,26 +21,68 @@ def pico_pose_to_open3d(extrinsic):
1921 return output
2022
2123
24+ def ensure_right_handed_rotation (rotation_matrix : np .ndarray ) -> np .ndarray :
25+ r = np .array (rotation_matrix , dtype = np .float64 , copy = True )
26+ u , _ , vh = np .linalg .svd (r )
27+ corrected = u @ vh
28+ if np .linalg .det (corrected ) < 0 :
29+ u [:, - 1 ] *= - 1
30+ corrected = u @ vh
31+ return corrected
32+
33+
34+ def head_to_imu (head_pose : np .ndarray , head_model_offset : np .ndarray ) -> np .ndarray :
35+ """Convert head pose (T_W_H) into IMU pose (T_W_I) matching Utilities::HeadToImu."""
36+ head_pose = np .asarray (head_pose , dtype = np .float64 )
37+ if head_pose .shape != (4 , 4 ):
38+ raise ValueError ("head_pose must be a 4x4 matrix" )
39+
40+ head_model_offset = np .asarray (head_model_offset , dtype = np .float64 )
41+ if head_model_offset .shape != (3 ,):
42+ raise ValueError ("head_model_offset must be a 3-element vector" )
43+
44+ rotation_matrix = head_pose [:3 , :3 ]
45+ translation = head_pose [:3 , 3 ].copy ()
46+
47+ rotation = Rotation .from_matrix (rotation_matrix )
48+ translation -= rotation .apply (head_model_offset )
49+
50+ quat_x , quat_y , quat_z , quat_w = rotation .as_quat () # scipy returns xyzw
51+ imu_quat = np .array ([quat_y , - quat_x , quat_z , quat_w ], dtype = np .float64 ) # xyzw
52+ imu_rotation = Rotation .from_quat (imu_quat ).as_matrix ()
53+ imu_translation = np .array ([translation [1 ], - translation [0 ], translation [2 ]], dtype = np .float64 )
54+
55+ imu_pose = np .eye (4 , dtype = np .float64 )
56+ imu_pose [:3 , :3 ] = imu_rotation
57+ imu_pose [:3 , 3 ] = imu_translation
58+ return imu_pose
59+
60+
2261def main (
2362 video_file : str ,
2463 depth_only : bool = False ,
64+ rgb_only : bool = False ,
65+ topk : Optional [int ] = typer .Option (None , help = "Limit visualization to the first K frames." ),
2566):
2667 """Visualize spatialmp4 using rerun."""
68+
2769 reader = sm .Reader (video_file )
2870
2971 if depth_only :
3072 reader .set_read_mode (sm .ReadMode .DEPTH_ONLY )
73+ elif rgb_only :
74+ reader .set_read_mode (sm .ReadMode .RGB_ONLY )
3175 else :
3276 reader .set_read_mode (sm .ReadMode .DEPTH_FIRST )
3377
34- if not reader .has_depth ():
78+ if not rgb_only and not reader .has_depth ():
3579 typer .echo (typer .style (f"No depth found in input file" , fg = typer .colors .RED ))
3680 return
3781 if not reader .has_pose ():
38- typer .echo (typer .style (f"No depth found in input file" , fg = typer .colors .RED ))
82+ typer .echo (typer .style (f"No pose found in input file" , fg = typer .colors .RED ))
3983 return
4084
41- blueprint = rrb .Horizontal (
85+ blueprint = rrb .Horizontal (
4286 rrb .Vertical (
4387 rrb .Spatial3DView (name = "3D" , origin = "world" ),
4488 rrb .TextDocumentView (name = "Description" , origin = "/description" ),
@@ -80,6 +124,8 @@ def main(
80124 cx = float (reader .get_rgb_intrinsics_left ().cx )
81125 cy = float (reader .get_rgb_intrinsics_left ().cy )
82126
127+ processed = 0
128+
83129 while reader .has_next ():
84130 if depth_only :
85131 depth_frame = reader .load_depth ()
@@ -91,22 +137,35 @@ def main(
91137 extrinsic = np .eye (4 )
92138 extrinsic [:3 , 3 ] = [TWH .x , TWH .y , TWH .z ]
93139 extrinsic [:3 , :3 ] = Rotation .from_quat ((TWH .qx , TWH .qy , TWH .qz , TWH .qw )).as_matrix ()
140+ elif rgb_only :
141+ frame_rgb = reader .load_rgb ()
142+ timestamp = frame_rgb .timestamp
143+
144+ T_I_Srgb = reader .get_rgb_extrinsics_left ().as_se3 ()
145+ T_W_Hrgb = frame_rgb .pose .as_se3 ()
146+ T_W_Irgb = sm .head_to_imu (T_W_Hrgb , sm .HEAD_MODEL_OFFSET )
147+ T_W_Srgb = T_W_Irgb @ T_I_Srgb
148+ extrinsic = T_W_Srgb
149+
94150 else :
95151 rgbd = reader .load_rgbd (True )
96152 timestamp = rgbd .timestamp
97153 depth_np = rgbd .depth
98154 extrinsic = rgbd .T_W_S
99- print (f"Loading frame { reader .get_index () + 1 } / { reader .get_frame_count ()} , timestamp: { timestamp } " )
100155
101- # preprocess on depthmap
102- depth_uint16 = (depth_np * 1000 ).astype (np .uint16 )
103- sobelx = cv2 .Sobel (depth_uint16 , cv2 .CV_32F , 1 , 0 , ksize = 3 )
104- sobely = cv2 .Sobel (depth_uint16 , cv2 .CV_32F , 0 , 1 , ksize = 3 )
105- grad_mag = np .sqrt (sobelx ** 2 + sobely ** 2 )
106- depth_np [grad_mag > 500 ] = 0
107- depth_np [(depth_np < 0.2 ) | (depth_np > 5 )] = 0
156+ print (f"Loading frame { reader .get_index () + 1 } / { reader .get_frame_count ()} , timestamp: { timestamp } " )
157+
158+ if not rgb_only :
159+ # preprocess on depthmap
160+ depth_uint16 = (depth_np * 1000 ).astype (np .uint16 )
161+ sobelx = cv2 .Sobel (depth_uint16 , cv2 .CV_32F , 1 , 0 , ksize = 3 )
162+ sobely = cv2 .Sobel (depth_uint16 , cv2 .CV_32F , 0 , 1 , ksize = 3 )
163+ grad_mag = np .sqrt (sobelx ** 2 + sobely ** 2 )
164+ depth_np [grad_mag > 500 ] = 0
165+ depth_np [(depth_np < 0.2 ) | (depth_np > 5 )] = 0
108166
109167 extrinsic = pico_pose_to_open3d (extrinsic )
168+ extrinsic [:3 , :3 ] = ensure_right_handed_rotation (extrinsic [:3 , :3 ])
110169
111170 rr .set_time_seconds ("time" , timestamp )
112171 rr .log ("world/xyz" , rr .Arrows3D (vectors = [[1 , 0 , 0 ], [0 , 1 , 0 ], [0 , 0 , 1 ]], colors = [[255 , 0 , 0 ], [0 , 255 , 0 ], [0 , 0 , 255 ]]))
@@ -119,9 +178,19 @@ def main(
119178 position = extrinsic [:3 , 3 ]
120179 rotation = Rotation .from_matrix (extrinsic [:3 , :3 ]).as_quat ()
121180 rr .log ("world/camera" , rr .Transform3D (translation = position , rotation = rr .Quaternion (xyzw = rotation )))
122- rr .log ("world/camera/image/depth" , rr .DepthImage (depth_np , meter = 1.0 ))
123- if not depth_only :
124- rr .log ("world/camera/image/rgb" , rr .Image (rgbd .rgb , color_model = "BGR" ).compress (jpeg_quality = 95 ))
181+
182+ if rgb_only :
183+ rr .log ("world/camera/image/rgb" , rr .Image (frame_rgb .left_rgb , color_model = "BGR" ).compress (jpeg_quality = 95 ))
184+ else :
185+ rr .log ("world/camera/image/depth" , rr .DepthImage (depth_np , meter = 1.0 ))
186+ if not depth_only :
187+ rr .log ("world/camera/image/rgb" , rr .Image (rgbd .rgb , color_model = "BGR" ).compress (jpeg_quality = 95 ))
188+
189+ processed += 1
190+ if topk is not None and processed >= topk :
191+ break
192+ if topk and reader .get_index () > topk :
193+ break
125194
126195
127196if __name__ == "__main__" :#
0 commit comments