-
Notifications
You must be signed in to change notification settings - Fork 2
/
mj_point_clouds.py
223 lines (183 loc) · 7.81 KB
/
mj_point_clouds.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python3
import math
import numpy as np
from PIL import Image as PIL_Image
import open3d as o3d
"""
Generates numpy rotation matrix from quaternion
@param quat: w-x-y-z quaternion rotation tuple
@return np_rot_mat: 3x3 rotation matrix as numpy array
"""
def quat2Mat(quat):
if len(quat) != 4:
print("Quaternion", quat, "invalid when generating transformation matrix.")
raise ValueError
# Note that the following code snippet can be used to generate the 3x3
# rotation matrix, we don't use it because this file should not depend
# on mujoco.
'''
from mujoco_py import functions
res = np.zeros(9)
functions.mju_quat2Mat(res, camera_quat)
res = res.reshape(3,3)
'''
# This function is lifted directly from scipy source code
#https://github.com/scipy/scipy/blob/v1.3.0/scipy/spatial/transform/rotation.py#L956
w = quat[0]
x = quat[1]
y = quat[2]
z = quat[3]
x2 = x * x
y2 = y * y
z2 = z * z
w2 = w * w
xy = x * y
zw = z * w
xz = x * z
yw = y * w
yz = y * z
xw = x * w
rot_mat_arr = [x2 - y2 - z2 + w2, 2 * (xy - zw), 2 * (xz + yw), \
2 * (xy + zw), - x2 + y2 - z2 + w2, 2 * (yz - xw), \
2 * (xz - yw), 2 * (yz + xw), - x2 - y2 + z2 + w2]
np_rot_mat = rotMatList2NPRotMat(rot_mat_arr)
return np_rot_mat
"""
Generates numpy rotation matrix from rotation matrix as list len(9)
@param rot_mat_arr: rotation matrix in list len(9) (row 0, row 1, row 2)
@return np_rot_mat: 3x3 rotation matrix as numpy array
"""
def rotMatList2NPRotMat(rot_mat_arr):
np_rot_arr = np.array(rot_mat_arr)
np_rot_mat = np_rot_arr.reshape((3, 3))
return np_rot_mat
"""
Generates numpy transformation matrix from position list len(3) and
numpy rotation matrix
@param pos: list len(3) containing position
@param rot_mat: 3x3 rotation matrix as numpy array
@return t_mat: 4x4 transformation matrix as numpy array
"""
def posRotMat2Mat(pos, rot_mat):
t_mat = np.eye(4)
t_mat[:3, :3] = rot_mat
t_mat[:3, 3] = np.array(pos)
return t_mat
"""
Generates Open3D camera intrinsic matrix object from numpy camera intrinsic
matrix and image width and height
@param cam_mat: 3x3 numpy array representing camera intrinsic matrix
@param width: image width in pixels
@param height: image height in pixels
@return t_mat: 4x4 transformation matrix as numpy array
"""
def cammat2o3d(cam_mat, width, height):
cx = cam_mat[0,2]
fx = cam_mat[0,0]
cy = cam_mat[1,2]
fy = cam_mat[1,1]
return o3d.camera.PinholeCameraIntrinsic(width, height, fx, fy, cx, cy)
#
# and combines them into point clouds
"""
Class that renders depth images in MuJoCo, processes depth images from
multiple cameras, converts them to point clouds, and processes the point
clouds
"""
class PointCloudGenerator(object):
"""
initialization function
@param sim: MuJoCo simulation object
@param min_bound: If not None, list len(3) containing smallest x, y, and z
values that will not be cropped
@param max_bound: If not None, list len(3) containing largest x, y, and z
values that will not be cropped
"""
def __init__(self, sim, min_bound=None, max_bound=None):
super(PointCloudGenerator, self).__init__()
self.sim = sim
# I think these can be set to anything
self.img_width = 640
self.img_height = 480
self.cam_names = self.sim.model.camera_names
self.target_bounds=None
if min_bound != None and max_bound != None:
self.target_bounds = o3d.geometry.AxisAlignedBoundingBox(min_bound=min_bound, max_bound=max_bound)
# List of camera intrinsic matrices
self.cam_mats = []
for cam_id in range(len(self.cam_names)):
fovy = math.radians(self.sim.model.cam_fovy[cam_id])
f = self.img_height / (2 * math.tan(fovy / 2))
cam_mat = np.array(((f, 0, self.img_width / 2), (0, f, self.img_height / 2), (0, 0, 1)))
self.cam_mats.append(cam_mat)
def generateCroppedPointCloud(self, save_img_dir=None):
o3d_clouds = []
cam_poses = []
for cam_i in range(len(self.cam_names)):
# Render and optionally save image from camera corresponding to cam_i
depth_img = self.captureImage(cam_i)
# If directory was provided, save color and depth images
# (overwriting previous)
if save_img_dir != None:
self.saveImg(depth_img, save_img_dir, "depth_test_" + str(cam_i))
color_img = self.captureImage(cam_i, False)
self.saveImg(color_img, save_img_dir, "color_test_" + str(cam_i))
# convert camera matrix and depth image to Open3D format, then
# generate point cloud
od_cammat = cammat2o3d(self.cam_mats[cam_i], self.img_width, self.img_height)
od_depth = o3d.geometry.Image(depth_img)
o3d_cloud = o3d.geometry.PointCloud.create_from_depth_image(od_depth, od_cammat)
# Compute world to camera transformation matrix
cam_body_id = self.sim.model.cam_bodyid[cam_i]
cam_pos = self.sim.model.body_pos[cam_body_id]
c2b_r = rotMatList2NPRotMat(self.sim.model.cam_mat0[cam_i])
# In MuJoCo, we assume that a camera is specified in XML as a body
# with pose p, and that that body has a camera sub-element
# with pos and euler 0.
# Therefore, camera frame with body euler 0 must be rotated about
# x-axis by 180 degrees to align it with the world frame.
b2w_r = quat2Mat([0, 1, 0, 0])
c2w_r = np.matmul(c2b_r, b2w_r)
c2w = posRotMat2Mat(cam_pos, c2w_r)
transformed_cloud = o3d_cloud.transform(c2w)
# If both minimum and maximum bounds are provided, crop cloud to fit
# inside them.
if self.target_bounds != None:
transformed_cloud = transformed_cloud.crop(self.target_bounds)
# Estimate normals of cropped cloud, then flip them based on camera
# position.
transformed_cloud.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=0.03, max_nn=250))
transformed_cloud.orient_normals_towards_camera_location(cam_pos)
o3d_clouds.append(transformed_cloud)
combined_cloud = o3d.geometry.PointCloud()
for cloud in o3d_clouds:
combined_cloud += cloud
return combined_cloud
# https://github.com/htung0101/table_dome/blob/master/table_dome_calib/utils.py#L160
def depthimg2Meters(self, depth):
extent = self.sim.model.stat.extent
near = self.sim.model.vis.map.znear * extent
far = self.sim.model.vis.map.zfar * extent
image = near / (1 - depth * (1 - near / far))
return image
def verticalFlip(self, img):
return np.flip(img, axis=0)
# Render and process an image
def captureImage(self, cam_ind, capture_depth=True):
rendered_images = self.sim.render(self.img_width, self.img_height, camera_name=self.cam_names[cam_ind], depth=capture_depth)
if capture_depth:
img, depth = rendered_images
depth = self.verticalFlip(depth)
real_depth = self.depthimg2Meters(depth)
return real_depth
else:
img = rendered_images
# Rendered images appear to be flipped about vertical axis
return self.verticalFlip(img)
# Normalizes an image so the maximum pixel value is 255,
# then writes to file
def saveImg(self, img, filepath, filename):
normalized_image = img/img.max()*255
normalized_image = normalized_image.astype(np.uint8)
im = PIL_Image.fromarray(normalized_image)
im.save(filepath + '/' + filename + ".jpg")