-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathgeometry.py
More file actions
executable file
·484 lines (398 loc) · 14.5 KB
/
geometry.py
File metadata and controls
executable file
·484 lines (398 loc) · 14.5 KB
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
"""
A collection of geometric transformation operations
@author: Zhaoyang Lv
@Date: March, 2019
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import torch
import torch.nn as nn
from torch import sin, cos, atan2, acos
_NEXT_AXIS = [1, 2, 0, 1]
# map axes strings to/from tuples of inner axis, parity, repetition, frame
_AXES2TUPLE = {
'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0),
'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0),
'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0),
'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0),
'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1),
'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1),
'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1),
'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)}
_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items())
def meshgrid(H, W, B=None, is_cuda=False):
""" torch version of numpy meshgrid function
:input
:param height
:param width
:param batch size
:param initialize a cuda tensor if true
-------
:return
:param meshgrid in column
:param meshgrid in row
"""
u = torch.arange(0, W)
v = torch.arange(0, H)
if is_cuda:
u, v = u.cuda(), v.cuda()
u = u.repeat(H, 1).view(1,H,W)
v = v.repeat(W, 1).t_().view(1,H,W)
if B is not None:
u, v = u.repeat(B,1,1,1), v.repeat(B,1,1,1)
return u, v
def generate_xy_grid(B, H, W, K):
""" Generate a batch of image grid from image space to world space
px = (u - cx) / fx
py = (y - cy) / fy
function tested in 'test_geometry.py'
:input
:param batch size
:param height
:param width
:param camera intrinsic array [fx,fy,cx,cy]
---------
:return
:param
:param
"""
fx, fy, cx, cy = K.split(1,dim=1)
uv_grid = meshgrid(H, W, B)
u_grid, v_grid = [uv.type_as(cx) for uv in uv_grid]
px = ((u_grid.view(B,-1) - cx) / fx).view(B,1,H,W)
py = ((v_grid.view(B,-1) - cy) / fy).view(B,1,H,W)
return px, py
def batch_inverse_Rt(R, t):
""" The inverse of the R, t: [R' | -R't]
function tested in 'test_geometry.py'
:input
:param rotation Bx3x3
:param translation Bx3
----------
:return
:param rotation inverse Bx3x3
:param translation inverse Bx3
"""
R_t = R.transpose(1,2)
t_inv = -torch.bmm(R_t, t.contiguous().view(-1, 3, 1))
return R_t, t_inv.view(-1,3)
def batch_Rt_compose(d_R, d_t, R0, t0):
""" Compose operator of R, t: [d_R*R | d_R*t + d_t]
We use left-mulitplication rule here.
function tested in 'test_geometry.py'
:input
:param rotation incremental Bx3x3
:param translation incremental Bx3
:param initial rotation Bx3x3
:param initial translation Bx3
----------
:return
:param composed rotation Bx3x3
:param composed translation Bx3
"""
R1 = d_R.bmm(R0)
t1 = d_R.bmm(t0.view(-1,3,1)) + d_t.view(-1,3,1)
return R1, t1.view(-1,3)
def batch_Rt_between(R0, t0, R1, t1):
""" Between operator of R, t, transform of T_0=[R0, t0] to T_1=[R1, t1]
which is T_1 \compose T^{-1}_0
function tested in 'test_geometry.py'
:input
:param rotation of source Bx3x3
:param translation of source Bx3
:param rotation of target Bx3x3
:param translation of target Bx3
----------
:return
:param incremental rotation Bx3x3
:param incremnetal translation Bx3
"""
R0t = R0.transpose(1,2)
dR = R1.bmm(R0t)
dt = t1.view(-1,3) - dR.bmm(t0.view(-1,3,1)).view(-1,3)
return dR, dt
def batch_skew(w):
""" Generate a batch of skew-symmetric matrices.
function tested in 'test_geometry.py'
:input
:param skew symmetric matrix entry Bx3
---------
:return
:param the skew-symmetric matrix Bx3x3
"""
B, D = w.size()
assert(D == 3)
o = torch.zeros(B).type_as(w)
w0, w1, w2 = w[:, 0], w[:, 1], w[:, 2]
return torch.stack((o, -w2, w1, w2, o, -w0, -w1, w0, o), 1).view(B, 3, 3)
def batch_twist2Mat(twist):
""" The exponential map from so3 to SO3
Calculate the rotation matrix using Rodrigues' Rotation Formula
http://electroncastle.com/wp/?p=39
or Ethan Eade's lie group note:
http://ethaneade.com/lie.pdf equation (13)-(15)
@todo: may rename the interface to batch_so3expmap(twist)
functioned tested with cv2.Rodrigues implementation in 'test_geometry.py'
:input
:param twist/axis angle Bx3 \in \so3 space
----------
:return
:param Rotation matrix Bx3x3 \in \SO3 space
"""
B = twist.size()[0]
theta = twist.norm(p=2, dim=1).view(B, 1)
w_so3 = twist / theta.expand(B, 3)
W = batch_skew(w_so3)
return torch.eye(3).repeat(B,1,1).type_as(W) \
+ W*sin(theta.view(B,1,1)) \
+ W.bmm(W)*(1-cos(theta).view(B,1,1))
def batch_mat2angle(R):
""" Calcuate the axis angles (twist) from a batch of rotation matrices
Ethan Eade's lie group note:
http://ethaneade.com/lie.pdf equation (17)
function tested in 'test_geometry.py'
:input
:param Rotation matrix Bx3x3 \in \SO3 space
--------
:return
:param the axis angle B
"""
R1 = [torch.trace(R[i]) for i in range(R.size()[0])]
R_trace = torch.stack(R1)
# clamp if the angle is too large (break small angle assumption)
# @todo: not sure whether it is absoluately necessary in training.
angle = acos( ((R_trace - 1)/2).clamp(-1,1))
return angle
def batch_mat2twist(R):
""" The log map from SO3 to so3
Calculate the twist vector from Rotation matrix
Ethan Eade's lie group note:
http://ethaneade.com/lie.pdf equation (18)
@todo: may rename the interface to batch_so3logmap(R)
function tested in 'test_geometry.py'
@note: it currently does not consider extreme small values.
If you use it as training loss, you may run into problems
:input
:param Rotation matrix Bx3x3 \in \SO3 space
--------
:param the twist vector Bx3 \in \so3 space
"""
B = R.size()[0]
R1 = [torch.trace(R[i]) for i in range(R.size()[0])]
tr = torch.stack(R1)
theta = acos( ((tr - 1)/2).clamp(-1,1) )
r11,r12,r13,r21,r22,r23,r31,r32,r33 = torch.split(R.view(B,-1),1,dim=1)
res = torch.cat([r32-r23, r13-r31, r21-r12],dim=1)
magnitude = (0.5*theta/sin(theta))
return magnitude.view(B,1) * res
def batch_warp_inverse_depth(p_x, p_y, p_invD, pose, K):
""" Compute the warping grid w.r.t. the SE3 transform given the inverse depth
:input
:param p_x the x coordinate map
:param p_y the y coordinate map
:param p_invD the inverse depth
:param pose the 3D transform in SE3
:param K the intrinsics
--------
:return
:param projected u coordinate in image space Bx1xHxW
:param projected v coordinate in image space Bx1xHxW
:param projected inverse depth Bx1XHxW
"""
[R, t] = pose
B, _, H, W = p_x.shape
I = torch.ones((B,1,H,W)).type_as(p_invD)
x_y_1 = torch.cat((p_x, p_y, I), dim=1)
warped = torch.bmm(R, x_y_1.view(B,3,H*W)) + \
torch.bmm(t.view(B,3,1), p_invD.view(B,1,H*W))
x_, y_, s_ = torch.split(warped, 1, dim=1)
fx, fy, cx, cy = torch.split(K, 1, dim=1)
u_ = (x_ / s_).view(B,-1) * fx + cx
v_ = (y_ / s_).view(B,-1) * fy + cy
inv_z_ = p_invD / s_.view(B,1,H,W)
return u_.view(B,1,H,W), v_.view(B,1,H,W), inv_z_
def batch_warp_affine(pu, pv, affine):
# A = affine[:,:,:2]
# t = affine[:,:, 2]
B,_,H,W = pu.shape
ones = torch.ones(pu.shape).type_as(pu)
uv = torch.cat((pu, pv, ones), dim=1)
uv = torch.bmm(affine, uv.view(B,3,-1)) #+ t.view(B,2,1)
return uv[:,0].view(B,1,H,W), uv[:,1].view(B,1,H,W)
def check_occ(inv_z_buffer, inv_z_ref, u, v, thres=1e-1):
""" z-buffering check of occlusion
:param inverse depth of target frame
:param inverse depth of reference frame
"""
B, _, H, W = inv_z_buffer.shape
inv_z_warped = warp_features(inv_z_ref, u, v)
inlier = (inv_z_buffer > inv_z_warped - thres)
inviews = inlier & (u > 0) & (u < W) & \
(v > 0) & (v < H)
return 1-inviews
def warp_features(F, u, v):
"""
Warp the feature map (F) w.r.t. the grid (u, v)
"""
B, C, H, W = F.shape
u_norm = u / ((W-1)/2) - 1
v_norm = v / ((H-1)/2) - 1
uv_grid = torch.cat((u_norm.view(B,H,W,1), v_norm.view(B,H,W,1)), dim=3)
F_warped = nn.functional.grid_sample(F, uv_grid,
mode='bilinear', padding_mode='border')
return F_warped
def batch_transform_xyz(xyz_tensor, R, t, get_Jacobian=True):
'''
transform the point cloud w.r.t. the transformation matrix
:param xyz_tensor: B * 3 * H * W
:param R: rotation matrix B * 3 * 3
:param t: translation vector B * 3
'''
B, C, H, W = xyz_tensor.size()
t_tensor = t.contiguous().view(B,3,1).repeat(1,1,H*W)
p_tensor = xyz_tensor.contiguous().view(B, C, H*W)
# the transformation process is simply:
# p' = t + R*p
xyz_t_tensor = torch.baddbmm(t_tensor, R, p_tensor)
if get_Jacobian:
# return both the transformed tensor and its Jacobian matrix
J_r = R.bmm(batch_skew_symmetric_matrix(-1*p_tensor.permute(0,2,1)))
J_t = -1 * torch.eye(3).view(1,3,3).expand(B,3,3)
J = torch.cat((J_r, J_t), 1)
return xyz_t_tensor.view(B, C, H, W), J
else:
return xyz_t_tensor.view(B, C, H, W)
def flow_from_rigid_transform(depth, extrinsic, intrinsic):
"""
Get the optical flow induced by rigid transform [R,t] and depth
"""
[R, t] = extrinsic
[fx, fy, cx, cy] = intrinsic
def batch_project(xyz_tensor, K):
""" Project a point cloud into pixels (u,v) given intrinsic K
[u';v';w] = [K][x;y;z]
u = u' / w; v = v' / w
:param the xyz points
:param calibration is a torch array composed of [fx, fy, cx, cy]
-------
:return u, v grid tensor in image coordinate
(tested through inverse project)
"""
B, _, H, W = xyz_tensor.size()
batch_K = K.expand(H, W, B, 4).permute(2,3,0,1)
x, y, z = torch.split(xyz_tensor, 1, dim=1)
fx, fy, cx, cy = torch.split(batch_K, 1, dim=1)
u = fx*x / z + cx
v = fy*y / z + cy
return torch.cat((u,v), dim=1)
def batch_inverse_project(depth, K):
""" Inverse project pixels (u,v) to a point cloud given intrinsic
:param depth dim B*H*W
:param calibration is torch array composed of [fx, fy, cx, cy]
:param color (optional) dim B*3*H*W
-------
:return xyz tensor (batch of point cloud)
(tested through projection)
"""
if depth.dim() == 3:
B, H, W = depth.size()
else:
B, _, H, W = depth.size()
x, y = generate_xy_grid(B,H,W,K)
z = depth.view(B,1,H,W)
return torch.cat((x*z, y*z, z), dim=1)
def batch_euler2mat(ai, aj, ak, axes='sxyz'):
""" A torch implementation euler2mat from transform3d:
https://github.com/matthew-brett/transforms3d/blob/master/transforms3d/euler.py
:param ai : First rotation angle (according to `axes`).
:param aj : Second rotation angle (according to `axes`).
:param ak : Third rotation angle (according to `axes`).
:param axes : Axis specification; one of 24 axis sequences as string or encoded tuple - e.g. ``sxyz`` (the default).
-------
:return rotation matrix, array-like shape (B, 3, 3)
Tested w.r.t. transforms3d.euler module
"""
B = ai.size()[0]
try:
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
except (AttributeError, KeyError):
_TUPLE2AXES[axes] # validation
firstaxis, parity, repetition, frame = axes
i = firstaxis
j = _NEXT_AXIS[i+parity]
k = _NEXT_AXIS[i-parity+1]
order = [i, j, k]
if frame:
ai, ak = ak, ai
if parity:
ai, aj, ak = -ai, -aj, -ak
si, sj, sk = sin(ai), sin(aj), sin(ak)
ci, cj, ck = cos(ai), cos(aj), cos(ak)
cc, cs = ci*ck, ci*sk
sc, ss = si*ck, si*sk
# M = torch.zeros(B, 3, 3).cuda()
if repetition:
c_i = [cj, sj*si, sj*ci]
c_j = [sj*sk, -cj*ss+cc, -cj*cs-sc]
c_k = [-sj*ck, cj*sc+cs, cj*cc-ss]
else:
c_i = [cj*ck, sj*sc-cs, sj*cc+ss]
c_j = [cj*sk, sj*ss+cc, sj*cs-sc]
c_k = [-sj, cj*si, cj*ci]
def permute(X): # sort X w.r.t. the axis indices
return [ x for (y, x) in sorted(zip(order, X)) ]
c_i = permute(c_i)
c_j = permute(c_j)
c_k = permute(c_k)
r =[torch.stack(c_i, 1),
torch.stack(c_j, 1),
torch.stack(c_k, 1)]
r = permute(r)
return torch.stack(r, 1)
def batch_mat2euler(M, axes='sxyz'):
""" A torch implementation euler2mat from transform3d:
https://github.com/matthew-brett/transforms3d/blob/master/transforms3d/euler.py
:param array-like shape (3, 3) or (4, 4). Rotation matrix or affine.
:param Axis specification; one of 24 axis sequences as string or encoded tuple - e.g. ``sxyz`` (the default).
--------
:returns
:param ai : First rotation angle (according to `axes`).
:param aj : Second rotation angle (according to `axes`).
:param ak : Third rotation angle (according to `axes`).
"""
try:
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
except (AttributeError, KeyError):
_TUPLE2AXES[axes] # validation
firstaxis, parity, repetition, frame = axes
i = firstaxis
j = _NEXT_AXIS[i+parity]
k = _NEXT_AXIS[i-parity+1]
if repetition:
sy = torch.sqrt(M[:, i, j]**2 + M[:, i, k]**2)
# A lazy way to cope with batch data. Can be more efficient
mask = ~(sy > 1e-8)
ax = atan2( M[:, i, j], M[:, i, k])
ay = atan2( sy, M[:, i, i])
az = atan2( M[:, j, i], -M[:, k, i])
if mask.sum() > 0:
ax[mask] = atan2(-M[:, j, k][mask], M[:, j, j][mask])
ay[mask] = atan2( sy[mask], M[:, i, i][mask])
az[mask] = 0.0
else:
cy = torch.sqrt(M[:, i, i]**2 + M[:, j, i]**2)
mask = ~(cy > 1e-8)
ax = atan2( M[:, k, j], M[:, k, k])
ay = atan2(-M[:, k, i], cy)
az = atan2( M[:, j, i], M[:, i, i])
if mask.sum() > 0:
ax[mask] = atan2(-M[:, j, k][mask], M[:, j, j][mask])
ay[mask] = atan2(-M[:, k, i][mask], cy[mask])
az[mask] = 0.0
if parity:
ax, ay, az = -ax, -ay, -az
if frame:
ax, az = az, ax
return ax, ay, az