1import copy 2 3import numpy as np 4 5from .. import util 6 7 8class Camera(object): 9 10 def __init__( 11 self, 12 name=None, 13 resolution=None, 14 focal=None, 15 fov=None, 16 z_near=0.01, 17 z_far=1000.0): 18 """ 19 Create a new Camera object that stores camera intrinsic 20 and extrinsic parameters. 21 22 TODO: skew is not supported 23 TODO: cx and cy that are not half of width and height 24 25 Parameters 26 ------------ 27 name : str or None 28 Name for camera to be used as node name 29 resolution : (2,) int 30 Pixel size in (height, width) 31 focal : (2,) float 32 Focal length in pixels. Either pass this OR FOV 33 but not both. focal = (K[0][0], K[1][1]) 34 fov : (2,) float 35 Field of view (fovx, fovy) in degrees 36 z_near : float 37 What is the closest 38 """ 39 40 if name is None: 41 # if name is not passed, make it something unique 42 self.name = 'camera_{}'.format(util.unique_id(6).upper()) 43 else: 44 # otherwise assign it 45 self.name = name 46 47 if fov is None and focal is None: 48 raise ValueError('either focal length or FOV required!') 49 50 # store whether or not we computed the focal length 51 self._focal_computed = False 52 53 # set the passed (2,) float focal length 54 self.focal = focal 55 56 # set the passed (2,) float FOV in degrees 57 self.fov = fov 58 59 if resolution is None: 60 # if unset make resolution 30 pixels per degree 61 resolution = (self.fov * 30.0).round().astype(np.int64) 62 self.resolution = resolution 63 64 # what is the farthest from the camera it should render 65 self.z_far = float(z_far) 66 # what is the closest to the camera it should render 67 self.z_near = float(z_near) 68 69 def copy(self): 70 """ 71 Safely get a copy of the current camera. 72 """ 73 return Camera( 74 name=copy.deepcopy(self.name), 75 resolution=copy.deepcopy(self.resolution), 76 focal=copy.deepcopy(self.focal), 77 fov=copy.deepcopy(self.fov)) 78 79 @property 80 def resolution(self): 81 """ 82 Get the camera resolution in pixels. 83 84 Returns 85 ------------ 86 resolution (2,) float 87 Camera resolution in pixels 88 """ 89 return self._resolution 90 91 @resolution.setter 92 def resolution(self, values): 93 """ 94 Set the camera resolution in pixels. 95 96 Parameters 97 ------------ 98 resolution (2,) float 99 Camera resolution in pixels 100 """ 101 values = np.asanyarray(values, dtype=np.int64) 102 if values.shape != (2,): 103 raise ValueError('resolution must be (2,) float') 104 values.flags.writeable = False 105 self._resolution = values 106 # unset computed value that depends on the other plus resolution 107 if self._focal_computed: 108 self._focal = None 109 else: 110 # fov must be computed 111 self._fov = None 112 113 @property 114 def focal(self): 115 """ 116 Get the focal length in pixels for the camera. 117 118 Returns 119 ------------ 120 focal : (2,) float 121 Focal length in pixels 122 """ 123 if self._focal is None: 124 # calculate focal length from FOV 125 focal = ( 126 self._resolution / (2.0 * np.tan(np.radians(self._fov / 2.0)))) 127 focal.flags.writeable = False 128 self._focal = focal 129 130 return self._focal 131 132 @focal.setter 133 def focal(self, values): 134 """ 135 Set the focal length in pixels for the camera. 136 137 Returns 138 ------------ 139 focal : (2,) float 140 Focal length in pixels. 141 """ 142 if values is None: 143 self._focal = None 144 else: 145 # flag this as not computed (hence fov must be) 146 # this is necessary so changes to resolution can reset the 147 # computed quantity without changing the explicitly set quantity 148 self._focal_computed = False 149 values = np.asanyarray(values, dtype=np.float64) 150 if values.shape != (2,): 151 raise ValueError('focal length must be (2,) float') 152 values.flags.writeable = False 153 # assign passed values to focal length 154 self._focal = values 155 # focal overrides FOV 156 self._fov = None 157 158 @property 159 def K(self): 160 """ 161 Get the intrinsic matrix for the Camera object. 162 163 Returns 164 ----------- 165 K : (3, 3) float 166 Intrinsic matrix for camera 167 """ 168 K = np.eye(3, dtype=np.float64) 169 K[0, 0] = self.focal[0] 170 K[1, 1] = self.focal[1] 171 K[:2, 2] = self.resolution / 2.0 172 return K 173 174 @K.setter 175 def K(self, values): 176 if values is None: 177 return 178 values = np.asanyarray(values, dtype=np.float64) 179 if values.shape != (3, 3): 180 raise ValueError('matrix must be (3,3)!') 181 182 if not np.allclose(values.flatten()[[1, 3, 6, 7, 8]], 183 [0, 0, 0, 0, 1]): 184 raise ValueError( 185 'matrix should only have focal length and resolution!') 186 187 # set focal length from matrix 188 self.focal = [values[0, 0], values[1, 1]] 189 # set resolution from matrix 190 self.resolution = values[:2, 2] * 2 191 192 @property 193 def fov(self): 194 """ 195 Get the field of view in degrees. 196 197 Returns 198 ------------- 199 fov : (2,) float 200 XY field of view in degrees 201 """ 202 if self._fov is None: 203 fov = 2.0 * np.degrees( 204 np.arctan((self._resolution / 2.0) / self._focal)) 205 fov.flags.writeable = False 206 self._fov = fov 207 return self._fov 208 209 @fov.setter 210 def fov(self, values): 211 """ 212 Set the field of view in degrees. 213 214 Parameters 215 ------------- 216 values : (2,) float 217 Size of FOV to set in degrees 218 """ 219 if values is None: 220 self._fov = None 221 else: 222 # flag this as computed (hence fov must not be) 223 # this is necessary so changes to resolution can reset the 224 # computed quantity without changing the explicitly set quantity 225 self._focal_computed = True 226 values = np.asanyarray(values, dtype=np.float64) 227 if values.shape != (2,): 228 raise ValueError('focal length must be (2,) int') 229 values.flags.writeable = False 230 # assign passed values to FOV 231 self._fov = values 232 # fov overrides focal 233 self._focal = None 234 235 def to_rays(self): 236 """ 237 Calculate ray direction vectors. 238 239 Will return one ray per pixel, as set in self.resolution. 240 241 Returns 242 -------------- 243 vectors : (n, 3) float 244 Ray direction vectors in camera frame with z == -1 245 """ 246 return camera_to_rays(self) 247 248 def angles(self): 249 """ 250 Get ray spherical coordinates in radians. 251 252 253 Returns 254 -------------- 255 angles : (n, 2) float 256 Ray spherical coordinate angles in radians. 257 """ 258 return np.arctan(-ray_pixel_coords(self)) 259 260 def look_at(self, points, rotation=None, distance=None, center=None): 261 """ 262 Generate transform for a camera to keep a list 263 of points in the camera's field of view. 264 265 Parameters 266 ------------- 267 points : (n, 3) float 268 Points in space 269 rotation : None, or (4, 4) float 270 Rotation matrix for initial rotation 271 distance : None or float 272 Distance from camera to center 273 center : None, or (3,) float 274 Center of field of view. 275 276 Returns 277 -------------- 278 transform : (4, 4) float 279 Transformation matrix from world to camera 280 """ 281 return look_at(points, 282 fov=self.fov, 283 rotation=rotation, 284 distance=distance, 285 center=center) 286 287 def __repr__(self): 288 return '<trimesh.scene.Camera> FOV: {} Resolution: {}'.format( 289 self.fov, self.resolution) 290 291 292def look_at(points, fov, rotation=None, distance=None, center=None): 293 """ 294 Generate transform for a camera to keep a list 295 of points in the camera's field of view. 296 297 Parameters 298 ------------- 299 points : (n, 3) float 300 Points in space 301 fov : (2,) float 302 Field of view, in DEGREES 303 rotation : None, or (4, 4) float 304 Rotation matrix for initial rotation 305 distance : None or float 306 Distance from camera to center 307 center : None, or (3,) float 308 Center of field of view. 309 310 Returns 311 -------------- 312 transform : (4, 4) float 313 Transformation matrix from world to camera 314 """ 315 316 if rotation is None: 317 rotation = np.eye(4) 318 else: 319 rotation = np.asanyarray(rotation, dtype=np.float64) 320 points = np.asanyarray(points, dtype=np.float64) 321 322 # Transform points to camera frame (just use the rotation part) 323 rinv = rotation[:3, :3].T 324 points_c = rinv.dot(points.T).T 325 326 if center is None: 327 # Find the center of the points' AABB in camera frame 328 center_c = points_c.min(axis=0) + 0.5 * points_c.ptp(axis=0) 329 else: 330 # Transform center to camera frame 331 center_c = rinv.dot(center) 332 333 # Re-center the points around the camera-frame origin 334 points_c -= center_c 335 336 # Find the minimum distance for the camera from the origin 337 # so that all points fit in the view frustrum 338 tfov = np.tan(np.radians(fov) / 2.0) 339 340 if distance is None: 341 distance = np.max(np.abs(points_c[:, :2]) / 342 tfov + points_c[:, 2][:, np.newaxis]) 343 344 # set the pose translation 345 center_w = rotation[:3, :3].dot(center_c) 346 cam_pose = rotation.copy() 347 cam_pose[:3, 3] = center_w + distance * cam_pose[:3, 2] 348 349 return cam_pose 350 351 352def ray_pixel_coords(camera): 353 """ 354 Get the x-y coordinates of rays in camera coordinates at 355 z == -1. 356 357 One coordinate pair will be given for each pixel as defined in 358 camera.resolution. If reshaped, the returned array corresponds 359 to pixels of the rendered image. 360 361 Examples 362 ------------ 363 ```python 364 xy = ray_pixel_coords(camera).reshape( 365 tuple(camera.coordinates) + (2,)) 366 top_left == xy[0, 0] 367 bottom_right == xy[-1, -1] 368 ``` 369 370 Parameters 371 -------------- 372 camera : trimesh.scene.Camera 373 Camera object to generate rays from 374 375 Returns 376 -------------- 377 xy : (n, 2) float 378 x-y coordinates of intersection of each camera ray 379 with the z == -1 frame 380 """ 381 # shorthand 382 res = camera.resolution 383 half_fov = np.radians(camera.fov) / 2.0 384 385 right_top = np.tan(half_fov) 386 # move half a pixel width in 387 right_top *= 1 - (1.0 / res) 388 left_bottom = -right_top 389 # we are looking down the negative z axis, so 390 # right_top corresponds to maximum x/y values 391 # bottom_left corresponds to minimum x/y values 392 right, top = right_top 393 left, bottom = left_bottom 394 395 # create a grid of vectors 396 xy = util.grid_linspace( 397 bounds=[[left, top], [right, bottom]], 398 count=camera.resolution) 399 400 # create a matching array of pixel indexes for the rays 401 pixels = util.grid_linspace( 402 bounds=[[0, res[1]], [res[0], 0]], 403 count=res).astype(np.int64) 404 assert xy.shape == pixels.shape 405 406 return xy, pixels 407 408 409def camera_to_rays(camera): 410 """ 411 Calculate the trimesh.scene.Camera object to direction vectors. 412 413 Will return one ray per pixel, as set in camera.resolution. 414 415 Parameters 416 -------------- 417 camera : trimesh.scene.Camera 418 419 Returns 420 -------------- 421 vectors : (n, 3) float 422 Ray direction vectors in camera frame with z == -1 423 """ 424 # get the on-plane coordinates 425 xy, pixels = ray_pixel_coords(camera) 426 # convert vectors to 3D unit vectors 427 vectors = util.unitize( 428 np.column_stack((xy, -np.ones_like(xy[:, :1])))) 429 return vectors, pixels 430