import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from ipywidgets import interact, FloatSlider
Determining Camera XY-Axis in MuJoCo
Introduction
In mujoco schemas you can define camera objects like the following:
camera name="my_camera" pos="0 0 2" xyaxes="1 0 0 0 1 0"/> <
This camera has default xyaxes values and depending on it’s frame of reference it’s “lookat” vector is determined by the z-axis orthogonal to the xyaxes: \(\mathbf{z} = [0, 0, 1]^T\).
Now if we want to do the opposite, that is, to determine xyaxes from the \(\mathbf{z}\) vector we need to do a bit of linear algebra. We need to construct an orthonormal set of vectors \({\mathbf{x}, \mathbf{y}, \mathbf{z}}\). In this problem we know \(\mathbf{z}\) but we need t o determine a plausible xyaxes. We can do so by first considering a non-parrallel normal vector \(\mathbf{u}\) to \(\mathbf{z}\):
\[ \mathbf{x} = \frac{\mathbf{u} \times \mathbf{z}}{\|\mathbf{u} \times \mathbf{z}\|} \]
And finally to get \(\mathbf{y}\):
\[ \mathbf{y} = \mathbf{z} \times \mathbf{x} \]
This produces a valid orthonormal set \({\mathbf{x}, \mathbf{y}, \mathbf{z}}\) for our camera object.
Implementation
def orthonormal_basis_from_z(z):
"""
Constructs an orthonormal basis (x, y, z) given a z-axis vector.
Args:
z (np.ndarray): A 3-element array representing the z-axis direction.
Returns:
tuple: A tuple of three np.ndarray vectors (x, y, z), each of shape (3,), forming an orthonormal basis.
"""
= z / np.linalg.norm(z) # normalize z (or ensure it's normalized)
z
## Example
## If z = [0,0,1] then u = [1,0,0]
## Else z = [0,1,0] then u = [0,0,1]
= np.array([0, 0, 1]) if np.abs(z[2]) < 0.999 else np.array([0, 1, 0])
u
= np.cross(u, z) # x = u cross z
x /= np.linalg.norm(x) # normalize x
x = np.cross(z, x) # y = z cross x
y return x, y, z
# Test orthonormal_basis_from_z with a known z vector
= np.array([0, 0, 1])
z_test = orthonormal_basis_from_z(z_test)
x, y, z print(x,y,z)
# Check orthonormality
assert np.allclose(np.dot(x, y), 0), "x and y are not orthogonal"
assert np.allclose(np.dot(y, z), 0), "y and z are not orthogonal"
assert np.allclose(np.dot(z, x), 0), "z and x are not orthogonal"
assert np.allclose(np.linalg.norm(x), 1), "x is not unit length"
assert np.allclose(np.linalg.norm(y), 1), "y is not unit length"
assert np.allclose(np.linalg.norm(z), 1), "z is not unit length"
print("All assertions passed.")
[1. 0. 0.] [0. 1. 0.] [0. 0. 1.]
All assertions passed.
def plot_basis(x, y, z, origin=np.zeros(3)):
= plt.figure(figsize=(6,6))
fig = fig.add_subplot(111, projection='3d')
ax
*origin, *x, color='r', label='x', length=1, normalize=True)
ax.quiver(*origin, *y, color='g', label='y', length=1, normalize=True)
ax.quiver(*origin, *z, color='b', label='z', length=1, normalize=True)
ax.quiver(
-1, 1])
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim(['X')
ax.set_xlabel('Y')
ax.set_ylabel('Z')
ax.set_zlabel("Interactive Orthonormal Basis from z-axis")
ax.set_title(
ax.legend([f"x: [{x[0]:.2f}, {x[1]:.2f}, {x[2]:.2f}]",
f"y: [{y[0]:.2f}, {y[1]:.2f}, {y[2]:.2f}]",
f"z: [{z[0]:.2f}, {z[1]:.2f}, {z[2]:.2f}]"
])
plt.tight_layout() plt.show()
def update(zx, zy, zz):
= np.array([zx, zy, zz])
z_vec if np.linalg.norm(z_vec) < 1e-6:
print("Z norm is too small")
return
= orthonormal_basis_from_z(z_vec)
x, y, z plot_basis(x, y, z)
interact(
update,=FloatSlider(min=-1, max=1, step=0.1, value=0.0, description='z_x'),
zx=FloatSlider(min=-1, max=1, step=0.1, value=0.0, description='z_y'),
zy=FloatSlider(min=-1, max=1, step=0.1, value=1.0, description='z_z'),
zz )
<function __main__.update(zx, zy, zz)>