430 lines
12 KiB
Python
430 lines
12 KiB
Python
import copy
|
|
|
|
import numpy as np
|
|
|
|
from .. import util
|
|
|
|
|
|
class Camera(object):
|
|
|
|
def __init__(
|
|
self,
|
|
name=None,
|
|
resolution=None,
|
|
focal=None,
|
|
fov=None,
|
|
z_near=0.01,
|
|
z_far=1000.0):
|
|
"""
|
|
Create a new Camera object that stores camera intrinsic
|
|
and extrinsic parameters.
|
|
|
|
TODO: skew is not supported
|
|
TODO: cx and cy that are not half of width and height
|
|
|
|
Parameters
|
|
------------
|
|
name : str or None
|
|
Name for camera to be used as node name
|
|
resolution : (2,) int
|
|
Pixel size in (height, width)
|
|
focal : (2,) float
|
|
Focal length in pixels. Either pass this OR FOV
|
|
but not both. focal = (K[0][0], K[1][1])
|
|
fov : (2,) float
|
|
Field of view (fovx, fovy) in degrees
|
|
z_near : float
|
|
What is the closest
|
|
"""
|
|
|
|
if name is None:
|
|
# if name is not passed, make it something unique
|
|
self.name = 'camera_{}'.format(util.unique_id(6).upper())
|
|
else:
|
|
# otherwise assign it
|
|
self.name = name
|
|
|
|
if fov is None and focal is None:
|
|
raise ValueError('either focal length or FOV required!')
|
|
|
|
# store whether or not we computed the focal length
|
|
self._focal_computed = False
|
|
|
|
# set the passed (2,) float focal length
|
|
self.focal = focal
|
|
|
|
# set the passed (2,) float FOV in degrees
|
|
self.fov = fov
|
|
|
|
if resolution is None:
|
|
# if unset make resolution 30 pixels per degree
|
|
resolution = (self.fov * 30.0).round().astype(np.int64)
|
|
self.resolution = resolution
|
|
|
|
# what is the farthest from the camera it should render
|
|
self.z_far = float(z_far)
|
|
# what is the closest to the camera it should render
|
|
self.z_near = float(z_near)
|
|
|
|
def copy(self):
|
|
"""
|
|
Safely get a copy of the current camera.
|
|
"""
|
|
return Camera(
|
|
name=copy.deepcopy(self.name),
|
|
resolution=copy.deepcopy(self.resolution),
|
|
focal=copy.deepcopy(self.focal),
|
|
fov=copy.deepcopy(self.fov))
|
|
|
|
@property
|
|
def resolution(self):
|
|
"""
|
|
Get the camera resolution in pixels.
|
|
|
|
Returns
|
|
------------
|
|
resolution (2,) float
|
|
Camera resolution in pixels
|
|
"""
|
|
return self._resolution
|
|
|
|
@resolution.setter
|
|
def resolution(self, values):
|
|
"""
|
|
Set the camera resolution in pixels.
|
|
|
|
Parameters
|
|
------------
|
|
resolution (2,) float
|
|
Camera resolution in pixels
|
|
"""
|
|
values = np.asanyarray(values, dtype=np.int64)
|
|
if values.shape != (2,):
|
|
raise ValueError('resolution must be (2,) float')
|
|
values.flags.writeable = False
|
|
self._resolution = values
|
|
# unset computed value that depends on the other plus resolution
|
|
if self._focal_computed:
|
|
self._focal = None
|
|
else:
|
|
# fov must be computed
|
|
self._fov = None
|
|
|
|
@property
|
|
def focal(self):
|
|
"""
|
|
Get the focal length in pixels for the camera.
|
|
|
|
Returns
|
|
------------
|
|
focal : (2,) float
|
|
Focal length in pixels
|
|
"""
|
|
if self._focal is None:
|
|
# calculate focal length from FOV
|
|
focal = (
|
|
self._resolution / (2.0 * np.tan(np.radians(self._fov / 2.0))))
|
|
focal.flags.writeable = False
|
|
self._focal = focal
|
|
|
|
return self._focal
|
|
|
|
@focal.setter
|
|
def focal(self, values):
|
|
"""
|
|
Set the focal length in pixels for the camera.
|
|
|
|
Returns
|
|
------------
|
|
focal : (2,) float
|
|
Focal length in pixels.
|
|
"""
|
|
if values is None:
|
|
self._focal = None
|
|
else:
|
|
# flag this as not computed (hence fov must be)
|
|
# this is necessary so changes to resolution can reset the
|
|
# computed quantity without changing the explicitly set quantity
|
|
self._focal_computed = False
|
|
values = np.asanyarray(values, dtype=np.float64)
|
|
if values.shape != (2,):
|
|
raise ValueError('focal length must be (2,) float')
|
|
values.flags.writeable = False
|
|
# assign passed values to focal length
|
|
self._focal = values
|
|
# focal overrides FOV
|
|
self._fov = None
|
|
|
|
@property
|
|
def K(self):
|
|
"""
|
|
Get the intrinsic matrix for the Camera object.
|
|
|
|
Returns
|
|
-----------
|
|
K : (3, 3) float
|
|
Intrinsic matrix for camera
|
|
"""
|
|
K = np.eye(3, dtype=np.float64)
|
|
K[0, 0] = self.focal[0]
|
|
K[1, 1] = self.focal[1]
|
|
K[:2, 2] = self.resolution / 2.0
|
|
return K
|
|
|
|
@K.setter
|
|
def K(self, values):
|
|
if values is None:
|
|
return
|
|
values = np.asanyarray(values, dtype=np.float64)
|
|
if values.shape != (3, 3):
|
|
raise ValueError('matrix must be (3,3)!')
|
|
|
|
if not np.allclose(values.flatten()[[1, 3, 6, 7, 8]],
|
|
[0, 0, 0, 0, 1]):
|
|
raise ValueError(
|
|
'matrix should only have focal length and resolution!')
|
|
|
|
# set focal length from matrix
|
|
self.focal = [values[0, 0], values[1, 1]]
|
|
# set resolution from matrix
|
|
self.resolution = values[:2, 2] * 2
|
|
|
|
@property
|
|
def fov(self):
|
|
"""
|
|
Get the field of view in degrees.
|
|
|
|
Returns
|
|
-------------
|
|
fov : (2,) float
|
|
XY field of view in degrees
|
|
"""
|
|
if self._fov is None:
|
|
fov = 2.0 * np.degrees(
|
|
np.arctan((self._resolution / 2.0) / self._focal))
|
|
fov.flags.writeable = False
|
|
self._fov = fov
|
|
return self._fov
|
|
|
|
@fov.setter
|
|
def fov(self, values):
|
|
"""
|
|
Set the field of view in degrees.
|
|
|
|
Parameters
|
|
-------------
|
|
values : (2,) float
|
|
Size of FOV to set in degrees
|
|
"""
|
|
if values is None:
|
|
self._fov = None
|
|
else:
|
|
# flag this as computed (hence fov must not be)
|
|
# this is necessary so changes to resolution can reset the
|
|
# computed quantity without changing the explicitly set quantity
|
|
self._focal_computed = True
|
|
values = np.asanyarray(values, dtype=np.float64)
|
|
if values.shape != (2,):
|
|
raise ValueError('focal length must be (2,) int')
|
|
values.flags.writeable = False
|
|
# assign passed values to FOV
|
|
self._fov = values
|
|
# fov overrides focal
|
|
self._focal = None
|
|
|
|
def to_rays(self):
|
|
"""
|
|
Calculate ray direction vectors.
|
|
|
|
Will return one ray per pixel, as set in self.resolution.
|
|
|
|
Returns
|
|
--------------
|
|
vectors : (n, 3) float
|
|
Ray direction vectors in camera frame with z == -1
|
|
"""
|
|
return camera_to_rays(self)
|
|
|
|
def angles(self):
|
|
"""
|
|
Get ray spherical coordinates in radians.
|
|
|
|
|
|
Returns
|
|
--------------
|
|
angles : (n, 2) float
|
|
Ray spherical coordinate angles in radians.
|
|
"""
|
|
return np.arctan(-ray_pixel_coords(self))
|
|
|
|
def look_at(self, points, rotation=None, distance=None, center=None):
|
|
"""
|
|
Generate transform for a camera to keep a list
|
|
of points in the camera's field of view.
|
|
|
|
Parameters
|
|
-------------
|
|
points : (n, 3) float
|
|
Points in space
|
|
rotation : None, or (4, 4) float
|
|
Rotation matrix for initial rotation
|
|
distance : None or float
|
|
Distance from camera to center
|
|
center : None, or (3,) float
|
|
Center of field of view.
|
|
|
|
Returns
|
|
--------------
|
|
transform : (4, 4) float
|
|
Transformation matrix from world to camera
|
|
"""
|
|
return look_at(points,
|
|
fov=self.fov,
|
|
rotation=rotation,
|
|
distance=distance,
|
|
center=center)
|
|
|
|
def __repr__(self):
|
|
return '<trimesh.scene.Camera> FOV: {} Resolution: {}'.format(
|
|
self.fov, self.resolution)
|
|
|
|
|
|
def look_at(points, fov, rotation=None, distance=None, center=None):
|
|
"""
|
|
Generate transform for a camera to keep a list
|
|
of points in the camera's field of view.
|
|
|
|
Parameters
|
|
-------------
|
|
points : (n, 3) float
|
|
Points in space
|
|
fov : (2,) float
|
|
Field of view, in DEGREES
|
|
rotation : None, or (4, 4) float
|
|
Rotation matrix for initial rotation
|
|
distance : None or float
|
|
Distance from camera to center
|
|
center : None, or (3,) float
|
|
Center of field of view.
|
|
|
|
Returns
|
|
--------------
|
|
transform : (4, 4) float
|
|
Transformation matrix from world to camera
|
|
"""
|
|
|
|
if rotation is None:
|
|
rotation = np.eye(4)
|
|
else:
|
|
rotation = np.asanyarray(rotation, dtype=np.float64)
|
|
points = np.asanyarray(points, dtype=np.float64)
|
|
|
|
# Transform points to camera frame (just use the rotation part)
|
|
rinv = rotation[:3, :3].T
|
|
points_c = rinv.dot(points.T).T
|
|
|
|
if center is None:
|
|
# Find the center of the points' AABB in camera frame
|
|
center_c = points_c.min(axis=0) + 0.5 * points_c.ptp(axis=0)
|
|
else:
|
|
# Transform center to camera frame
|
|
center_c = rinv.dot(center)
|
|
|
|
# Re-center the points around the camera-frame origin
|
|
points_c -= center_c
|
|
|
|
# Find the minimum distance for the camera from the origin
|
|
# so that all points fit in the view frustrum
|
|
tfov = np.tan(np.radians(fov) / 2.0)
|
|
|
|
if distance is None:
|
|
distance = np.max(np.abs(points_c[:, :2]) /
|
|
tfov + points_c[:, 2][:, np.newaxis])
|
|
|
|
# set the pose translation
|
|
center_w = rotation[:3, :3].dot(center_c)
|
|
cam_pose = rotation.copy()
|
|
cam_pose[:3, 3] = center_w + distance * cam_pose[:3, 2]
|
|
|
|
return cam_pose
|
|
|
|
|
|
def ray_pixel_coords(camera):
|
|
"""
|
|
Get the x-y coordinates of rays in camera coordinates at
|
|
z == -1.
|
|
|
|
One coordinate pair will be given for each pixel as defined in
|
|
camera.resolution. If reshaped, the returned array corresponds
|
|
to pixels of the rendered image.
|
|
|
|
Examples
|
|
------------
|
|
```python
|
|
xy = ray_pixel_coords(camera).reshape(
|
|
tuple(camera.coordinates) + (2,))
|
|
top_left == xy[0, 0]
|
|
bottom_right == xy[-1, -1]
|
|
```
|
|
|
|
Parameters
|
|
--------------
|
|
camera : trimesh.scene.Camera
|
|
Camera object to generate rays from
|
|
|
|
Returns
|
|
--------------
|
|
xy : (n, 2) float
|
|
x-y coordinates of intersection of each camera ray
|
|
with the z == -1 frame
|
|
"""
|
|
# shorthand
|
|
res = camera.resolution
|
|
half_fov = np.radians(camera.fov) / 2.0
|
|
|
|
right_top = np.tan(half_fov)
|
|
# move half a pixel width in
|
|
right_top *= 1 - (1.0 / res)
|
|
left_bottom = -right_top
|
|
# we are looking down the negative z axis, so
|
|
# right_top corresponds to maximum x/y values
|
|
# bottom_left corresponds to minimum x/y values
|
|
right, top = right_top
|
|
left, bottom = left_bottom
|
|
|
|
# create a grid of vectors
|
|
xy = util.grid_linspace(
|
|
bounds=[[left, top], [right, bottom]],
|
|
count=camera.resolution)
|
|
|
|
# create a matching array of pixel indexes for the rays
|
|
pixels = util.grid_linspace(
|
|
bounds=[[0, res[1] - 1], [res[0] - 1, 0]],
|
|
count=res).astype(np.int64)
|
|
assert xy.shape == pixels.shape
|
|
|
|
return xy, pixels
|
|
|
|
|
|
def camera_to_rays(camera):
|
|
"""
|
|
Calculate the trimesh.scene.Camera object to direction vectors.
|
|
|
|
Will return one ray per pixel, as set in camera.resolution.
|
|
|
|
Parameters
|
|
--------------
|
|
camera : trimesh.scene.Camera
|
|
|
|
Returns
|
|
--------------
|
|
vectors : (n, 3) float
|
|
Ray direction vectors in camera frame with z == -1
|
|
"""
|
|
# get the on-plane coordinates
|
|
xy, pixels = ray_pixel_coords(camera)
|
|
# convert vectors to 3D unit vectors
|
|
vectors = util.unitize(
|
|
np.column_stack((xy, -np.ones_like(xy[:, :1]))))
|
|
return vectors, pixels
|