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