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_PINHOLE
78 */
79
80template <typename T>
81static inline bool
82pinhole_project(const t_camera_model_params &dist, //
83 const T x, //
84 const T y, //
85 const T z, //
86 T &out_x, //
87 T &out_y)
88{
89 out_x = ((CAST(dist.fx) * x / z) + CAST(dist.cx));
90 out_y = ((CAST(dist.fy) * y / z) + CAST(dist.cy));
91
92 bool is_valid = z >= kSqrtEpsilon;
93 return is_valid;
94}
95
96template <typename T>
97static inline bool
98pinhole_unproject(const t_camera_model_params &dist, //
99 const T x, //
100 const T y, //
101 T &out_x, //
102 T &out_y, //
103 T &out_z)
104{
105 const T mx = (x - CAST(dist.cx)) / CAST(dist.fx);
106 const T my = (y - CAST(dist.cy)) / CAST(dist.fy);
107
108 const T r2 = mx * mx + my * my;
109
110 const T norm = sqrt(CAST(1.0) + r2);
111
112 const T norm_inv = CAST(1.0) / norm;
113
114 out_x = mx * norm_inv;
115 out_y = my * norm_inv;
116 out_z = norm_inv;
117
118 // Pinhole unprojection is always valid :)
119 return true;
120}
121
122/*
123 * Functions for @ref T_DISTORTION_FISHEYE_KB4 (un)projections
124 */
125
126template <typename T>
127static inline T
128kb4_calc_r_theta(const t_camera_model_params &dist, //
129 const T &theta, //
130 const T &theta2)
131{
132 T r_theta = CAST(dist.fisheye.k4) * theta2;
133 r_theta += CAST(dist.fisheye.k3);
134 r_theta *= theta2;
135 r_theta += CAST(dist.fisheye.k2);
136 r_theta *= theta2;
137 r_theta += CAST(dist.fisheye.k1);
138 r_theta *= theta2;
139 r_theta += CAST(1.0);
140 r_theta *= theta;
141
142 return r_theta;
143}
144
145template <typename T>
146static inline bool
147kb4_project(const t_camera_model_params &dist, //
148 const T &x, //
149 const T &y, //
150 const T &z, //
151 T &out_x, //
152 T &out_y)
153{
154 const T r2 = x * x + y * y;
155 const T r = sqrt(r2);
156
157 if (r > kSqrtEpsilon) {
158 const T theta = atan2(r, z);
159 const T theta2 = theta * theta;
160
161 T r_theta = kb4_calc_r_theta(dist, theta, theta2);
162
163 const T mx = x * r_theta / r;
164 const T my = y * r_theta / r;
165
166 out_x = CAST(dist.fx) * mx + CAST(dist.cx);
167 out_y = CAST(dist.fy) * my + CAST(dist.cy);
168
169 return true;
170 } else {
171 out_x = CAST(dist.fx) * x / z + CAST(dist.cx);
172 out_y = CAST(dist.fy) * y / z + CAST(dist.cy);
173
174 // The projection is only valid if the point is not close to the zero norm.
175 return z >= kSqrtEpsilon;
176 }
177
178 assert(!"Unreachable");
179}
180
181template <typename T>
182static inline T
183kb4_solve_theta(const t_camera_model_params &dist, const T &r_theta, T *d_func_d_theta)
184{
185 T theta = r_theta;
186 for (int i = 4; i > 0; i--) {
187 T theta2 = theta * theta;
188
189 T func = CAST(dist.fisheye.k4) * theta2;
190 func += CAST(dist.fisheye.k3);
191 func *= theta2;
192 func += CAST(dist.fisheye.k2);
193 func *= theta2;
194 func += CAST(dist.fisheye.k1);
195 func *= theta2;
196 func += CAST(1.0);
197 func *= theta;
198
199 (*d_func_d_theta) = CAST(9.0 * dist.fisheye.k4) * theta2;
200 (*d_func_d_theta) += CAST(7.0 * dist.fisheye.k3);
201 (*d_func_d_theta) *= theta2;
202 (*d_func_d_theta) += CAST(5.0 * dist.fisheye.k2);
203 (*d_func_d_theta) *= theta2;
204 (*d_func_d_theta) += CAST(3.0 * dist.fisheye.k1);
205 (*d_func_d_theta) *= theta2;
206 (*d_func_d_theta) += CAST(1.0);
207
208 // Iteration of Newton method
209 theta += (r_theta - func) / (*d_func_d_theta);
210 }
211
212 return theta;
213}
214
215template <typename T>
216static inline bool
218 const T &x, //
219 const T &y, //
220 T &out_x, //
221 T &out_y, //
222 T &out_z)
223{
224 const T mx = (x - CAST(dist.cx)) / CAST(dist.fx);
225 const T my = (y - CAST(dist.cy)) / CAST(dist.fy);
226
227 T theta = CAST(0.0);
228 T sin_theta = CAST(0.0);
229 T cos_theta = CAST(1.0);
230 T thetad = sqrt(mx * mx + my * my);
231 T scaling = CAST(1.0);
232 T d_func_d_theta = CAST(0.0);
233
234 if (thetad > kSqrtEpsilon) {
235 theta = kb4_solve_theta(dist, thetad, &d_func_d_theta);
236
237 sin_theta = sin(theta);
238 cos_theta = cos(theta);
239 scaling = sin_theta / thetad;
240 }
241
242 out_x = mx * scaling;
243 out_y = my * scaling;
244 out_z = cos_theta;
245
246 //! @todo I'm not 100% sure if kb4 is always non-injective. basalt-headers always returns true here,
247 //! so it might be wrong too.
248 return true;
249}
250
251template <typename T>
252static inline void
253kb4_undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
254{
255 T xp, yp, zp;
256
257 kb4_unproject(dist, x, y, xp, yp, zp);
258
259 out_x = xp / zp;
260 out_y = yp / zp;
261}
262
263/*
264 * Functions for radial-tangential (un)projections
265 */
266
267template <typename T>
268static inline bool
269rt8_project(const t_camera_model_params &dist, //
270 const T &x, //
271 const T &y, //
272 const T &z, //
273 T &out_x, //
274 T &out_y)
275{
276 const T xp = x / z;
277 const T yp = y / z;
278 const T rp2 = xp * xp + yp * yp;
279 const T cdist = (CAST(1.0) + rp2 * (CAST(dist.rt8.k1) + rp2 * (CAST(dist.rt8.k2) + rp2 * CAST(dist.rt8.k3)))) /
280 (CAST(1.0) + rp2 * (CAST(dist.rt8.k4) + rp2 * (CAST(dist.rt8.k5) + rp2 * CAST(dist.rt8.k6))));
281 const T deltaX = CAST(2.0f * dist.rt8.p1) * xp * yp + CAST(dist.rt8.p2) * (rp2 + CAST(2.0) * xp * xp);
282 const T deltaY = CAST(2.0f * dist.rt8.p2) * xp * yp + CAST(dist.rt8.p1) * (rp2 + CAST(2.0) * yp * yp);
283 const T xpp = xp * cdist + deltaX;
284 const T ypp = yp * cdist + deltaY;
285 const T u = CAST(dist.fx) * xpp + CAST(dist.cx);
286 const T v = CAST(dist.fy) * ypp + CAST(dist.cy);
287
288 out_x = u;
289 out_y = v;
290
291 const float rpmax = dist.rt8.metric_radius;
292
293 bool positive_z = z >= kSqrtEpsilon; // Sophus::Constants<Scalar>::epsilonSqrt();
294 bool in_injective_area = rpmax == 0.0 ? true : rp2 <= rpmax * rpmax;
295 bool is_valid = positive_z && in_injective_area;
296
297 return is_valid;
298}
299
300template <typename T>
301static inline void
302rt8_distort(const t_camera_model_params &params,
303 const Vector2<T> &undist,
304 Vector2<T> &out_dist,
305 Matrix2x2<T> &out_d_dist_d_undist)
306{
307 const T k1 = CAST(params.rt8.k1);
308 const T k2 = CAST(params.rt8.k2);
309 const T p1 = CAST(params.rt8.p1);
310 const T p2 = CAST(params.rt8.p2);
311 const T k3 = CAST(params.rt8.k3);
312 const T k4 = CAST(params.rt8.k4);
313 const T k5 = CAST(params.rt8.k5);
314 const T k6 = CAST(params.rt8.k6);
315
316 const T xp = undist.x;
317 const T yp = undist.y;
318 const T rp2 = xp * xp + yp * yp;
319 const T cdist = (CAST(1.0) + rp2 * (k1 + rp2 * (k2 + rp2 * k3))) / //
320 (CAST(1.0) + rp2 * (k4 + rp2 * (k5 + rp2 * k6))); //
321 const T deltaX = CAST(2.0) * p1 * xp * yp + p2 * (rp2 + CAST(2.0) * xp * xp);
322 const T deltaY = CAST(2.0) * p2 * xp * yp + p1 * (rp2 + CAST(2.0) * yp * yp);
323 const T xpp = xp * cdist + deltaX;
324 const T ypp = yp * cdist + deltaY;
325 out_dist.x = xpp;
326 out_dist.y = ypp;
327
328 // Jacobian part!
329 // Expressions derived with sympy
330 const T v0 = xp * xp;
331 const T v1 = yp * yp;
332 const T v2 = v0 + v1;
333 const T v3 = k6 * v2;
334 const T v4 = k4 + v2 * (k5 + v3);
335 const T v5 = v2 * v4 + CAST(1.0);
336 const T v6 = v5 * v5;
337 const T v7 = CAST(1.0) / v6;
338 const T v8 = p1 * yp;
339 const T v9 = p2 * xp;
340 const T v10 = CAST(2.0) * v6;
341 const T v11 = k3 * v2;
342 const T v12 = k1 + v2 * (k2 + v11);
343 const T v13 = v12 * v2 + CAST(1.0);
344 const T v14 = v13 * (v2 * (k5 + CAST(2.0) * v3) + v4);
345 const T v15 = CAST(2.0) * v14;
346 const T v16 = v12 + v2 * (k2 + CAST(2.0) * v11);
347 const T v17 = CAST(2.0) * v16;
348 const T v18 = xp * yp;
349 const T v19 = CAST(2.0) * v7 * (-v14 * v18 + v16 * v18 * v5 + v6 * (p1 * xp + p2 * yp));
350
351 const T dxpp_dxp = v7 * (-v0 * v15 + v10 * (v8 + CAST(3.0) * v9) + v5 * (v0 * v17 + v13));
352 const T dxpp_dyp = v19;
353 const T dypp_dxp = v19;
354 const T dypp_dyp = v7 * (-v1 * v15 + v10 * (CAST(3.0) * v8 + v9) + v5 * (v1 * v17 + v13));
355
356 out_d_dist_d_undist.v[0] = dxpp_dxp;
357 out_d_dist_d_undist.v[1] = dxpp_dyp;
358 out_d_dist_d_undist.v[2] = dypp_dxp;
359 out_d_dist_d_undist.v[3] = dypp_dyp;
360}
361
362template <typename T>
363static inline void
364rt8_undistort(const t_camera_model_params &params, const T &u, const T &v, T &out_x, T &out_y)
365{
366 const T x0 = (u - CAST(params.cx)) / CAST(params.fx);
367 const T y0 = (v - CAST(params.cy)) / CAST(params.fy);
368
369 //! @todo Decide if besides rpmax, it could be useful to have an rppmax
370 //! field. A good starting point to having this would be using the sqrt of
371 //! the max rpp2 value computed in the optimization of `computeRpmax()`.
372
373 // Newton solver
374 Vector2<T> dist = {x0, y0};
375 Vector2<T> undist = dist;
376
377 const int N = 5; // Max iterations
378 for (int i = 0; i < N; i++) {
379 Matrix2x2<T> J;
380 Vector2<T> fundist;
381
382 rt8_distort(params, undist, fundist, J);
383 Vector2<T> residual = fundist.sub(dist);
384
385 // fundist - dist;
386 Matrix2x2<T> J_inverse;
387
388 J.invert(J_inverse);
389
390 Vector2<T> undist_sub;
391
392 J_inverse.transformVector2(residual, undist_sub);
393
394 undist = undist.sub(undist_sub);
395 if (residual.length() < kSqrtEpsilon) {
396 break;
397 }
398 }
399
400 out_x = undist.x;
401 out_y = undist.y;
402}
403
404template <typename T>
405static inline bool
406rt8_unproject(const t_camera_model_params &params, const T &u, const T &v, T &out_x, T &out_y, T &out_z)
407{
408 T xp, yp;
409 rt8_undistort(params, u, v, xp, yp);
410
411 const T norm_inv = CAST(1.0) / sqrt(xp * xp + yp * yp + CAST(1.0));
412 out_x = xp * norm_inv;
413 out_y = yp * norm_inv;
414 out_z = norm_inv;
415
416 const T rp2 = xp * xp + yp * yp;
417 bool in_injective_area =
418 params.rt8.metric_radius == 0.0f ? true : rp2 <= CAST(params.rt8.metric_radius * params.rt8.metric_radius);
419 bool is_valid = in_injective_area;
420
421 return is_valid;
422}
423
424/*
425 * Functions for @ref T_DISTORTION_RIFT_CV1 (un)projections
426 */
427
428//! Radial undistortion scale s = |ray| / |p| for a distorted normalized image point of radius @p r.
429template <typename T>
430static inline T
432{
433 if (r == CAST(0.0)) {
434 return CAST(1.0);
435 }
436
437 // The image radius r is used directly as the field angle: t = tan(r). (KB4 would use atan(r).)
438 const T t = tan(r);
439 const T t2 = t * t;
440
441 // P(t) = 1 + k1 t^2 + k2 t^4 + k3 t^6 + k4 t^8 (Oculus d1..d4), Horner form.
442 T poly = CAST(dist.cv1.k4) * t2;
443 poly += CAST(dist.cv1.k3);
444 poly *= t2;
445 poly += CAST(dist.cv1.k2);
446 poly *= t2;
447 poly += CAST(dist.cv1.k1);
448 poly *= t2;
449 poly += CAST(1.0);
450
451 return (t / r) / poly;
452}
453
454//! Tangential ("decentering") delta with the CV1 4th-order affine gain (p1, p2, g3, g4), for a scaled point @p q.
455template <typename T>
456static inline void
457cv1_decentering_delta(const t_camera_model_params &dist, const T &qx, const T &qy, T &out_dx, T &out_dy)
458{
459 const T p1 = CAST(dist.cv1.p1);
460 const T p2 = CAST(dist.cv1.p2);
461 const T rq2 = qx * qx + qy * qy;
462
463 T dx = (CAST(2.0) * qx * qx + rq2) * p1 + CAST(2.0) * p2 * qx * qy;
464 T dy = (CAST(2.0) * qy * qy + rq2) * p2 + CAST(2.0) * p1 * qx * qy;
465
466 // Affine gain g = 1 + g3 rq^2 + g4 rq^4, applied to the decentering delta.
467 const T gain = CAST(1.0) + rq2 * (CAST(dist.cv1.g3) + rq2 * CAST(dist.cv1.g4));
468
469 out_dx = dx * gain;
470 out_dy = dy * gain;
471}
472
473//! Maps a distorted image-space point (@p x, @p y) to a pinhole ray tangent (x/z, y/z).
474template <typename T>
475static inline void
476cv1_undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
477{
478 // Normalize the distorted pixel onto the sensor plane. CV1 uses a single focal length, so
479 // fx == fy here.
480 const T px = (x - CAST(dist.cx)) / CAST(dist.fx);
481 const T py = (y - CAST(dist.cy)) / CAST(dist.fy);
482
483 const T r = sqrt(px * px + py * py);
484 const T scale = cv1_radial_undistort_scale(dist, r);
485
486 const T qx = scale * px;
487 const T qy = scale * py;
488
489 T dx, dy;
490 cv1_decentering_delta(dist, qx, qy, dx, dy);
491
492 out_x = qx + dx;
493 out_y = qy + dy;
494}
495
496// This is a very common name, so make sure to undef it.
497#undef CAST
498
499/*
500 * Exported C++ functions.
501 */
502
503template <typename T>
504bool
505project(const t_camera_model_params &dist, //
506 const T &x, //
507 const T &y, //
508 const T &z, //
509 T &out_x, //
510 T &out_y)
511{
512 switch (dist.model) {
514 return pinhole_project(dist, x, y, z, out_x, out_y);
515 } break;
517 return rt8_project(dist, x, y, z, out_x, out_y);
518 }; break;
520 return kb4_project(dist, x, y, z, out_x, out_y);
521 }; break;
523 // Dummy value so we aren't returning uninitialized values.
524 out_x = T(dist.fx) * x / z + T(dist.cx);
525 out_y = T(dist.fy) * y / z + T(dist.cy);
526
527 // No projection path is present for CV1
528 return false;
529 }; break;
530 // Return false so we don't get warnings on Release builds.
531 default: assert(false); return false;
532 }
533}
534
535template <typename T>
536void
537undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
538{
539 switch (dist.model) {
541 out_x = x;
542 out_y = y;
543 }; break;
545 rt8_undistort(dist, x, y, out_x, out_y);
546 }; break;
548 kb4_undistort(dist, x, y, out_x, out_y);
549 }; break;
551 cv1_undistort(dist, x, y, out_x, out_y);
552 }; break;
553 // Return false so we don't get warnings on Release builds.
554 default: assert(false);
555 }
556}
557
558}; // 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:87
@ T_DISTORTION_FISHEYE_KB4
Juho Kannalla and Sami Sebastian Brandt's fisheye distortion model.
Definition t_tracking.h:121
@ T_DISTORTION_RIFT_CV1
The Oculus Rift CV1 constellation sensor's lens model (LensModel "type 6" in Oculus' runtime).
Definition t_tracking.h:149
@ T_DISTORTION_PINHOLE
A perfect pinhole camera with no distortion.
Definition t_tracking.h:67
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:62
Definition t_camera_models.hpp:33
Camera (un)projection C API for various camera models.
static T cv1_radial_undistort_scale(const t_camera_model_params &dist, const T &r)
Radial undistortion scale s = |ray| / |p| for a distorted normalized image point of radius r.
Definition t_camera_models.hpp:431
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:364
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:217
static void cv1_undistort(const t_camera_model_params &dist, const T &x, const T &y, T &out_x, T &out_y)
Maps a distorted image-space point (x, y) to a pinhole ray tangent (x/z, y/z).
Definition t_camera_models.hpp:476
static void cv1_decentering_delta(const t_camera_model_params &dist, const T &qx, const T &qy, T &out_dx, T &out_dy)
Tangential ("decentering") delta with the CV1 4th-order affine gain (p1, p2, g3, g4),...
Definition t_camera_models.hpp:457
Tracking API interface.
__le16 gain
observed 16 to 255
Definition wmr_camera.c:5