Source code for padvinder.camera

#!/usr/bin/python
# -*- coding: latin-1 -*-
"""
Cameras produce the initial ray from the camera position through the currently
rendered pixel and into the scene.

.. moduleauthor:: Adrian Köring
"""

import numpy as np
from padvinder.ray  import Ray
from padvinder.util import normalize
from padvinder.util import check_finite

[docs]class Camera(object): """ This Camera Model sets up and contains an orthonormal coordinate system. The cameras position is the starting positon of all rays fired into the scene. The rays through the center of the image will pass through the look_at point. Position and look_at define the optical axis. The up vector provides a vague upwards direction helping to orient the camera. Parameters ---------- position : numpy.ndarray_like the position of the camera in the scene, origin of the fired rays up : numpy.ndarray_like the general upwards direction of the camera look_at : numpy.ndarray_like the position in the scene that the camera will look at Raises ------ ValueError if any of position, up or look_at contain Infs or NaNs or if the position and look_at are at the same point Examples -------- >>> Camera() >>> Camera((0,0,0), (0,1,0), (0,0,-1), 35) Camera(position=[0.0, 0.0, -1.0], up=[0.0, 1.0, 0.0], look_at=[0.0, 0.0, 0.0]) """ def __init__(self, position = (0,0,0), up = (0,1,0), look_at = (0,0,1)): check_finite(position, up, look_at) if np.isclose(position, look_at).all(): raise ValueError("Position and look_at of a Camera can not be " + "the same point, move either one away") self._position = np.array(position, dtype=np.float64) self._optical_axis = None self._righthand = None self._up = None self._build_coordinate_system(up, look_at - self._position) def _build_coordinate_system(self, vague_up, optical_axis): """ Given the rough user specified setup of a camera, create an orthonormal coordinate system. Parameters ---------- vague_up : numpy.ndarray_like the general upwards direction of the camera. optical_axis : numpy.ndarray_like the direction in which the optical axis of the camera points - will be orthongonal to the image plane. """ vague_up = normalize(vague_up) self._optical_axis = normalize(optical_axis) self._righthand = np.cross(vague_up, self._optical_axis) self._up = np.cross(self._optical_axis, self._righthand) @property def position(self): """ Return the position of the camera. """ return self._position @property def up(self): """ Return the up vector of the camera. """ return self._up @property def optical_axis(self): """ Return the optical axis of the camera. """ return self._optical_axis
[docs] def ray(self, pixel, resolutions, rand = True): """ Given the pixel and the camera resolution, returns a ray that originates at the camera position and passes through the pixel. If rand is set true, a little random offset (smaller than the distance between two pixels is added to the pixel position. This will together with multiple samples per pixel mitigate aliasing. Parameters ---------- pixel : numpy.ndarray_like (x, y) coordinates of the pixel in the image - numpy style. Aka (0, 0) is the upper left hand corner and the x values are iterating downwards while y is iterating horizontally. x must be in the intervall of [0, dimension[0]] and y must be in [0, dimension[1]] The pixel [0,0] is the upper lefthand corner and the pixel [res_x, rex_y] is the lower righthand corner. resolutions : numpy.ndarray_like (res_x, resx_y) the resolution of the camera in x and y. rand : boolean When False, every ray passes through the exact center of the pixel. When True a random offset smaller than the distance between two pixels is added the the pixel center. The ray then passes through the perturbed pixel center. Returns ------- ray : Ray with the position being the camera position and direction being a vector that starts at the position and passes through the (potentiall offsetted) given pixel Raises ------ NotImplemeted because this is an abstract base class Examples -------- >>> camera = PerspectiveCamera() >>> camera.ray((0,0), (100, 100), False) """ raise NotImplemented()
def __repr__(self): return "Camera(position={}, ".format(self._position) \ + "up={}, ".format(self._up) \ + "optical axis={})".format(self._optical_axis)
[docs]class PerspectiveCamera(Camera): """ The Perspective Camera Model extends the orthonormal coordinate system with a focal length and therefore a concrete image plane. The cameras position is the starting positon of all rays fired into the scene. The rays through the center of the image will pass through the look_at point. Position and look_at define the optical axis. The up vector provides a vague upwards direction helping to orient the camera. The focal length defines how far the 35mm equivalent sized image plane is from the camera position. Parameters ---------- position : numpy.ndarray_like the position of the camera in the scene, origin of the fired rays up : numpy.ndarray_like the general upwards direction of the camera look_at : numpy.ndarray_like the position in the scene that the camera will look at focal_length : float the distance in mm between the position and the image plane - must be in the intervall of (0, +inf). Raises ------ ValueError if any of position, up or look_at contain Infs or NaNs or if the position and look_at are at the same point of ir the focal_length is not positive Examples -------- >>> PerspectiveCamera() >>> PerspectiveCamera((0,0,1), (0,1,0), (0,0,0), 24) Camera(position=[0.0, 0.0, 1.0], up=[0.0, 1.0, 0.0], look_at=[0.0, 0.0, 0.0], focal_length=24) """ def __init__(self, position = (0,0,0), up = (0,1,0), look_at = (0,0,1), focal_length = 24): super().__init__(position, up, look_at) if focal_length <= 0: raise ValueError("Focal length must be larger than zero") self._focal_length = focal_length self._image_plane_center = (self.position + (self.focal_length/24.) * self.optical_axis) @property def focal_length(self): """ Return the focal length of the camera. """ return self._focal_length
[docs] def ray(self, pixel, resolutions, rand = True): """ Given the pixel and the camera resolution, returns a ray that originates at the camera position and passes through the pixel. If rand is set true, a little random offset (smaller than the distance between two pixels is added to the pixel position. This will together with multiple samples per pixel mitigate aliasing. Parameters ---------- pixel : numpy.ndarray_like of shape (2, ) (x, y) coordinates of the pixel in the image. Numpy style: aka (0, 0) is the upper left hand corner and the x values are iterating downwards while y is iterating horizontally. x must be in the intervall of [0, dimension[0]] and y must be in [0, dimension[1]] The pixel [0,0] is the upper lefthand corner and the pixel [res_x, rex_y] is the lower righthand corner. resolutions : numpy.ndarray_like of shape (2, ) the resolution of the camera in x and y. rand : boolean When False, every ray passes through the exact center of the pixel. When True a random offset smaller than the distance between two pixels is added the the pixel center. The ray then passes through the perturbed pixel center. Returns ------- ray : Ray with the position being the camera position and direction being a vector that starts at the position and passes through the (potentiall offsetted) given pixel Examples -------- >>> camera = PerspectiveCamera() >>> camera.ray((50, 50), (100, 100), False) Ray(position=[0, 0, 0], direction=[0, 0, 1]) """ pixel_x, pixel_y = pixel res_x, res_y = resolutions max_dim = np.maximum(res_x, res_y) # image plane coordinates (x, y) of the pixel # x and y are in [-1, 1] x = (res_x - 2*pixel_x) / max_dim y = (res_y - 2*pixel_y) / max_dim if rand: # add a small perturbation to the pixel coordinate delta_x, delta_y = 1/res_x, 1/res_y sample_x, sample_y = np.random.uniform(-1, 1, size=(2,)) x += sample_x * delta_x y += sample_y * delta_y # Numpy Style - x is vertical - y is horizontal world_coord = self._image_plane_center + x*self._up + y*self._righthand return Ray(self._position, world_coord - self._position)
def __repr__(self): return ("PerspectiveCamera(position={}, ".format(self._position) + "up={}, ".format(self._up) + "optical axis={}, ".format(self._optical_axis) + "focal length={})".format(self._focal_length))