Pinhole Lens
In this tutorial you will get familiar with Pinhole Lens, which is a sensor that projects 3D points on the scene
to the image plane with perspective transformation. It is the most used type of lens, as its mechanism is the same
as in the human eye or commonly used cameras.
Agenda:
- Pinhole lens model details
- PinholeLens - render and undistortion
- PinholeLens - parameters change
Scene setup
Let's use custom scene composer to set up the scene.
from skyrenderer.cases.utils import SensorSceneComposer
scene_composer = SensorSceneComposer(antialiasing_level=2048)
scene_composer.setup_scene(camera_position=[0, 0, 15])
renderer_context = scene_composer.renderer_context
2025-02-04 13:07:21,171 | skyrenderer.scene.renderer_context | INFO: Root paths:
- root path: /dli/skyenvironment/skyrenderer/skyrenderer
- assets path: /dli/mount/assets
- config path: /dli/skyenvironment/skyrenderer/skyrenderer/config
- gpu sources path: /dli/skyenvironment/skyrenderer/skyrenderer/optix_sources/sources
- cache path: /dli/mount/cache
- ptx cache path: compiled_ptx/ptx
- ocio path: ocio_configs/aces_1.2/config.ocio
Pinhole lens model details
In generalized Lens projection model (INTRO_Lens), following re-projection steps are done:
- (u,v) are reprojected onto projection plane (z=1).
- Points are being undistorted, to match original ray directions (using distortion back mapping and
precalculated distortion map). - Based on re-projection radial distance and projection equation ray angle (theta) is calculated.
- Projection sphere coordinates are reconstructed (unit vector with angle theta between incident plane pi and
sensor optical axis). - Ray made from O and P' is generated.
Pinhole camera distortion (2nd step)
In real cameras, lenses introduce distortion, especially radial and tangential distortion, which can affect the
image. These distortions are most apparent at the edges of the image and arise due to the physical properties of
the lens used in the camera.
Distorted coordinates of a point are transformed with radial based and tangential distortion as follows:
\begin{equation}
\begin{bmatrix}
u \
v
\end{bmatrix} = \begin{bmatrix}
f_x x' + c_x \
f_y y' + c_y
\end{bmatrix}
\end{equation}
where
\begin{align}
\begin{bmatrix}
x' \
y'
\end{bmatrix} &= \begin{bmatrix}
x \cdot r_f + 2p_1 \cdot x \cdot y + p_2 \cdot (r^2+2x^2) + s_1 \cdot r^2 + s_2 \cdot r^4 \
y \cdot r_f + p_1 \cdot (r^2+2y^2) + 2p_2 \cdot x \cdot y + s_3 \cdot r^2 + s_4 \cdot r^4 \
\end{bmatrix} \
r_f &= \dfrac{1+k_1\cdot r^2+k_2\cdot r^4+k_3\cdot r^6}{1+k_4\cdot r^2+k_5\cdot r^4+k_6\cdot r^6} \
r &= x^2+y^2 \
\begin{bmatrix}
x\
y
\end{bmatrix} &= \begin{bmatrix}
X_c/Z_c \
Y_c/Z_c
\end{bmatrix} \
\end{align}
Pinhole re-projection (3rd step)
In pinhole model incident angle and sensor matrix radius are connected with each other according to equation:
\begin{align}
r &= f \cdot \tan{\theta} \to r = \tan \theta, & \text{when focal length is normalized} \
\end{align}
Camera extrinsic parameters
Extrinsic parameters describe the camera's position and orientation in the 3D world. They consist of:
- Rotation ($R$): The orientation of the camera relative to the world.
- Translation ($T$): The position of the camera center relative to the world origin.
The extrinsic parameters form the 3x4 camera extrinsic matrix $[R|T]$, which transforms world coordinates into
camera coordinates. They are stored in SceneLayoutNode/SceneNode transformations.
Camera intrinsic parameters
Intrinsic parameters stores the properties of the camera itself that affect how 3D points are mapped to
2D coordinates. They include:
- Focal length ($f$) - the distance from the pinhole to the image plane.
- Principal point ($c_x, c_y$) - the point where the principal axis intersects the image plane (often the image
center).
Intrinsic parameters are passed in the lens parameters. More about the influence of intrinsic parameters on ray
generation can be found in INTRO_Lens.py tutorial.
PinholeLens - render and undistortion
In our implementation of the PinholeLens, we can adjust parameters presented in the section above.
In this example, we will adjust camera focal length and principal point, as well as all distortions parameters.
PinholeLens setup and visualization
# Image resolution
width = 1000
height = 1000
# Camera intrinsic parameters
camera_fx = 750
camera_fy = 750
camera_cx = width / 2
camera_cy = height / 2
# Camera distortion parameters
dist_k1 = -0.2
dist_k2 = 0.1660696363841201
dist_k3 = -0.1046838971475154
dist_k4 = -0.05589875277472606
dist_k5 = 0
dist_k6 = 0
dist_p1 = -0.00104636649317819
dist_p2 = -0.00168027284332261
dist_s1 = 0.01253
dist_s2 = 0.05434
dist_s3 = 0.08452
dist_s4 = 0.01137
dist_sampling_factor = 4
dist_range_extension = 0
We can pass the above parameters to the PinholeLens through parameter provider, and pass parametrized lens to
VisibleLightRenderStep as in SENSOR_PinholeLens tutorial.
from skyrenderer.render_chain import RenderChain, VisibleLightRenderStep, Denoiser
from skyrenderer.render_chain.camera_steps.Lens.pinhole_lens import PinholeLens
pinhole_params = PinholeLens.create_parameter_provider(
renderer_context,
fx=camera_fx,
fy=camera_fy,
cx_relative=0.5,
cy_relative=0.5,
dist_k1=dist_k1,
dist_k2=dist_k2,
dist_k3=dist_k3,
dist_k4=dist_k4,
dist_k5=dist_k5,
dist_k6=dist_k6,
dist_p1=dist_p1,
dist_p2=dist_p2,
dist_s1=dist_s1,
dist_s2=dist_s2,
dist_s3=dist_s3,
dist_s4=dist_s4,
dist_sampling_factor=dist_sampling_factor,
dist_range_extension=dist_range_extension,
)
lens = PinholeLens(renderer_context, parameter_provider=pinhole_params)
rs = VisibleLightRenderStep(
renderer_context,
lens=lens,
origin_name="camera_CAM_NUL",
target_name="top_node",
)
renderer_context.define_render_chain(
RenderChain(render_steps=[rs, Denoiser(renderer_context)], width=width, height=height)
)
distorted_image = scene_composer.get_render()
scene_composer.visualize(distorted_image)
2025-02-04 13:07:21,768 | skyrenderer.utils.time_measurement | INFO: Setup time: 544 ms
2025-02-04 13:07:22,485 | skyrenderer.utils.time_measurement | INFO: Context update time: 716 ms
2025-02-04 13:07:23,351 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:23,352 | skyrenderer.utils.time_measurement | INFO: Render time: 864 ms
Image undistortion
Our Pinhole model implementation is compatible with the state-of-the-art solutions (e.g. OpenCV), therefore we can
perform undistortion on the rendered image with 3rd party library:
import cv2 as cv
import numpy as np
camera_matrix = np.array([[camera_fx, 0, camera_cx], [0, camera_fy, camera_cy], [0, 0, 1]], np.float)
dist_coeffs = np.array(
[dist_k1, dist_k2, dist_p1, dist_p2, dist_k3, dist_k4, dist_k5, dist_k6, dist_s1, dist_s2, dist_s3, dist_s4],
np.float,
)
img_undistorted = cv.undistort(distorted_image, camera_matrix, dist_coeffs)
scene_composer.visualize(img_undistorted)
PinholeLens - parameters change
fx, fy - focal length
width = 450
height = 450
renders = {}
scene_composer.setup_pinhole_lens(width, height, fx=200)
renders["fx=200"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=915)
renders["fx=915(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=1500)
renders["fx=1500"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=450, fy=200)
renders["fy=200"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fy=915)
renders["fy=915(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fy=1500)
renders["fy=1500"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=200, fy=200)
renders["fx=fy=200"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=915, fy=915)
renders["fx=fy=915(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, fx=1500, fy=1500)
renders["fx=fy=1500"] = scene_composer.get_render()
scene_composer.visualize_grid_desc(renders, shape=(3 height, 3 width), n_cols=3)
2025-02-04 13:07:23,959 | skyrenderer.utils.time_measurement | INFO: Setup time: 48 ms
2025-02-04 13:07:24,489 | skyrenderer.utils.time_measurement | INFO: Context update time: 529 ms
2025-02-04 13:07:25,456 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:25,457 | skyrenderer.utils.time_measurement | INFO: Render time: 968 ms
2025-02-04 13:07:25,511 | skyrenderer.utils.time_measurement | INFO: Setup time: 49 ms
2025-02-04 13:07:26,048 | skyrenderer.utils.time_measurement | INFO: Context update time: 536 ms
2025-02-04 13:07:26,231 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:26,232 | skyrenderer.utils.time_measurement | INFO: Render time: 184 ms
2025-02-04 13:07:26,283 | skyrenderer.utils.time_measurement | INFO: Setup time: 47 ms
2025-02-04 13:07:26,811 | skyrenderer.utils.time_measurement | INFO: Context update time: 527 ms
2025-02-04 13:07:26,996 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:26,998 | skyrenderer.utils.time_measurement | INFO: Render time: 185 ms
2025-02-04 13:07:27,050 | skyrenderer.utils.time_measurement | INFO: Setup time: 48 ms
2025-02-04 13:07:27,582 | skyrenderer.utils.time_measurement | INFO: Context update time: 531 ms
2025-02-04 13:07:28,510 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:28,511 | skyrenderer.utils.time_measurement | INFO: Render time: 928 ms
2025-02-04 13:07:28,564 | skyrenderer.utils.time_measurement | INFO: Setup time: 49 ms
2025-02-04 13:07:29,091 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:29,979 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:29,980 | skyrenderer.utils.time_measurement | INFO: Render time: 888 ms
2025-02-04 13:07:30,098 | skyrenderer.utils.time_measurement | INFO: Setup time: 113 ms
2025-02-04 13:07:30,625 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:31,560 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:31,561 | skyrenderer.utils.time_measurement | INFO: Render time: 936 ms
2025-02-04 13:07:31,611 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:32,138 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:33,015 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:33,015 | skyrenderer.utils.time_measurement | INFO: Render time: 877 ms
2025-02-04 13:07:33,065 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:33,590 | skyrenderer.utils.time_measurement | INFO: Context update time: 525 ms
2025-02-04 13:07:34,507 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:34,508 | skyrenderer.utils.time_measurement | INFO: Render time: 916 ms
2025-02-04 13:07:34,558 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:35,120 | skyrenderer.utils.time_measurement | INFO: Context update time: 562 ms
2025-02-04 13:07:36,085 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:36,086 | skyrenderer.utils.time_measurement | INFO: Render time: 965 ms
cx_relative, cy_relative - center of projection
renders = {}
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.2)
renders["cx=0.2"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.5)
renders["cx=0.5(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.8)
renders["cx=0.8"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.5, cy_relative=0.2)
renders["cy=0.2"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cy_relative=0.5)
renders["cy=0.5(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cy_relative=0.8)
renders["cy=0.8"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.2, cy_relative=0.2)
renders["cx=cy=0.2"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.5, cy_relative=0.5)
renders["cx=cy=0.5(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.8, cy_relative=0.8)
renders["cx=cy=0.8"] = scene_composer.get_render()
scene_composer.visualize_grid_desc(renders, shape=(3 height, 3 width), n_cols=3)
2025-02-04 13:07:36,837 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:37,369 | skyrenderer.utils.time_measurement | INFO: Context update time: 531 ms
2025-02-04 13:07:37,551 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:37,552 | skyrenderer.utils.time_measurement | INFO: Render time: 182 ms
2025-02-04 13:07:37,607 | skyrenderer.utils.time_measurement | INFO: Setup time: 50 ms
2025-02-04 13:07:38,134 | skyrenderer.utils.time_measurement | INFO: Context update time: 527 ms
2025-02-04 13:07:38,976 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:38,977 | skyrenderer.utils.time_measurement | INFO: Render time: 842 ms
2025-02-04 13:07:39,028 | skyrenderer.utils.time_measurement | INFO: Setup time: 47 ms
2025-02-04 13:07:39,557 | skyrenderer.utils.time_measurement | INFO: Context update time: 528 ms
2025-02-04 13:07:39,743 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:39,744 | skyrenderer.utils.time_measurement | INFO: Render time: 187 ms
2025-02-04 13:07:39,795 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:40,322 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:40,505 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:40,506 | skyrenderer.utils.time_measurement | INFO: Render time: 183 ms
2025-02-04 13:07:40,557 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:41,083 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:41,985 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:41,986 | skyrenderer.utils.time_measurement | INFO: Render time: 902 ms
2025-02-04 13:07:42,036 | skyrenderer.utils.time_measurement | INFO: Setup time: 45 ms
2025-02-04 13:07:42,563 | skyrenderer.utils.time_measurement | INFO: Context update time: 527 ms
2025-02-04 13:07:42,745 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:42,746 | skyrenderer.utils.time_measurement | INFO: Render time: 181 ms
2025-02-04 13:07:42,796 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:43,323 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:44,237 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:44,238 | skyrenderer.utils.time_measurement | INFO: Render time: 915 ms
2025-02-04 13:07:44,288 | skyrenderer.utils.time_measurement | INFO: Setup time: 46 ms
2025-02-04 13:07:44,818 | skyrenderer.utils.time_measurement | INFO: Context update time: 529 ms
2025-02-04 13:07:45,730 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:45,731 | skyrenderer.utils.time_measurement | INFO: Render time: 912 ms
2025-02-04 13:07:45,782 | skyrenderer.utils.time_measurement | INFO: Setup time: 47 ms
2025-02-04 13:07:46,315 | skyrenderer.utils.time_measurement | INFO: Context update time: 532 ms
2025-02-04 13:07:47,175 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:47,175 | skyrenderer.utils.time_measurement | INFO: Render time: 860 ms
aperture_jitter and focus_distance
apperture_jitter is the parameter responsible for aperture related to focal length. The higher value, the smaller
DOF of a camera. If 0, infinite DOF is present. On the other hand, focus_distance is parameter holding a value of
plane distance in meters. It is used, when aperture jitter is nonzero.
renders = {}
scene_composer.setup_pinhole_lens(width, height, cx_relative=0.5, cy_relative=0.5)
renders["aperture=0(default)"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, aperture_jitter=1, focus_distance=5)
renders["aperture=1,focus_dist=5"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, aperture_jitter=1, focus_distance=15)
renders["aperture=1,focus_dist=15"] = scene_composer.get_render()
scene_composer.setup_pinhole_lens(width, height, aperture_jitter=1, focus_distance=25)
renders["aperture=1,focus_dist=25"] = scene_composer.get_render()
scene_composer.visualize_grid_desc(renders, shape=(height, 4 * width), n_cols=4, font_scale=2)
2025-02-04 13:07:48,031 | skyrenderer.utils.time_measurement | INFO: Setup time: 47 ms
2025-02-04 13:07:48,560 | skyrenderer.utils.time_measurement | INFO: Context update time: 528 ms
2025-02-04 13:07:49,446 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:49,447 | skyrenderer.utils.time_measurement | INFO: Render time: 887 ms
2025-02-04 13:07:49,498 | skyrenderer.utils.time_measurement | INFO: Setup time: 47 ms
2025-02-04 13:07:50,028 | skyrenderer.utils.time_measurement | INFO: Context update time: 529 ms
2025-02-04 13:07:50,952 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:50,953 | skyrenderer.utils.time_measurement | INFO: Render time: 924 ms
2025-02-04 13:07:51,006 | skyrenderer.utils.time_measurement | INFO: Setup time: 48 ms
2025-02-04 13:07:51,530 | skyrenderer.utils.time_measurement | INFO: Context update time: 524 ms
2025-02-04 13:07:52,353 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:52,353 | skyrenderer.utils.time_measurement | INFO: Render time: 822 ms
2025-02-04 13:07:52,403 | skyrenderer.utils.time_measurement | INFO: Setup time: 45 ms
2025-02-04 13:07:52,930 | skyrenderer.utils.time_measurement | INFO: Context update time: 526 ms
2025-02-04 13:07:53,835 | skyrenderer.utils.time_measurement | INFO: Key points calculation time: 0 ms
2025-02-04 13:07:53,836 | skyrenderer.utils.time_measurement | INFO: Render time: 905 ms
Summary
In this section you have learnt:
- All parameters from pinhole lens mathematical theory can be set in PinholeLens's
create_parameter_provider
method. - Pinhole lens model follows generic projection model described in INTRO_Lens.
- Model-based parameters of pinhole lens are compatible with popular CV libraries like OpenCV, and renders can be
undistorted using 3rd party library.