|
|
''' |
|
|
Functions about transforming mesh(changing the position: modify vertices). |
|
|
1. forward: transform(transform, camera, project). |
|
|
2. backward: estimate transform matrix from correspondences. |
|
|
|
|
|
Preparation knowledge: |
|
|
transform&camera model: |
|
|
https://cs184.eecs.berkeley.edu/lecture/transforms-2 |
|
|
Part I: camera geometry and single view geometry in MVGCV |
|
|
''' |
|
|
|
|
|
from __future__ import absolute_import |
|
|
from __future__ import division |
|
|
from __future__ import print_function |
|
|
|
|
|
import numpy as np |
|
|
import math |
|
|
from math import cos, sin |
|
|
|
|
|
def angle2matrix(angles): |
|
|
''' get rotation matrix from three rotation angles(degree). right-handed. |
|
|
Args: |
|
|
angles: [3,]. x, y, z angles |
|
|
x: pitch. positive for looking down. |
|
|
y: yaw. positive for looking left. |
|
|
z: roll. positive for tilting head right. |
|
|
Returns: |
|
|
R: [3, 3]. rotation matrix. |
|
|
''' |
|
|
x, y, z = np.deg2rad(angles[0]), np.deg2rad(angles[1]), np.deg2rad(angles[2]) |
|
|
|
|
|
Rx=np.array([[1, 0, 0], |
|
|
[0, cos(x), -sin(x)], |
|
|
[0, sin(x), cos(x)]]) |
|
|
|
|
|
Ry=np.array([[ cos(y), 0, sin(y)], |
|
|
[ 0, 1, 0], |
|
|
[-sin(y), 0, cos(y)]]) |
|
|
|
|
|
Rz=np.array([[cos(z), -sin(z), 0], |
|
|
[sin(z), cos(z), 0], |
|
|
[ 0, 0, 1]]) |
|
|
|
|
|
R=Rz.dot(Ry.dot(Rx)) |
|
|
return R.astype(np.float32) |
|
|
|
|
|
def angle2matrix_3ddfa(angles): |
|
|
''' get rotation matrix from three rotation angles(radian). The same as in 3DDFA. |
|
|
Args: |
|
|
angles: [3,]. x, y, z angles |
|
|
x: pitch. |
|
|
y: yaw. |
|
|
z: roll. |
|
|
Returns: |
|
|
R: 3x3. rotation matrix. |
|
|
''' |
|
|
|
|
|
x, y, z = angles[0], angles[1], angles[2] |
|
|
|
|
|
|
|
|
Rx=np.array([[1, 0, 0], |
|
|
[0, cos(x), sin(x)], |
|
|
[0, -sin(x), cos(x)]]) |
|
|
|
|
|
Ry=np.array([[ cos(y), 0, -sin(y)], |
|
|
[ 0, 1, 0], |
|
|
[sin(y), 0, cos(y)]]) |
|
|
|
|
|
Rz=np.array([[cos(z), sin(z), 0], |
|
|
[-sin(z), cos(z), 0], |
|
|
[ 0, 0, 1]]) |
|
|
R = Rx.dot(Ry).dot(Rz) |
|
|
return R.astype(np.float32) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rotate(vertices, angles): |
|
|
''' rotate vertices. |
|
|
X_new = R.dot(X). X: 3 x 1 |
|
|
Args: |
|
|
vertices: [nver, 3]. |
|
|
rx, ry, rz: degree angles |
|
|
rx: pitch. positive for looking down |
|
|
ry: yaw. positive for looking left |
|
|
rz: roll. positive for tilting head right |
|
|
Returns: |
|
|
rotated vertices: [nver, 3] |
|
|
''' |
|
|
R = angle2matrix(angles) |
|
|
rotated_vertices = vertices.dot(R.T) |
|
|
|
|
|
return rotated_vertices |
|
|
|
|
|
def similarity_transform(vertices, s, R, t3d): |
|
|
''' similarity transform. dof = 7. |
|
|
3D: s*R.dot(X) + t |
|
|
Homo: M = [[sR, t],[0^T, 1]]. M.dot(X) |
|
|
Args:(float32) |
|
|
vertices: [nver, 3]. |
|
|
s: [1,]. scale factor. |
|
|
R: [3,3]. rotation matrix. |
|
|
t3d: [3,]. 3d translation vector. |
|
|
Returns: |
|
|
transformed vertices: [nver, 3] |
|
|
''' |
|
|
t3d = np.squeeze(np.array(t3d, dtype = np.float32)) |
|
|
transformed_vertices = s * vertices.dot(R.T) + t3d[np.newaxis, :] |
|
|
|
|
|
return transformed_vertices |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize(x): |
|
|
epsilon = 1e-12 |
|
|
norm = np.sqrt(np.sum(x**2, axis = 0)) |
|
|
norm = np.maximum(norm, epsilon) |
|
|
return x/norm |
|
|
|
|
|
def lookat_camera(vertices, eye, at = None, up = None): |
|
|
""" 'look at' transformation: from world space to camera space |
|
|
standard camera space: |
|
|
camera located at the origin. |
|
|
looking down negative z-axis. |
|
|
vertical vector is y-axis. |
|
|
Xcam = R(X - C) |
|
|
Homo: [[R, -RC], [0, 1]] |
|
|
Args: |
|
|
vertices: [nver, 3] |
|
|
eye: [3,] the XYZ world space position of the camera. |
|
|
at: [3,] a position along the center of the camera's gaze. |
|
|
up: [3,] up direction |
|
|
Returns: |
|
|
transformed_vertices: [nver, 3] |
|
|
""" |
|
|
if at is None: |
|
|
at = np.array([0, 0, 0], np.float32) |
|
|
if up is None: |
|
|
up = np.array([0, 1, 0], np.float32) |
|
|
|
|
|
eye = np.array(eye).astype(np.float32) |
|
|
at = np.array(at).astype(np.float32) |
|
|
z_aixs = -normalize(at - eye) |
|
|
x_aixs = normalize(np.cross(up, z_aixs)) |
|
|
y_axis = np.cross(z_aixs, x_aixs) |
|
|
|
|
|
R = np.stack((x_aixs, y_axis, z_aixs)) |
|
|
transformed_vertices = vertices - eye |
|
|
transformed_vertices = transformed_vertices.dot(R.T) |
|
|
return transformed_vertices |
|
|
|
|
|
|
|
|
|
|
|
def orthographic_project(vertices): |
|
|
''' scaled orthographic projection(just delete z) |
|
|
assumes: variations in depth over the object is small relative to the mean distance from camera to object |
|
|
x -> x*f/z, y -> x*f/z, z -> f. |
|
|
for point i,j. zi~=zj. so just delete z |
|
|
** often used in face |
|
|
Homo: P = [[1,0,0,0], [0,1,0,0], [0,0,1,0]] |
|
|
Args: |
|
|
vertices: [nver, 3] |
|
|
Returns: |
|
|
projected_vertices: [nver, 3] if isKeepZ=True. [nver, 2] if isKeepZ=False. |
|
|
''' |
|
|
return vertices.copy() |
|
|
|
|
|
def perspective_project(vertices, fovy, aspect_ratio = 1., near = 0.1, far = 1000.): |
|
|
''' perspective projection. |
|
|
Args: |
|
|
vertices: [nver, 3] |
|
|
fovy: vertical angular field of view. degree. |
|
|
aspect_ratio : width / height of field of view |
|
|
near : depth of near clipping plane |
|
|
far : depth of far clipping plane |
|
|
Returns: |
|
|
projected_vertices: [nver, 3] |
|
|
''' |
|
|
fovy = np.deg2rad(fovy) |
|
|
top = near*np.tan(fovy) |
|
|
bottom = -top |
|
|
right = top*aspect_ratio |
|
|
left = -right |
|
|
|
|
|
|
|
|
P = np.array([[near/right, 0, 0, 0], |
|
|
[0, near/top, 0, 0], |
|
|
[0, 0, -(far+near)/(far-near), -2*far*near/(far-near)], |
|
|
[0, 0, -1, 0]]) |
|
|
vertices_homo = np.hstack((vertices, np.ones((vertices.shape[0], 1)))) |
|
|
projected_vertices = vertices_homo.dot(P.T) |
|
|
projected_vertices = projected_vertices/projected_vertices[:,3:] |
|
|
projected_vertices = projected_vertices[:,:3] |
|
|
projected_vertices[:,2] = -projected_vertices[:,2] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return projected_vertices |
|
|
|
|
|
|
|
|
def to_image(vertices, h, w, is_perspective = False): |
|
|
''' change vertices to image coord system |
|
|
3d system: XYZ, center(0, 0, 0) |
|
|
2d image: x(u), y(v). center(w/2, h/2), flip y-axis. |
|
|
Args: |
|
|
vertices: [nver, 3] |
|
|
h: height of the rendering |
|
|
w : width of the rendering |
|
|
Returns: |
|
|
projected_vertices: [nver, 3] |
|
|
''' |
|
|
image_vertices = vertices.copy() |
|
|
if is_perspective: |
|
|
|
|
|
image_vertices[:,0] = image_vertices[:,0]*w/2 |
|
|
image_vertices[:,1] = image_vertices[:,1]*h/2 |
|
|
|
|
|
image_vertices[:,0] = image_vertices[:,0] + w/2 |
|
|
image_vertices[:,1] = image_vertices[:,1] + h/2 |
|
|
|
|
|
image_vertices[:,1] = h - image_vertices[:,1] - 1 |
|
|
return image_vertices |
|
|
|
|
|
|
|
|
|
|
|
def estimate_affine_matrix_3d23d(X, Y): |
|
|
''' Using least-squares solution |
|
|
Args: |
|
|
X: [n, 3]. 3d points(fixed) |
|
|
Y: [n, 3]. corresponding 3d points(moving). Y = PX |
|
|
Returns: |
|
|
P_Affine: (3, 4). Affine camera matrix (the third row is [0, 0, 0, 1]). |
|
|
''' |
|
|
X_homo = np.hstack((X, np.ones([X.shape[1],1]))) |
|
|
P = np.linalg.lstsq(X_homo, Y)[0].T |
|
|
return P |
|
|
|
|
|
def estimate_affine_matrix_3d22d(X, x): |
|
|
''' Using Golden Standard Algorithm for estimating an affine camera |
|
|
matrix P from world to image correspondences. |
|
|
See Alg.7.2. in MVGCV |
|
|
Code Ref: https://github.com/patrikhuber/eos/blob/master/include/eos/fitting/affine_camera_estimation.hpp |
|
|
x_homo = X_homo.dot(P_Affine) |
|
|
Args: |
|
|
X: [n, 3]. corresponding 3d points(fixed) |
|
|
x: [n, 2]. n>=4. 2d points(moving). x = PX |
|
|
Returns: |
|
|
P_Affine: [3, 4]. Affine camera matrix |
|
|
''' |
|
|
X = X.T; x = x.T |
|
|
assert(x.shape[1] == X.shape[1]) |
|
|
n = x.shape[1] |
|
|
assert(n >= 4) |
|
|
|
|
|
|
|
|
|
|
|
mean = np.mean(x, 1) |
|
|
x = x - np.tile(mean[:, np.newaxis], [1, n]) |
|
|
average_norm = np.mean(np.sqrt(np.sum(x**2, 0))) |
|
|
scale = np.sqrt(2) / average_norm |
|
|
x = scale * x |
|
|
|
|
|
T = np.zeros((3,3), dtype = np.float32) |
|
|
T[0, 0] = T[1, 1] = scale |
|
|
T[:2, 2] = -mean*scale |
|
|
T[2, 2] = 1 |
|
|
|
|
|
|
|
|
X_homo = np.vstack((X, np.ones((1, n)))) |
|
|
mean = np.mean(X, 1) |
|
|
X = X - np.tile(mean[:, np.newaxis], [1, n]) |
|
|
m = X_homo[:3,:] - X |
|
|
average_norm = np.mean(np.sqrt(np.sum(X**2, 0))) |
|
|
scale = np.sqrt(3) / average_norm |
|
|
X = scale * X |
|
|
|
|
|
U = np.zeros((4,4), dtype = np.float32) |
|
|
U[0, 0] = U[1, 1] = U[2, 2] = scale |
|
|
U[:3, 3] = -mean*scale |
|
|
U[3, 3] = 1 |
|
|
|
|
|
|
|
|
A = np.zeros((n*2, 8), dtype = np.float32); |
|
|
X_homo = np.vstack((X, np.ones((1, n)))).T |
|
|
A[:n, :4] = X_homo |
|
|
A[n:, 4:] = X_homo |
|
|
b = np.reshape(x, [-1, 1]) |
|
|
|
|
|
|
|
|
p_8 = np.linalg.pinv(A).dot(b) |
|
|
P = np.zeros((3, 4), dtype = np.float32) |
|
|
P[0, :] = p_8[:4, 0] |
|
|
P[1, :] = p_8[4:, 0] |
|
|
P[-1, -1] = 1 |
|
|
|
|
|
|
|
|
P_Affine = np.linalg.inv(T).dot(P.dot(U)) |
|
|
return P_Affine |
|
|
|
|
|
def P2sRt(P): |
|
|
''' decompositing camera matrix P |
|
|
Args: |
|
|
P: (3, 4). Affine Camera Matrix. |
|
|
Returns: |
|
|
s: scale factor. |
|
|
R: (3, 3). rotation matrix. |
|
|
t: (3,). translation. |
|
|
''' |
|
|
t = P[:, 3] |
|
|
R1 = P[0:1, :3] |
|
|
R2 = P[1:2, :3] |
|
|
s = (np.linalg.norm(R1) + np.linalg.norm(R2))/2.0 |
|
|
r1 = R1/np.linalg.norm(R1) |
|
|
r2 = R2/np.linalg.norm(R2) |
|
|
r3 = np.cross(r1, r2) |
|
|
|
|
|
R = np.concatenate((r1, r2, r3), 0) |
|
|
return s, R, t |
|
|
|
|
|
|
|
|
def isRotationMatrix(R): |
|
|
''' checks if a matrix is a valid rotation matrix(whether orthogonal or not) |
|
|
''' |
|
|
Rt = np.transpose(R) |
|
|
shouldBeIdentity = np.dot(Rt, R) |
|
|
I = np.identity(3, dtype = R.dtype) |
|
|
n = np.linalg.norm(I - shouldBeIdentity) |
|
|
return n < 1e-6 |
|
|
|
|
|
def matrix2angle(R): |
|
|
''' get three Euler angles from Rotation Matrix |
|
|
Args: |
|
|
R: (3,3). rotation matrix |
|
|
Returns: |
|
|
x: pitch |
|
|
y: yaw |
|
|
z: roll |
|
|
''' |
|
|
assert(isRotationMatrix) |
|
|
sy = math.sqrt(R[0,0] * R[0,0] + R[1,0] * R[1,0]) |
|
|
|
|
|
singular = sy < 1e-6 |
|
|
|
|
|
if not singular : |
|
|
x = math.atan2(R[2,1] , R[2,2]) |
|
|
y = math.atan2(-R[2,0], sy) |
|
|
z = math.atan2(R[1,0], R[0,0]) |
|
|
else : |
|
|
x = math.atan2(-R[1,2], R[1,1]) |
|
|
y = math.atan2(-R[2,0], sy) |
|
|
z = 0 |
|
|
|
|
|
|
|
|
rx, ry, rz = x*180/np.pi, y*180/np.pi, z*180/np.pi |
|
|
return rx, ry, rz |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|