a/tracking: Introduce t_camera_models.h

This commit is contained in:
Moses Turner 2023-02-04 15:21:25 -06:00
parent 3cdc02c1bb
commit bfdeaa7d8f

View file

@ -0,0 +1,555 @@
// Copyright 2023, Collabora, Ltd.
// SPDX-License-Identifier: BSL-1.0
/*!
* @file
* @brief Simple, untemplated, C, float-only, camera (un)projection functions for various camera models.
* @author Moses Turner <moses@collabora.com>
* @ingroup aux_tracking
*
* Some notes:
* These functions should return exactly the same values as basalt-headers, down to floating point bits.
*
* They were mainly written as an expedient way to stop depending on OpenCV-based (un)projection code in Monado's hand
* tracking code, and to encourage compiler optimizations through inlining.
*
* Current users:
*
* * Mercury hand tracking
*/
#pragma once
#include "math/m_vec2.h"
#include "math/m_matrix_2x2.h"
#include "math/m_mathinclude.h"
#include "t_tracking.h"
#include <assert.h>
/*!
* Floating point parameters for @ref T_DISTORTION_FISHEYE_KB4
* @ingroup aux_tracking
*/
struct t_camera_calibration_kb4_params_float
{
float k1, k2, k3, k4;
};
/*!
* Floating point parameters for @ref T_DISTORTION_OPENCV_RT8, also including metric_radius.
* @ingroup aux_tracking
*/
struct t_camera_calibration_rt8_params_float
{
float k1, k2, p1, p2, k3, k4, k5, k6, metric_radius;
};
/*!
* Floating point calibration data for a single calibrated camera.
* @note This is basically @ref t_camera_calibration, just without some compatibility stuff and using single floats
* instead of doubles.
* @ingroup aux_tracking
*/
struct t_camera_model_params
{
float fx, fy, cx, cy;
union {
struct t_camera_calibration_kb4_params_float fisheye;
struct t_camera_calibration_rt8_params_float rt8;
};
// This model gets reinterpreted from values in the main t_camera_calibration struct to either
// * T_DISTORTION_FISHEYE_KB4
// * T_DISTORTION_OPENCV_RADTAN_8
enum t_camera_distortion_model model;
};
const float SQRT_EPSILON = 0.00316; // sqrt(1e-05)
/*
* Functions for @ref T_DISTORTION_FISHEYE_KB4 (un)projections
*/
static inline float
kb4_calc_r_theta(const struct t_camera_model_params *dist, //
const float theta, //
const float theta2)
{
float r_theta = dist->fisheye.k4 * theta2;
r_theta += dist->fisheye.k3;
r_theta *= theta2;
r_theta += dist->fisheye.k2;
r_theta *= theta2;
r_theta += dist->fisheye.k1;
r_theta *= theta2;
r_theta += 1.0f;
r_theta *= theta;
return r_theta;
}
static inline bool
kb4_project(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
const float z, //
float *out_x, //
float *out_y)
{
bool is_valid = true;
const float r2 = x * x + y * y;
const float r = sqrtf(r2);
if (r > SQRT_EPSILON) {
const float theta = atan2(r, z);
const float theta2 = theta * theta;
float r_theta = kb4_calc_r_theta(dist, theta, theta2);
const float mx = x * r_theta / r;
const float my = y * r_theta / r;
*out_x = dist->fx * mx + dist->cx;
*out_y = dist->fy * my + dist->cy;
} else {
// Check that the point is not cloze to zero norm
if (z < SQRT_EPSILON) {
is_valid = false;
}
*out_x = dist->fx * x / z + dist->cx;
*out_y = dist->fy * y / z + dist->cy;
}
return is_valid;
}
static inline float
kb4_solve_theta(const struct t_camera_model_params *dist, const float *r_theta, float *d_func_d_theta)
{
float theta = *r_theta;
for (int i = 4; i > 0; i--) {
float theta2 = theta * theta;
float func = dist->fisheye.k4 * theta2;
func += dist->fisheye.k3;
func *= theta2;
func += dist->fisheye.k2;
func *= theta2;
func += dist->fisheye.k1;
func *= theta2;
func += 1.0f;
func *= theta;
*d_func_d_theta = 9.0f * dist->fisheye.k4 * theta2;
*d_func_d_theta += 7.0f * dist->fisheye.k3;
*d_func_d_theta *= theta2;
*d_func_d_theta += 5.0f * dist->fisheye.k2;
*d_func_d_theta *= theta2;
*d_func_d_theta += 3.0f * dist->fisheye.k1;
*d_func_d_theta *= theta2;
*d_func_d_theta += 1.0f;
// Iteration of Newton method
theta += ((*r_theta) - func) / (*d_func_d_theta);
}
return theta;
}
static inline bool
kb4_unproject(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
float *out_x, //
float *out_y, //
float *out_z)
{
const float mx = (x - dist->cx) / dist->fx;
const float my = (y - dist->cy) / dist->fy;
float theta(0);
float sin_theta(0);
float cos_theta(1);
float thetad = sqrt(mx * mx + my * my);
float scaling(1);
float d_func_d_theta(0);
if (thetad > SQRT_EPSILON) {
theta = kb4_solve_theta(dist, &thetad, &d_func_d_theta);
sin_theta = sin(theta);
cos_theta = cos(theta);
scaling = sin_theta / thetad;
}
*out_x = mx * scaling;
*out_y = my * scaling;
*out_z = cos_theta;
//!@todo I'm not 100% sure if kb4 is always non-injective. basalt-headers always returns true here, so it might
//! be wrong too.
return true;
}
/*
* Functions for radial-tangential (un)projections
*/
static inline bool
rt8_project(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
const float z, //
float *out_x, //
float *out_y)
{
const float xp = x / z;
const float yp = y / z;
const float rp2 = xp * xp + yp * yp;
const float cdist = (1.0f + rp2 * (dist->rt8.k1 + rp2 * (dist->rt8.k2 + rp2 * dist->rt8.k3))) /
(1.0f + rp2 * (dist->rt8.k4 + rp2 * (dist->rt8.k5 + rp2 * dist->rt8.k6)));
const float deltaX = 2.0f * dist->rt8.p1 * xp * yp + dist->rt8.p2 * (rp2 + 2.0f * xp * xp);
const float deltaY = 2.0f * dist->rt8.p2 * xp * yp + dist->rt8.p1 * (rp2 + 2.0f * yp * yp);
const float xpp = xp * cdist + deltaX;
const float ypp = yp * cdist + deltaY;
const float u = dist->fx * xpp + dist->cx;
const float v = dist->fy * ypp + dist->cy;
*out_x = u;
*out_y = v;
const float rpmax = dist->rt8.metric_radius;
bool positive_z = z >= SQRT_EPSILON; // Sophus::Constants<Scalar>::epsilonSqrt();
bool in_injective_area = rpmax == 0.0f ? true : rp2 <= rpmax * rpmax;
bool is_valid = positive_z && in_injective_area;
return is_valid;
}
static inline void
rt8_distort(const t_camera_model_params *params,
const xrt_vec2 *undist,
xrt_vec2 *dist,
xrt_matrix_2x2 *d_dist_d_undist)
{
const float &k1 = params->rt8.k1;
const float &k2 = params->rt8.k2;
const float &p1 = params->rt8.p1;
const float &p2 = params->rt8.p2;
const float &k3 = params->rt8.k3;
const float &k4 = params->rt8.k4;
const float &k5 = params->rt8.k5;
const float &k6 = params->rt8.k6;
const float xp = undist->x;
const float yp = undist->y;
const float rp2 = xp * xp + yp * yp;
const float cdist = (1.0f + rp2 * (k1 + rp2 * (k2 + rp2 * k3))) / (1.0f + rp2 * (k4 + rp2 * (k5 + rp2 * k6)));
const float deltaX = 2.0f * p1 * xp * yp + p2 * (rp2 + 2.0f * xp * xp);
const float deltaY = 2.0f * p2 * xp * yp + p1 * (rp2 + 2.0f * yp * yp);
const float xpp = xp * cdist + deltaX;
const float ypp = yp * cdist + deltaY;
dist->x = xpp;
dist->y = ypp;
// Jacobian part!
// Expressions derived with sympy
const float v0 = xp * xp;
const float v1 = yp * yp;
const float v2 = v0 + v1;
const float v3 = k6 * v2;
const float v4 = k4 + v2 * (k5 + v3);
const float v5 = v2 * v4 + 1.0f;
const float v6 = v5 * v5;
const float v7 = 1.0f / v6;
const float v8 = p1 * yp;
const float v9 = p2 * xp;
const float v10 = 2.0f * v6;
const float v11 = k3 * v2;
const float v12 = k1 + v2 * (k2 + v11);
const float v13 = v12 * v2 + 1.0f;
const float v14 = v13 * (v2 * (k5 + 2.0f * v3) + v4);
const float v15 = 2.0f * v14;
const float v16 = v12 + v2 * (k2 + 2.0f * v11);
const float v17 = 2.0f * v16;
const float v18 = xp * yp;
const float v19 = 2.0f * v7 * (-v14 * v18 + v16 * v18 * v5 + v6 * (p1 * xp + p2 * yp));
const float dxpp_dxp = v7 * (-v0 * v15 + v10 * (v8 + 3.0f * v9) + v5 * (v0 * v17 + v13));
const float dxpp_dyp = v19;
const float dypp_dxp = v19;
const float dypp_dyp = v7 * (-v1 * v15 + v10 * (3.0f * v8 + v9) + v5 * (v1 * v17 + v13));
d_dist_d_undist->v[0] = dxpp_dxp;
d_dist_d_undist->v[1] = dxpp_dyp;
d_dist_d_undist->v[2] = dypp_dxp;
d_dist_d_undist->v[3] = dypp_dyp;
}
static inline bool
rt8_unproject(
const struct t_camera_model_params *hg_dist, const float u, const float v, float *out_x, float *out_y, float *out_z)
{
const float x0 = (u - hg_dist->cx) / hg_dist->fx;
const float y0 = (v - hg_dist->cy) / hg_dist->fy;
//! @todo Decide if besides rpmax, it could be useful to have an rppmax
//! field. A good starting point to having this would be using the sqrt of
//! the max rpp2 value computed in the optimization of `computeRpmax()`.
// Newton solver
struct xrt_vec2 dist = {x0, y0};
struct xrt_vec2 undist = dist;
const int N = 5; // Max iterations
for (int i = 0; i < N; i++) {
struct xrt_matrix_2x2 J;
struct xrt_vec2 fundist;
rt8_distort(hg_dist, &undist, &fundist, &J);
struct xrt_vec2 residual = m_vec2_sub(fundist, dist);
// fundist - dist;
struct xrt_matrix_2x2 J_inverse;
m_mat2x2_invert(&J, &J_inverse);
struct xrt_vec2 undist_sub;
m_mat2x2_transform_vec2(&J_inverse, &residual, &undist_sub);
undist = m_vec2_sub(undist, undist_sub);
if (m_vec2_len(residual) < SQRT_EPSILON) {
break;
}
}
const float xp = undist.x;
const float yp = undist.y;
const float norm_inv = 1.0f / sqrt(xp * xp + yp * yp + 1.0f);
*out_x = xp * norm_inv;
*out_y = yp * norm_inv;
*out_z = norm_inv;
const float rp2 = xp * xp + yp * yp;
bool in_injective_area =
hg_dist->rt8.metric_radius == 0.0f ? true : rp2 <= hg_dist->rt8.metric_radius * hg_dist->rt8.metric_radius;
bool is_valid = in_injective_area;
return is_valid;
}
#if 0
static inline bool
zero_distortion_pinhole_project(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
const float z, //
float *out_x, //
float *out_y)
{
*out_x = ((dist->fx * x / z) + dist->cx);
*out_y = ((dist->fy * y / z) + dist->cy);
bool is_valid = z >= SQRT_EPSILON;
return is_valid;
}
static inline bool
zero_distortion_pinhole_unproject(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
float *out_x, //
float *out_y, //
float *out_z)
{
const float mx = (x - dist->cx) / dist->fx;
const float my = (y - dist->cy) / dist->fy;
const float r2 = mx * mx + my * my;
const float norm = sqrtf(1.0f + r2);
const float norm_inv = 1.0f / norm;
*out_x = mx * norm_inv;
*out_y = my * norm_inv;
*out_z = norm_inv;
// Pinhole unprojection is always valid :)
return true;
}
#endif
/*
* Misc functions.
*/
static inline void
interpret_as_rt8(const struct t_camera_calibration *cc, struct t_camera_model_params *out_params)
{
// Make a temporary buffer that definitely has zeros in it.
double distortion_tmp[XRT_DISTORTION_MAX_DIM] = {0};
if (cc->distortion_model != T_DISTORTION_OPENCV_RADTAN_8) {
U_LOG_W("Reinterpreting %s distortion as %s", t_stringify_camera_distortion_model(cc->distortion_model),
t_stringify_camera_distortion_model(T_DISTORTION_OPENCV_RADTAN_8));
}
size_t dist_num = t_num_params_from_distortion_model(cc->distortion_model);
for (size_t i = 0; i < dist_num; i++) {
// Copy only the valid values over. The high indices will be zero, which means that rt4 and rt5
// calibrations will work correctly.
distortion_tmp[i] = cc->distortion_parameters_as_array[i];
}
int acc_idx = 0;
out_params->rt8.k1 = distortion_tmp[acc_idx++];
out_params->rt8.k2 = distortion_tmp[acc_idx++];
out_params->rt8.p1 = distortion_tmp[acc_idx++];
out_params->rt8.p2 = distortion_tmp[acc_idx++];
out_params->rt8.k3 = distortion_tmp[acc_idx++];
out_params->rt8.k4 = distortion_tmp[acc_idx++];
out_params->rt8.k5 = distortion_tmp[acc_idx++];
out_params->rt8.k6 = distortion_tmp[acc_idx++];
if (cc->distortion_model == T_DISTORTION_WMR) {
out_params->rt8.metric_radius = cc->wmr.rpmax;
} else {
out_params->rt8.metric_radius = 0;
}
out_params->model = T_DISTORTION_OPENCV_RADTAN_8;
}
/*
* "Exported" functions.
*/
/*!
* Takes a @ref t_camera_calibration through \p cc, and returns a @ref t_camera_model_params that shadows \p cc
* 's parameters through \p out_params
*/
static inline void
t_camera_model_params_from_t_camera_calibration(const struct t_camera_calibration *cc,
struct t_camera_model_params *out_params)
{
// Paranoia.
U_ZERO(out_params);
// First row, first column.
out_params->fx = (float)cc->intrinsics[0][0];
// Second row, second column.
out_params->fy = (float)cc->intrinsics[1][1];
// First row, third column.
out_params->cx = (float)cc->intrinsics[0][2];
// Second row, third column.
out_params->cy = (float)cc->intrinsics[1][2];
out_params->model = cc->distortion_model;
switch (cc->distortion_model) {
case T_DISTORTION_FISHEYE_KB4: {
out_params->fisheye.k1 = (float)cc->kb4.k1;
out_params->fisheye.k2 = (float)cc->kb4.k2;
out_params->fisheye.k3 = (float)cc->kb4.k3;
out_params->fisheye.k4 = (float)cc->kb4.k4;
} break;
case T_DISTORTION_OPENCV_RADTAN_14:
case T_DISTORTION_OPENCV_RADTAN_5: break;
case T_DISTORTION_OPENCV_RADTAN_8:
case T_DISTORTION_WMR: interpret_as_rt8(cc, out_params); break;
default:
U_LOG_E("t_camera_un_projections doesn't support camera model %s yet!",
t_stringify_camera_distortion_model(cc->distortion_model));
break;
}
}
/*!
* Takes a 2D image-space point through \p x and \p y, unprojects it to a normalized 3D direction, and returns
* the result through \p out_x, \p out_y and \p out_z
*/
static inline bool
t_camera_models_unproject(
const struct t_camera_model_params *dist, const float x, const float y, float *out_x, float *out_y, float *out_z)
{
switch (dist->model) {
case T_DISTORTION_OPENCV_RADTAN_8: {
return rt8_unproject(dist, x, y, out_x, out_y, out_z);
}; break;
case T_DISTORTION_FISHEYE_KB4: {
return kb4_unproject(dist, x, y, out_x, out_y, out_z);
}; break;
// Return false so we don't get warnings on Release builds.
default: assert(false); return false;
}
}
/*!
* Takes a 2D image-space point through \p x and \p y, unprojects it to a normalized 3D direction, flips its Y
* and Z values (performing a coordinate space transform from +Z forward -Y up to -Z forward +Y up) and returns the
* result through \p out_x, \p out_y and \p out_z
*/
static inline bool
t_camera_models_unproject_and_flip(
const struct t_camera_model_params *dist, const float x, const float y, float *out_x, float *out_y, float *out_z)
{
bool ret = t_camera_models_unproject(dist, x, y, out_x, out_y, out_z);
*out_z *= -1;
*out_y *= -1;
return ret;
}
/*!
* Takes a 3D point through \p x, \p y, and \p z, projects it into image space, and returns the result
* through \p out_x and \p out_y
*/
static inline bool
t_camera_models_project(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
const float z, //
float *out_x, //
float *out_y)
{
switch (dist->model) {
case T_DISTORTION_OPENCV_RADTAN_8: {
return rt8_project(dist, x, y, z, out_x, out_y);
}; break;
case T_DISTORTION_FISHEYE_KB4: {
return kb4_project(dist, x, y, z, out_x, out_y);
}; break;
// Return false so we don't get warnings on Release builds.
default: assert(false); return false;
}
}
/*!
* Takes a 3D point through \p x, \p y, and \p z, flips its Y and Z values (performing a coordinate space
* transform from -Z forward +Y up to +Z forward -Y up), projects it into image space, and returns the result through
* \p out_x and \p out_y
*/
static inline bool
t_camera_models_flip_and_project(const struct t_camera_model_params *dist, //
const float x, //
const float y, //
const float z, //
float *out_x, //
float *out_y)
{
float _y = y * -1;
float _z = z * -1;
return t_camera_models_project(dist, x, _y, _z, out_x, out_y);
}