Monado OpenXR Runtime
Loading...
Searching...
No Matches
t_camera_models.hpp
Go to the documentation of this file.
1// Copyright 2023, Collabora, Ltd.
2// Copyright 2026, Beyley Cardellio
3// SPDX-License-Identifier: BSL-1.0
4/*!
5 * @file
6 * @brief Camera (un)projection C++ API for various camera models.
7 * @author Moshi Turner <moshiturner@protonmail.com>
8 * @author Beyley Cardellio <ep1cm1n10n123@gmail.com>
9 * @ingroup aux_tracking
10 */
11
12#pragma once
13
14#include "math/m_vec2.h"
15#include "math/m_matrix_2x2.h"
16#include "math/m_mathinclude.h"
17
18#include "t_tracking.h"
19
21
22
23namespace xrt::auxiliary::tracking::camera_models {
24
25static constexpr double kSqrtEpsilon = 0.00316; // sqrt(1e-05)
26
27// We're doing a lot of these, so let's have a macro.
28#define CAST(x) static_cast<T>(x)
29
30// We could use Eigen here, but custom types keeps this file lean and quick to compile,
31// along with giving us more control.
32template <typename T> struct Vector2
33{
34public: // Fields
35 T x;
36 T y;
37
38public: // Methods
39 inline Vector2<T>
40 sub(const Vector2<T> &other) const
41 {
42 return {this->x - other.x, this->y - other.y};
43 }
44
45 inline T
46 length() const
47 {
48 return sqrt(this->x * this->x + this->y * this->y);
49 }
50};
51
52template <typename T> struct Matrix2x2
53{
54public: // Fields
55 T v[4];
56
57public: // Methods
58 inline void
59 invert(Matrix2x2<T> &invertedMatrix) const
60 {
61 T determinant = v[0] * v[3] - v[1] * v[2];
62 invertedMatrix.v[0] = v[3] / determinant;
63 invertedMatrix.v[1] = -v[1] / determinant;
64 invertedMatrix.v[2] = -v[2] / determinant;
65 invertedMatrix.v[3] = v[0] / determinant;
66 }
67
68 inline void
69 transformVector2(const Vector2<T> &vec, Vector2<T> &result_out) const
70 {
71 result_out.x = v[0] * vec.x + v[1] * vec.y;
72 result_out.y = v[2] * vec.x + v[3] * vec.y;
73 }
74};
75
76/*
77 * Functions for @ref T_DISTORTION_FISHEYE_KB4 (un)projections
78 */
79
80template <typename T>
81static inline T
82kb4_calc_r_theta(const t_camera_model_params &dist, //
83 const T &theta, //
84 const T &theta2)
85{
86 T r_theta = CAST(dist.fisheye.k4) * theta2;
87 r_theta += CAST(dist.fisheye.k3);
88 r_theta *= theta2;
89 r_theta += CAST(dist.fisheye.k2);
90 r_theta *= theta2;
91 r_theta += CAST(dist.fisheye.k1);
92 r_theta *= theta2;
93 r_theta += CAST(1.0);
94 r_theta *= theta;
95
96 return r_theta;
97}
98
99template <typename T>
100static inline bool
101kb4_project(const t_camera_model_params &dist, //
102 const T &x, //
103 const T &y, //
104 const T &z, //
105 T &out_x, //
106 T &out_y)
107{
108 const T r2 = x * x + y * y;
109 const T r = sqrt(r2);
110
111 if (r > kSqrtEpsilon) {
112 const T theta = atan2(r, z);
113 const T theta2 = theta * theta;
114
115 T r_theta = kb4_calc_r_theta(dist, theta, theta2);
116
117 const T mx = x * r_theta / r;
118 const T my = y * r_theta / r;
119
120 out_x = CAST(dist.fx) * mx + CAST(dist.cx);
121 out_y = CAST(dist.fy) * my + CAST(dist.cy);
122
123 return true;
124 } else {
125 out_x = CAST(dist.fx) * x / z + CAST(dist.cx);
126 out_y = CAST(dist.fy) * y / z + CAST(dist.cy);
127
128 // The projection is only valid if the point is not close to the zero norm.
129 return z >= kSqrtEpsilon;
130 }
131
132 assert(!"Unreachable");
133}
134
135template <typename T>
136static inline T
137kb4_solve_theta(const t_camera_model_params &dist, const T &r_theta, T *d_func_d_theta)
138{
139 T theta = r_theta;
140 for (int i = 4; i > 0; i--) {
141 T theta2 = theta * theta;
142
143 T func = CAST(dist.fisheye.k4) * theta2;
144 func += CAST(dist.fisheye.k3);
145 func *= theta2;
146 func += CAST(dist.fisheye.k2);
147 func *= theta2;
148 func += CAST(dist.fisheye.k1);
149 func *= theta2;
150 func += CAST(1.0);
151 func *= theta;
152
153 (*d_func_d_theta) = CAST(9.0 * dist.fisheye.k4) * theta2;
154 (*d_func_d_theta) += CAST(7.0 * dist.fisheye.k3);
155 (*d_func_d_theta) *= theta2;
156 (*d_func_d_theta) += CAST(5.0 * dist.fisheye.k2);
157 (*d_func_d_theta) *= theta2;
158 (*d_func_d_theta) += CAST(3.0 * dist.fisheye.k1);
159 (*d_func_d_theta) *= theta2;
160 (*d_func_d_theta) += CAST(1.0);
161
162 // Iteration of Newton method
163 theta += (r_theta - func) / (*d_func_d_theta);
164 }
165
166 return theta;
167}
168
169template <typename T>
170static inline bool
172 const T &x, //
173 const T &y, //
174 T &out_x, //
175 T &out_y, //
176 T &out_z)
177{
178 const T mx = (x - CAST(dist.cx)) / CAST(dist.fx);
179 const T my = (y - CAST(dist.cy)) / CAST(dist.fy);
180
181 T theta = CAST(0.0);
182 T sin_theta = CAST(0.0);
183 T cos_theta = CAST(1.0);
184 T thetad = sqrt(mx * mx + my * my);
185 T scaling = CAST(1.0);
186 T d_func_d_theta = CAST(0.0);
187
188 if (thetad > kSqrtEpsilon) {
189 theta = kb4_solve_theta(dist, thetad, &d_func_d_theta);
190
191 sin_theta = sin(theta);
192 cos_theta = cos(theta);
193 scaling = sin_theta / thetad;
194 }
195
196 out_x = mx * scaling;
197 out_y = my * scaling;
198 out_z = cos_theta;
199
200 //! @todo I'm not 100% sure if kb4 is always non-injective. basalt-headers always returns true here,
201 //! so it might be wrong too.
202 return true;
203}
204
205template <typename T>
206static inline void
207kb4_undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
208{
209 T xp, yp, zp;
210
211 kb4_unproject(dist, x, y, xp, yp, zp);
212
213 out_x = xp / zp;
214 out_y = yp / zp;
215}
216
217/*
218 * Functions for radial-tangential (un)projections
219 */
220
221template <typename T>
222static inline bool
223rt8_project(const t_camera_model_params &dist, //
224 const T &x, //
225 const T &y, //
226 const T &z, //
227 T &out_x, //
228 T &out_y)
229{
230 const T xp = x / z;
231 const T yp = y / z;
232 const T rp2 = xp * xp + yp * yp;
233 const T cdist = (CAST(1.0) + rp2 * (CAST(dist.rt8.k1) + rp2 * (CAST(dist.rt8.k2) + rp2 * CAST(dist.rt8.k3)))) /
234 (CAST(1.0) + rp2 * (CAST(dist.rt8.k4) + rp2 * (CAST(dist.rt8.k5) + rp2 * CAST(dist.rt8.k6))));
235 const T deltaX = CAST(2.0f * dist.rt8.p1) * xp * yp + CAST(dist.rt8.p2) * (rp2 + CAST(2.0) * xp * xp);
236 const T deltaY = CAST(2.0f * dist.rt8.p2) * xp * yp + CAST(dist.rt8.p1) * (rp2 + CAST(2.0) * yp * yp);
237 const T xpp = xp * cdist + deltaX;
238 const T ypp = yp * cdist + deltaY;
239 const T u = CAST(dist.fx) * xpp + CAST(dist.cx);
240 const T v = CAST(dist.fy) * ypp + CAST(dist.cy);
241
242 out_x = u;
243 out_y = v;
244
245 const float rpmax = dist.rt8.metric_radius;
246
247 bool positive_z = z >= kSqrtEpsilon; // Sophus::Constants<Scalar>::epsilonSqrt();
248 bool in_injective_area = rpmax == 0.0 ? true : rp2 <= rpmax * rpmax;
249 bool is_valid = positive_z && in_injective_area;
250
251 return is_valid;
252}
253
254template <typename T>
255static inline void
256rt8_distort(const t_camera_model_params &params,
257 const Vector2<T> &undist,
258 Vector2<T> &out_dist,
259 Matrix2x2<T> &out_d_dist_d_undist)
260{
261 const T k1 = CAST(params.rt8.k1);
262 const T k2 = CAST(params.rt8.k2);
263 const T p1 = CAST(params.rt8.p1);
264 const T p2 = CAST(params.rt8.p2);
265 const T k3 = CAST(params.rt8.k3);
266 const T k4 = CAST(params.rt8.k4);
267 const T k5 = CAST(params.rt8.k5);
268 const T k6 = CAST(params.rt8.k6);
269
270 const T xp = undist.x;
271 const T yp = undist.y;
272 const T rp2 = xp * xp + yp * yp;
273 const T cdist = (CAST(1.0) + rp2 * (k1 + rp2 * (k2 + rp2 * k3))) / //
274 (CAST(1.0) + rp2 * (k4 + rp2 * (k5 + rp2 * k6))); //
275 const T deltaX = CAST(2.0) * p1 * xp * yp + p2 * (rp2 + CAST(2.0) * xp * xp);
276 const T deltaY = CAST(2.0) * p2 * xp * yp + p1 * (rp2 + CAST(2.0) * yp * yp);
277 const T xpp = xp * cdist + deltaX;
278 const T ypp = yp * cdist + deltaY;
279 out_dist.x = xpp;
280 out_dist.y = ypp;
281
282 // Jacobian part!
283 // Expressions derived with sympy
284 const T v0 = xp * xp;
285 const T v1 = yp * yp;
286 const T v2 = v0 + v1;
287 const T v3 = k6 * v2;
288 const T v4 = k4 + v2 * (k5 + v3);
289 const T v5 = v2 * v4 + CAST(1.0);
290 const T v6 = v5 * v5;
291 const T v7 = CAST(1.0) / v6;
292 const T v8 = p1 * yp;
293 const T v9 = p2 * xp;
294 const T v10 = CAST(2.0) * v6;
295 const T v11 = k3 * v2;
296 const T v12 = k1 + v2 * (k2 + v11);
297 const T v13 = v12 * v2 + CAST(1.0);
298 const T v14 = v13 * (v2 * (k5 + CAST(2.0) * v3) + v4);
299 const T v15 = CAST(2.0) * v14;
300 const T v16 = v12 + v2 * (k2 + CAST(2.0) * v11);
301 const T v17 = CAST(2.0) * v16;
302 const T v18 = xp * yp;
303 const T v19 = CAST(2.0) * v7 * (-v14 * v18 + v16 * v18 * v5 + v6 * (p1 * xp + p2 * yp));
304
305 const T dxpp_dxp = v7 * (-v0 * v15 + v10 * (v8 + CAST(3.0) * v9) + v5 * (v0 * v17 + v13));
306 const T dxpp_dyp = v19;
307 const T dypp_dxp = v19;
308 const T dypp_dyp = v7 * (-v1 * v15 + v10 * (CAST(3.0) * v8 + v9) + v5 * (v1 * v17 + v13));
309
310 out_d_dist_d_undist.v[0] = dxpp_dxp;
311 out_d_dist_d_undist.v[1] = dxpp_dyp;
312 out_d_dist_d_undist.v[2] = dypp_dxp;
313 out_d_dist_d_undist.v[3] = dypp_dyp;
314}
315
316template <typename T>
317static inline void
318rt8_undistort(const t_camera_model_params &params, const T &u, const T &v, T &out_x, T &out_y)
319{
320 const T x0 = (u - CAST(params.cx)) / CAST(params.fx);
321 const T y0 = (v - CAST(params.cy)) / CAST(params.fy);
322
323 //! @todo Decide if besides rpmax, it could be useful to have an rppmax
324 //! field. A good starting point to having this would be using the sqrt of
325 //! the max rpp2 value computed in the optimization of `computeRpmax()`.
326
327 // Newton solver
328 Vector2<T> dist = {x0, y0};
329 Vector2<T> undist = dist;
330
331 const int N = 5; // Max iterations
332 for (int i = 0; i < N; i++) {
333 Matrix2x2<T> J;
334 Vector2<T> fundist;
335
336 rt8_distort(params, undist, fundist, J);
337 Vector2<T> residual = fundist.sub(dist);
338
339 // fundist - dist;
340 Matrix2x2<T> J_inverse;
341
342 J.invert(J_inverse);
343
344 Vector2<T> undist_sub;
345
346 J_inverse.transformVector2(residual, undist_sub);
347
348 undist = undist.sub(undist_sub);
349 if (residual.length() < kSqrtEpsilon) {
350 break;
351 }
352 }
353
354 out_x = undist.x;
355 out_y = undist.y;
356}
357
358template <typename T>
359static inline bool
360rt8_unproject(const t_camera_model_params &params, const T &u, const T &v, T &out_x, T &out_y, T &out_z)
361{
362 T xp, yp;
363 rt8_undistort(params, u, v, xp, yp);
364
365 const T norm_inv = CAST(1.0) / sqrt(xp * xp + yp * yp + CAST(1.0));
366 out_x = xp * norm_inv;
367 out_y = yp * norm_inv;
368 out_z = norm_inv;
369
370 const T rp2 = xp * xp + yp * yp;
371 bool in_injective_area =
372 params.rt8.metric_radius == 0.0f ? true : rp2 <= CAST(params.rt8.metric_radius * params.rt8.metric_radius);
373 bool is_valid = in_injective_area;
374
375 return is_valid;
376}
377
378#if 1
379template <typename T>
380static inline bool
381zero_distortion_pinhole_project(const t_camera_model_params &dist, //
382 const T x, //
383 const T y, //
384 const T z, //
385 T &out_x, //
386 T &out_y)
387{
388 out_x = ((CAST(dist.fx) * x / z) + CAST(dist.cx));
389 out_y = ((CAST(dist.fy) * y / z) + CAST(dist.cy));
390
391 bool is_valid = z >= kSqrtEpsilon;
392 return is_valid;
393}
394
395template <typename T>
396static inline bool
397zero_distortion_pinhole_unproject(const t_camera_model_params &dist, //
398 const T x, //
399 const T y, //
400 T &out_x, //
401 T &out_y, //
402 T &out_z)
403{
404 const T mx = (x - CAST(dist.cx)) / CAST(dist.fx);
405 const T my = (y - CAST(dist.cy)) / CAST(dist.fy);
406
407 const T r2 = mx * mx + my * my;
408
409 const T norm = sqrt(CAST(1.0) + r2);
410
411 const T norm_inv = CAST(1.0) / norm;
412
413 out_x = mx * norm_inv;
414 out_y = my * norm_inv;
415 out_z = norm_inv;
416
417 // Pinhole unprojection is always valid :)
418 return true;
419}
420#endif
421
422// This is a very common name, so make sure to undef it.
423#undef CAST
424
425/*
426 * Exported C++ functions.
427 */
428
429template <typename T>
430bool
431project(const t_camera_model_params &dist, //
432 const T &x, //
433 const T &y, //
434 const T &z, //
435 T &out_x, //
436 T &out_y)
437{
438 switch (dist.model) {
440 return rt8_project(dist, x, y, z, out_x, out_y);
441 }; break;
443 return kb4_project(dist, x, y, z, out_x, out_y);
444 }; break;
445 // Return false so we don't get warnings on Release builds.
446 default: assert(false); return false;
447 }
448}
449
450template <typename T>
451void
452undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
453{
454 switch (dist.model) {
456 rt8_undistort(dist, x, y, out_x, out_y);
457 }; break;
459 kb4_undistort(dist, x, y, out_x, out_y);
460 }; break;
461 // Return false so we don't get warnings on Release builds.
462 default: assert(false);
463 }
464}
465
466}; // namespace xrt::auxiliary::tracking::camera_models
Definition utility_northstar.h:270
@ T_DISTORTION_OPENCV_RADTAN_8
OpenCV's radial-tangential distortion model.
Definition t_tracking.h:82
@ T_DISTORTION_FISHEYE_KB4
Juho Kannalla and Sami Sebastian Brandt's fisheye distortion model.
Definition t_tracking.h:116
Wrapper header for <math.h> to ensure pi-related math constants are defined.
C matrix_2x2 math library.
C vec2 math library.
Floating point calibration data for a single calibrated camera.
Definition t_camera_models.h:47
Definition t_camera_models.hpp:33
Camera (un)projection C API for various camera models.
static void rt8_undistort(const t_camera_model_params &params, const T &u, const T &v, T &out_x, T &out_y)
Definition t_camera_models.hpp:318
static bool kb4_unproject(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y, T &out_z)
Definition t_camera_models.hpp:171
Tracking API interface.