stray_visualize.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import os
  2. import open3d as o3d
  3. import numpy as np
  4. from scipy.spatial.transform import Rotation
  5. from argparse import ArgumentParser
  6. from PIL import Image
  7. import skvideo.io
  8. description = """
  9. This script visualizes datasets collected using the Stray Scanner app.
  10. """
  11. usage = """
  12. Basic usage: python stray_visualize.py <path-to-dataset-folder>
  13. """
  14. DEPTH_WIDTH = 256
  15. DEPTH_HEIGHT = 192
  16. MAX_DEPTH = 20.0
  17. def read_args():
  18. parser = ArgumentParser(description=description, usage=usage)
  19. parser.add_argument('path', type=str, help="Path to StrayScanner dataset to process.")
  20. parser.add_argument('--trajectory', '-t', action='store_true', help="Visualize the trajectory of the camera as a line.")
  21. parser.add_argument('--frames', '-f', action='store_true', help="Visualize camera coordinate frames from the odometry file.")
  22. parser.add_argument('--point-clouds', '-p', action='store_true', help="Show concatenated point clouds.")
  23. parser.add_argument('--integrate', '-i', action='store_true', help="Integrate point clouds using the Open3D RGB-D integration pipeline, and visualize it.")
  24. parser.add_argument('--mesh-filename', type=str, help='Mesh generated from point cloud integration will be stored in this file. open3d.io.write_triangle_mesh will be used.', default=None)
  25. parser.add_argument('--every', type=int, default=60, help="Show only every nth point cloud and coordinate frames. Only used for point cloud and odometry visualization.")
  26. parser.add_argument('--voxel-size', type=float, default=0.015, help="Voxel size in meters to use in RGB-D integration.")
  27. parser.add_argument('--confidence', '-c', type=int, default=1,
  28. help="Keep only depth estimates with confidence equal or higher to the given value. There are three different levels: 0, 1 and 2. Higher is more confident.")
  29. return parser.parse_args()
  30. def _resize_camera_matrix(camera_matrix, scale_x, scale_y):
  31. fx = camera_matrix[0, 0]
  32. fy = camera_matrix[1, 1]
  33. cx = camera_matrix[0, 2]
  34. cy = camera_matrix[1, 2]
  35. return np.array([[fx * scale_x, 0.0, cx * scale_x],
  36. [0., fy * scale_y, cy * scale_y],
  37. [0., 0., 1.0]])
  38. def read_data(flags):
  39. intrinsics = np.loadtxt(os.path.join(flags.path, 'camera_matrix.csv'), delimiter=',')
  40. odometry = np.loadtxt(os.path.join(flags.path, 'odometry.csv'), delimiter=',', skiprows=1)
  41. poses = []
  42. for line in odometry:
  43. # timestamp, frame, x, y, z, qx, qy, qz, qw
  44. position = line[2:5]
  45. quaternion = line[5:]
  46. T_WC = np.eye(4)
  47. T_WC[:3, :3] = Rotation.from_quat(quaternion).as_matrix()
  48. T_WC[:3, 3] = position
  49. poses.append(T_WC)
  50. depth_dir = os.path.join(flags.path, 'depth')
  51. depth_frames = [os.path.join(depth_dir, p) for p in sorted(os.listdir(depth_dir))]
  52. depth_frames = [f for f in depth_frames if '.npy' in f or '.png' in f]
  53. return { 'poses': poses, 'intrinsics': intrinsics, 'depth_frames': depth_frames }
  54. def load_depth(path, confidence=None, filter_level=0):
  55. if path[-4:] == '.npy':
  56. depth_mm = np.load(path)
  57. elif path[-4:] == '.png':
  58. depth_mm = np.array(Image.open(path))
  59. depth_m = depth_mm.astype(np.float32) / 1000.0
  60. if confidence is not None:
  61. depth_m[confidence < filter_level] = 0.0
  62. return o3d.geometry.Image(depth_m)
  63. def load_confidence(path):
  64. return np.array(Image.open(path))
  65. def get_intrinsics(intrinsics):
  66. """
  67. Scales the intrinsics matrix to be of the appropriate scale for the depth maps.
  68. """
  69. intrinsics_scaled = _resize_camera_matrix(intrinsics, DEPTH_WIDTH / 1920, DEPTH_HEIGHT / 1440)
  70. return o3d.camera.PinholeCameraIntrinsic(width=DEPTH_WIDTH, height=DEPTH_HEIGHT, fx=intrinsics_scaled[0, 0],
  71. fy=intrinsics_scaled[1, 1], cx=intrinsics_scaled[0, 2], cy=intrinsics_scaled[1, 2])
  72. def trajectory(flags, data):
  73. """
  74. Returns a set of lines connecting each camera poses world frame position.
  75. returns: [open3d.geometry.LineSet]
  76. """
  77. line_sets = []
  78. previous_pose = None
  79. for i, T_WC in enumerate(data['poses']):
  80. if previous_pose is not None:
  81. points = o3d.utility.Vector3dVector([previous_pose[:3, 3], T_WC[:3, 3]])
  82. lines = o3d.utility.Vector2iVector([[0, 1]])
  83. line = o3d.geometry.LineSet(points=points, lines=lines)
  84. line_sets.append(line)
  85. previous_pose = T_WC
  86. return line_sets
  87. def show_frames(flags, data):
  88. """
  89. Returns a list of meshes of coordinate axes that have been transformed to represent the camera matrix
  90. at each --every:th frame.
  91. flags: Command line arguments
  92. data: dict with keys ['poses', 'intrinsics']
  93. returns: [open3d.geometry.TriangleMesh]
  94. """
  95. frames = [o3d.geometry.TriangleMesh.create_coordinate_frame().scale(0.25, np.zeros(3))]
  96. for i, T_WC in enumerate(data['poses']):
  97. if not i % flags.every == 0:
  98. continue
  99. print(f"Frame {i}", end="\r")
  100. mesh = o3d.geometry.TriangleMesh.create_coordinate_frame().scale(0.1, np.zeros(3))
  101. frames.append(mesh.transform(T_WC))
  102. return frames
  103. def point_clouds(flags, data):
  104. """
  105. Converts depth maps to point clouds and merges them all into one global point cloud.
  106. flags: command line arguments
  107. data: dict with keys ['intrinsics', 'poses']
  108. returns: [open3d.geometry.PointCloud]
  109. """
  110. pcs = []
  111. intrinsics = get_intrinsics(data['intrinsics'])
  112. pc = o3d.geometry.PointCloud()
  113. meshes = []
  114. rgb_path = os.path.join(flags.path, 'rgb.mp4')
  115. video = skvideo.io.vreader(rgb_path)
  116. for i, (T_WC, rgb) in enumerate(zip(data['poses'], video)):
  117. if i % flags.every != 0:
  118. continue
  119. print(f"Point cloud {i}", end="\r")
  120. T_CW = np.linalg.inv(T_WC)
  121. confidence = load_confidence(os.path.join(flags.path, 'confidence', f'{i:06}.png'))
  122. depth_path = data['depth_frames'][i]
  123. depth = load_depth(depth_path, confidence, filter_level=flags.confidence)
  124. rgb = Image.fromarray(rgb)
  125. rgb = rgb.resize((DEPTH_WIDTH, DEPTH_HEIGHT))
  126. rgb = np.array(rgb)
  127. rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
  128. o3d.geometry.Image(rgb), depth,
  129. depth_scale=1.0, depth_trunc=MAX_DEPTH, convert_rgb_to_intensity=False)
  130. pc += o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsics, extrinsic=T_CW)
  131. return [pc]
  132. def integrate(flags, data):
  133. """
  134. Integrates collected RGB-D maps using the Open3D integration pipeline.
  135. flags: command line arguments
  136. data: dict with keys ['intrinsics', 'poses']
  137. Returns: open3d.geometry.TriangleMesh
  138. """
  139. volume = o3d.pipelines.integration.ScalableTSDFVolume(
  140. voxel_length=flags.voxel_size,
  141. sdf_trunc=0.05,
  142. color_type=o3d.pipelines.integration.TSDFVolumeColorType.RGB8)
  143. intrinsics = get_intrinsics(data['intrinsics'])
  144. rgb_path = os.path.join(flags.path, 'rgb.mp4')
  145. video = skvideo.io.vreader(rgb_path)
  146. for i, (T_WC, rgb) in enumerate(zip(data['poses'], video)):
  147. print(f"Integrating frame {i:06}", end='\r')
  148. depth_path = data['depth_frames'][i]
  149. depth = load_depth(depth_path)
  150. rgb = Image.fromarray(rgb)
  151. rgb = rgb.resize((DEPTH_WIDTH, DEPTH_HEIGHT))
  152. rgb = np.array(rgb)
  153. rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth(
  154. o3d.geometry.Image(rgb), depth,
  155. depth_scale=1.0, depth_trunc=MAX_DEPTH, convert_rgb_to_intensity=False)
  156. volume.integrate(rgbd, intrinsics, np.linalg.inv(T_WC))
  157. mesh = volume.extract_triangle_mesh()
  158. mesh.compute_vertex_normals()
  159. return mesh
  160. def validate(flags):
  161. if not os.path.exists(os.path.join(flags.path, 'rgb.mp4')):
  162. absolute_path = os.path.abspath(flags.path)
  163. print(f"The directory {absolute_path} does not appear to be a directory created by the Stray Scanner app.")
  164. return False
  165. return True
  166. def main():
  167. flags = read_args()
  168. if not validate(flags):
  169. return
  170. if not flags.frames and not flags.point_clouds and not flags.integrate:
  171. flags.frames = True
  172. flags.point_clouds = True
  173. flags.trajectory = True
  174. data = read_data(flags)
  175. geometries = []
  176. if flags.trajectory:
  177. geometries += trajectory(flags, data)
  178. if flags.frames:
  179. geometries += show_frames(flags, data)
  180. if flags.point_clouds:
  181. geometries += point_clouds(flags, data)
  182. if flags.integrate:
  183. mesh = integrate(flags, data)
  184. if flags.mesh_filename is not None:
  185. o3d.io.write_triangle_mesh(flags.mesh_filename, mesh)
  186. geometries += [mesh]
  187. o3d.visualization.draw_geometries(geometries)
  188. if __name__ == "__main__":
  189. main()