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