sparse-sfm: complete Gate C geometry validation

This commit is contained in:
fy59 2026-08-10 13:22:04 +02:00
parent 95700de339
commit 67529791c4
7 changed files with 1464 additions and 4 deletions

View file

@ -781,6 +781,9 @@ validation, migration v15→v16 et comparateur fresh/migrated validés par Gate
Le modèle de persistance est gelé pour v1 ; le solveur numérique reste hors de Le modèle de persistance est gelé pour v1 ; le solveur numérique reste hors de
Project DB v16. Project DB v16.
Gate C ajoute uniquement des primitives numériques pures hors Project DB; aucune
table, migration ou identité v16 supplémentaire n'est introduite.
**IMPLEMENTED** — API C Track Model v1 : header `project_db.h` et **IMPLEMENTED** — API C Track Model v1 : header `project_db.h` et
source `project_db.c` exposent `create_track_set`, `load_track_set`, source `project_db.c` exposent `create_track_set`, `load_track_set`,
`find_track_set`, `list_track_sets`, `load_track`, `list_tracks`, `find_track_set`, `list_track_sets`, `load_track`, `list_tracks`,

View file

@ -124,15 +124,17 @@ USAC/MAGSAC avec configuration, seed et fingerprint déterministes.
**Statut :** COMPLETED/FROZEN — le Track Builder v1 direct et durable est **Statut :** COMPLETED/FROZEN — le Track Builder v1 direct et durable est
implémenté dans Project DB v15 (`track_sets`, `tracks`, `track_observations` et implémenté dans Project DB v15 (`track_sets`, `tracks`, `track_observations` et
le payload de tâche). La triangulation et le solveur Sparse SfM restent PLANNED; le payload de tâche). Les primitives de géométrie calibrée Gate C sont
le modèle de persistance Sparse SfM v16 est gelé après Gate B. implémentées; le solveur incrémental complet et l'orchestration restent PLANNED.
Le modèle de persistance Sparse SfM v16 est gelé après Gate B.
**Sparse SfM Gate A : PASS.** Le contrat géométrique, la stratégie **Sparse SfM Gate A : PASS.** Le contrat géométrique, la stratégie
incremental, la triangulation candidate, le gauge, les conventions de pose, incremental, la triangulation candidate, le gauge, les conventions de pose,
les limites BA et l'enveloppe matérielle sont documentés dans les limites BA et l'enveloppe matérielle sont documentés dans
`architecture/sparse_sfm.md`. Le solveur Sparse SfM reste `architecture/sparse_sfm.md`. Le solveur Sparse SfM reste
**NOT_IMPLEMENTED** jusqu'aux Gates B3G; sa **NOT_IMPLEMENTED** jusqu'aux Gates DG; ses primitives pures calibrées sont
persistance v16 et ses lecteurs bornés sont implémentés en B2. implémentées en Gate C, tandis que sa persistance v16 et ses lecteurs bornés
restent ceux de B2.
--- ---

View file

@ -383,6 +383,60 @@ metric alignment, persistent reconstruction schema and durable SfM checkpoints.
Their semantic ownership is decided here; their final numeric values require Their semantic ownership is decided here; their final numeric values require
the synthetic ground-truth and sparse-solver gates. the synthetic ground-truth and sparse-solver gates.
## Gate C — pure calibrated geometry
**GATE C — PASS.** Pure calibrated geometry primitives, synthetic ground truth,
degeneracy rejection, determinism, normal suite and ASan/UBSan validation are
complete. Incremental orchestration, BA and persistent geometry integration
remain later gates.
Gate C keeps geometry outside Project DB and exposes a C17-safe, synchronous
pure-primitive boundary. Inputs are binary64 calibrated pixels, fixed
world-to-camera poses, and caller-owned correspondence arrays; no primitive
opens SQLite, reads Feature Files, loads images or invokes the Task Runtime.
The v1 candidate uses OpenCV 5.0.0 `calib3d` operations with every scientific
parameter supplied by an explicit configuration structure. Public outputs use
row-major binary64 `R_cw` and `t_cw`; relative translation has unit norm and no
metric interpretation.
The candidate contract requires deterministic caller ordering, finite inputs,
explicit robust-estimator thresholds/confidence/iteration limits and a local
seed. Essential hypotheses are accepted only after explicit positive-depth
support, rotation validation, parallax and reprojection checks. Pure rotation,
low parallax, weak conditioning and non-finite results are failures. Two-view
and multi-view points use normalized-coordinate linear DLT followed by bounded
point-only binary64 refinement; PnP returns world-to-camera pose with explicit
cheirality and inlier diagnostics. The tested v1 parameter set is frozen by the
Gate C ground-truth and degeneracy evidence; future orchestration may choose
other explicitly fingerprinted configurations.
### Gate C tested threshold set
The pure API has no hidden defaults; callers provide all acceptance settings.
The Gate C reference matrix uses the following reproducible set:
| Parameter | Value | Unit/purpose |
|---|---:|---|
| Relative robust threshold | 1.0 px clean; 1.5 px matrix | pixel residual |
| Relative confidence | 0.999 | RANSAC confidence |
| Relative iterations | 1000 clean; 1500 matrix | iterations |
| Relative minimum inliers | 6 clean; 24 matrix | correspondences |
| Relative minimum ratio | 0.75 clean; 0.5 matrix | fraction |
| Minimum parallax | `1e-4` rad | seed geometry |
| Minimum cheirality ratio | 0.5 | positive depth |
| PnP threshold | 1.0 px clean; 1.5 px matrix | pixel residual |
| PnP confidence | 0.999 | RANSAC confidence |
| PnP iterations | 1000 | iterations |
| PnP minimum inliers | 6 clean; 12 matrix | correspondences |
| PnP minimum ratio | 0.75 clean; 0.5 matrix | fraction |
| Point refinement tolerance | `1e-12` | normalized residual |
| Point refinement iterations | 30 | iterations |
Degeneracy checks use finite values, positive depth, rotation SO(3) residual
`1e-6`, depth epsilon `1e-9`, homogeneous scale epsilon `1e-12`, and
collinearity covariance determinant `1e-10`. These are pure-geometry
parameters and do not alter Project DB identity.
## Out of scope ## Out of scope
No production Sparse SfM, triangulator, camera solver, BA, Project DB v16, No production Sparse SfM, triangulator, camera solver, BA, Project DB v16,

View file

@ -0,0 +1,138 @@
#ifndef LARDON3D_SPARSE_SFM_GEOMETRY_H
#define LARDON3D_SPARSE_SFM_GEOMETRY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LARDON3D_SPARSE_GEOMETRY_OK = 0,
LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT,
LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT,
LARDON3D_SPARSE_GEOMETRY_INSUFFICIENT_CORRESPONDENCES,
LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED,
LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX,
LARDON3D_SPARSE_GEOMETRY_DEGENERATE,
LARDON3D_SPARSE_GEOMETRY_CHEIRALITY_FAILED,
LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE
} Lardon3DSparseGeometryResult;
typedef struct {
uint32_t width;
uint32_t height;
double fx;
double fy;
double cx;
double cy;
double k1;
double k2;
double p1;
double p2;
} Lardon3DSparseGeometryCalibration;
typedef struct {
double x;
double y;
} Lardon3DSparseGeometryPoint2;
typedef struct {
double x;
double y;
double z;
} Lardon3DSparseGeometryPoint3;
typedef struct {
double rotation_cw[9];
double translation_cw[3];
} Lardon3DSparseGeometryPose;
typedef struct {
double robust_threshold_px;
double confidence;
uint32_t max_iterations;
uint32_t minimum_inliers;
double minimum_inlier_ratio;
double minimum_parallax_rad;
double minimum_cheirality_ratio;
uint64_t deterministic_seed;
} Lardon3DSparseGeometryRelativePoseParameters;
typedef struct {
Lardon3DSparseGeometryPose pose_ba;
uint32_t inlier_count;
double inlier_ratio;
double median_parallax_rad;
uint8_t *inlier_mask;
size_t inlier_mask_capacity;
} Lardon3DSparseGeometryRelativePoseResult;
typedef struct {
double reprojection_threshold_px;
double confidence;
uint32_t max_iterations;
uint32_t minimum_inliers;
double minimum_inlier_ratio;
uint64_t deterministic_seed;
} Lardon3DSparseGeometryPnPParameters;
typedef struct {
Lardon3DSparseGeometryPose pose_cw;
uint32_t inlier_count;
double inlier_ratio;
uint8_t *inlier_mask;
size_t inlier_mask_capacity;
} Lardon3DSparseGeometryPnPResult;
typedef struct {
uint32_t max_iterations;
double convergence_tolerance;
} Lardon3DSparseGeometryPointRefinementParameters;
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_normalize(
const Lardon3DSparseGeometryCalibration *calibration,
const Lardon3DSparseGeometryPoint2 *pixels, size_t count,
Lardon3DSparseGeometryPoint2 *normalized);
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
const Lardon3DSparseGeometryCalibration *calibration_a,
const Lardon3DSparseGeometryCalibration *calibration_b,
const Lardon3DSparseGeometryPoint2 *pixels_a,
const Lardon3DSparseGeometryPoint2 *pixels_b, size_t count,
const Lardon3DSparseGeometryRelativePoseParameters *parameters,
Lardon3DSparseGeometryRelativePoseResult *result);
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_triangulate_two_view(
const Lardon3DSparseGeometryPoint2 *normalized_a,
const Lardon3DSparseGeometryPoint2 *normalized_b,
const Lardon3DSparseGeometryPose *pose_a,
const Lardon3DSparseGeometryPose *pose_b,
Lardon3DSparseGeometryPoint3 *point);
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_triangulate_multi_view(
const Lardon3DSparseGeometryPoint2 *normalized_points,
const Lardon3DSparseGeometryPose *poses, size_t view_count,
Lardon3DSparseGeometryPoint3 *point);
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_refine_point(
const Lardon3DSparseGeometryPoint2 *normalized_points,
const Lardon3DSparseGeometryPose *poses, size_t view_count,
const Lardon3DSparseGeometryPoint3 *initial_point,
const Lardon3DSparseGeometryPointRefinementParameters *parameters,
Lardon3DSparseGeometryPoint3 *refined_point);
Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
const Lardon3DSparseGeometryCalibration *calibration,
const Lardon3DSparseGeometryPoint3 *points,
const Lardon3DSparseGeometryPoint2 *pixels, size_t count,
const Lardon3DSparseGeometryPnPParameters *parameters,
Lardon3DSparseGeometryPnPResult *result);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -144,6 +144,7 @@ executable(
'src/image_view.c', 'src/image_view.c',
'src/project.c', 'src/project.c',
'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/sparse_sfm_geometry.cpp',
'src/task.c', 'src/task.c',
'src/task_checkpoint.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kind_registry.c',
@ -523,6 +524,15 @@ sparse_sfm_model_test = executable(
test('sparse-sfm-model', sparse_sfm_model_test, timeout: 30) test('sparse-sfm-model', sparse_sfm_model_test, timeout: 30)
sparse_sfm_geometry_test = executable(
'test-sparse-sfm-geometry',
sources: ['tests/test_sparse_sfm_geometry.cpp', 'src/sparse_sfm_geometry.cpp'],
include_directories: include_directories('include'),
dependencies: [opencv_geometry],
)
test('sparse-sfm-geometry', sparse_sfm_geometry_test, timeout: 60)
sparse_sfm_resource_test = executable( sparse_sfm_resource_test = executable(
'test-sparse-sfm-resource', 'test-sparse-sfm-resource',
sources: [ sources: [

604
src/sparse_sfm_geometry.cpp Normal file
View file

@ -0,0 +1,604 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <vector>
#include <opencv2/calib3d.hpp>
#include <opencv2/core.hpp>
#include <lardon3d/sparse_sfm_geometry.h>
namespace {
using Result = Lardon3DSparseGeometryResult;
struct RngGuard {
uint64_t saved;
explicit RngGuard(uint64_t seed) : saved(cv::theRNG().state) {
cv::theRNG().state = seed;
}
~RngGuard() { cv::theRNG().state = saved; }
};
bool finite_value(double value) { return std::isfinite(value); }
bool calibration_valid(const Lardon3DSparseGeometryCalibration &calibration) {
return calibration.width > 0 && calibration.height > 0 &&
calibration.fx > 0.0 && calibration.fy > 0.0 &&
calibration.cx >= 0.0 && calibration.cx < calibration.width &&
calibration.cy >= 0.0 && calibration.cy < calibration.height &&
finite_value(calibration.fx) && finite_value(calibration.fy) &&
finite_value(calibration.cx) && finite_value(calibration.cy) &&
finite_value(calibration.k1) && finite_value(calibration.k2) &&
finite_value(calibration.p1) && finite_value(calibration.p2);
}
bool points_finite(const Lardon3DSparseGeometryPoint2 *points, size_t count) {
if (!points)
return false;
for (size_t index = 0; index < count; ++index)
if (!finite_value(points[index].x) || !finite_value(points[index].y))
return false;
return true;
}
cv::Mat camera_matrix(const Lardon3DSparseGeometryCalibration &calibration) {
cv::Mat matrix = cv::Mat::zeros(3, 3, CV_64F);
matrix.at<double>(0, 0) = calibration.fx;
matrix.at<double>(0, 2) = calibration.cx;
matrix.at<double>(1, 1) = calibration.fy;
matrix.at<double>(1, 2) = calibration.cy;
matrix.at<double>(2, 2) = 1.0;
return matrix;
}
cv::Mat distortion(const Lardon3DSparseGeometryCalibration &calibration) {
cv::Mat matrix = cv::Mat::zeros(1, 4, CV_64F);
matrix.at<double>(0, 0) = calibration.k1;
matrix.at<double>(0, 1) = calibration.k2;
matrix.at<double>(0, 2) = calibration.p1;
matrix.at<double>(0, 3) = calibration.p2;
return matrix;
}
bool rotation_valid(const cv::Mat &rotation) {
if (rotation.rows != 3 || rotation.cols != 3)
return false;
cv::Mat error = rotation.t() * rotation - cv::Mat::eye(3, 3, CV_64F);
double determinant = cv::determinant(rotation);
return cv::norm(error, cv::NORM_INF) < 1e-6 &&
std::abs(determinant - 1.0) < 1e-6;
}
void copy_pose(const cv::Mat &rotation, const cv::Mat &translation,
Lardon3DSparseGeometryPose *pose) {
for (int row = 0; row < 3; ++row)
for (int column = 0; column < 3; ++column)
pose->rotation_cw[row * 3 + column] = rotation.at<double>(row, column);
for (int index = 0; index < 3; ++index)
pose->translation_cw[index] = translation.at<double>(index, 0);
}
bool pose_finite(const Lardon3DSparseGeometryPose &pose) {
for (double value : pose.rotation_cw)
if (!finite_value(value))
return false;
for (double value : pose.translation_cw)
if (!finite_value(value))
return false;
return true;
}
cv::Mat pose_rotation(const Lardon3DSparseGeometryPose &pose) {
cv::Mat matrix(3, 3, CV_64F);
for (int row = 0; row < 3; ++row)
for (int column = 0; column < 3; ++column)
matrix.at<double>(row, column) = pose.rotation_cw[row * 3 + column];
return matrix;
}
cv::Mat pose_projection(const Lardon3DSparseGeometryPose &pose) {
cv::Mat projection = cv::Mat::zeros(3, 4, CV_64F);
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 3; ++column)
projection.at<double>(row, column) = pose.rotation_cw[row * 3 + column];
projection.at<double>(row, 3) = pose.translation_cw[row];
}
return projection;
}
bool triangulated_point_valid(const cv::Mat &homogeneous,
const cv::Mat &rotation_b,
const cv::Mat &translation_b,
cv::Mat *point) {
double scale = homogeneous.at<double>(3, 0);
if (!finite_value(scale) || std::abs(scale) < 1e-12)
return false;
cv::Mat candidate = homogeneous.rowRange(0, 3) / scale;
double depth_a = candidate.at<double>(2, 0);
cv::Mat point_b = rotation_b * candidate + translation_b;
double depth_b = point_b.at<double>(2, 0);
if (!finite_value(depth_a) || !finite_value(depth_b) || depth_a <= 1e-9 ||
depth_b <= 1e-9 || !cv::checkRange(candidate))
return false;
*point = candidate;
return true;
}
bool parallax_valid(const Lardon3DSparseGeometryPoint2 &point_a,
const Lardon3DSparseGeometryPoint2 &point_b,
const cv::Mat &rotation_b) {
cv::Mat ray_a(3, 1, CV_64F);
cv::Mat ray_b(3, 1, CV_64F);
ray_a.at<double>(0, 0) = point_a.x;
ray_a.at<double>(1, 0) = point_a.y;
ray_a.at<double>(2, 0) = 1.0;
ray_b.at<double>(0, 0) = point_b.x;
ray_b.at<double>(1, 0) = point_b.y;
ray_b.at<double>(2, 0) = 1.0;
ray_a /= cv::norm(ray_a);
ray_b = rotation_b.t() * ray_b;
ray_b /= cv::norm(ray_b);
double cosine = ray_a.dot(ray_b);
return std::acos(std::clamp(cosine, -1.0, 1.0)) >= 1e-4;
}
} // namespace
extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_normalize(
const Lardon3DSparseGeometryCalibration *calibration,
const Lardon3DSparseGeometryPoint2 *pixels, size_t count,
Lardon3DSparseGeometryPoint2 *normalized) {
if (!calibration || !pixels || !normalized || count == 0)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!calibration_valid(*calibration))
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!points_finite(pixels, count))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
try {
std::vector<cv::Point2d> source;
source.reserve(count);
for (size_t index = 0; index < count; ++index)
source.emplace_back(pixels[index].x, pixels[index].y);
std::vector<cv::Point2d> undistorted;
cv::undistortPoints(source, undistorted, camera_matrix(*calibration),
distortion(*calibration));
if (undistorted.size() != count)
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
for (size_t index = 0; index < count; ++index) {
normalized[index].x = undistorted[index].x;
normalized[index].y = undistorted[index].y;
if (!finite_value(normalized[index].x) ||
!finite_value(normalized[index].y))
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
return LARDON3D_SPARSE_GEOMETRY_OK;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}
extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_relative_pose(
const Lardon3DSparseGeometryCalibration *calibration_a,
const Lardon3DSparseGeometryCalibration *calibration_b,
const Lardon3DSparseGeometryPoint2 *pixels_a,
const Lardon3DSparseGeometryPoint2 *pixels_b, size_t count,
const Lardon3DSparseGeometryRelativePoseParameters *parameters,
Lardon3DSparseGeometryRelativePoseResult *result) {
if (!calibration_a || !calibration_b || !pixels_a || !pixels_b ||
!parameters || !result || count > static_cast<size_t>(INT_MAX) ||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
parameters->confidence >= 1.0 || parameters->robust_threshold_px <= 0.0 ||
parameters->minimum_parallax_rad < 0.0 ||
parameters->minimum_cheirality_ratio <= 0.0 ||
parameters->minimum_cheirality_ratio > 1.0)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (count < 5)
return LARDON3D_SPARSE_GEOMETRY_INSUFFICIENT_CORRESPONDENCES;
if (!calibration_valid(*calibration_a) || !calibration_valid(*calibration_b))
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!points_finite(pixels_a, count) || !points_finite(pixels_b, count))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
result->inlier_count = 0;
result->inlier_ratio = 0.0;
result->median_parallax_rad = 0.0;
if (result->inlier_mask && result->inlier_mask_capacity < count)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
try {
std::vector<cv::Point2d> normalized_a(count), normalized_b(count);
Lardon3DSparseGeometryResult status =
lardon3d_sparse_geometry_normalize(calibration_a, pixels_a, count,
reinterpret_cast<
Lardon3DSparseGeometryPoint2 *>(
normalized_a.data()));
if (status != LARDON3D_SPARSE_GEOMETRY_OK)
return status;
status = lardon3d_sparse_geometry_normalize(
calibration_b, pixels_b, count,
reinterpret_cast<Lardon3DSparseGeometryPoint2 *>(normalized_b.data()));
if (status != LARDON3D_SPARSE_GEOMETRY_OK)
return status;
double mean_ax = 0.0;
double mean_ay = 0.0;
double mean_bx = 0.0;
double mean_by = 0.0;
for (size_t index = 0; index < count; ++index) {
mean_ax += normalized_a[index].x;
mean_ay += normalized_a[index].y;
mean_bx += normalized_b[index].x;
mean_by += normalized_b[index].y;
}
mean_ax /= static_cast<double>(count);
mean_ay /= static_cast<double>(count);
mean_bx /= static_cast<double>(count);
mean_by /= static_cast<double>(count);
double covariance_axx = 0.0;
double covariance_ayy = 0.0;
double covariance_axy = 0.0;
double covariance_bxx = 0.0;
double covariance_byy = 0.0;
double covariance_bxy = 0.0;
for (size_t index = 0; index < count; ++index) {
double ax = normalized_a[index].x - mean_ax;
double ay = normalized_a[index].y - mean_ay;
double bx = normalized_b[index].x - mean_bx;
double by = normalized_b[index].y - mean_by;
covariance_axx += ax * ax;
covariance_ayy += ay * ay;
covariance_axy += ax * ay;
covariance_bxx += bx * bx;
covariance_byy += by * by;
covariance_bxy += bx * by;
}
if (covariance_axx * covariance_ayy - covariance_axy * covariance_axy <
1e-10 ||
covariance_bxx * covariance_byy - covariance_bxy * covariance_bxy <
1e-10)
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
cv::Mat points_a(static_cast<int>(count), 2, CV_64F, normalized_a.data());
cv::Mat points_b(static_cast<int>(count), 2, CV_64F, normalized_b.data());
cv::Mat mask;
RngGuard rng(parameters->deterministic_seed);
double normalized_threshold = parameters->robust_threshold_px /
std::max(calibration_a->fx,
calibration_a->fy);
cv::Mat essential = cv::findEssentialMat(
points_a, points_b, 1.0, cv::Point2d(0, 0), cv::RANSAC,
parameters->confidence, normalized_threshold,
static_cast<int>(parameters->max_iterations), mask);
if (essential.empty())
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
cv::Mat rotation, translation;
int inliers = cv::recoverPose(essential, points_a, points_b, rotation,
translation, 1.0, cv::Point2d(0, 0), mask);
double count_value = static_cast<double>(count);
if (inliers < static_cast<int>(parameters->minimum_inliers) ||
static_cast<double>(inliers) / count_value <
parameters->minimum_inlier_ratio ||
!rotation_valid(rotation) || cv::norm(translation) < 1e-12)
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
std::vector<double> parallaxes;
cv::Mat projection_a = cv::Mat::zeros(3, 4, CV_64F);
projection_a.at<double>(0, 0) = projection_a.at<double>(1, 1) =
projection_a.at<double>(2, 2) = 1.0;
cv::Mat projection_b = cv::Mat::zeros(3, 4, CV_64F);
rotation.copyTo(projection_b.colRange(0, 3));
translation.copyTo(projection_b.col(3));
cv::Mat points_4d;
cv::triangulatePoints(projection_a, projection_b, points_a.t(), points_b.t(),
points_4d);
for (int index = 0; index < points_4d.cols; ++index) {
if (!mask.at<unsigned char>(index))
continue;
cv::Mat point;
if (!triangulated_point_valid(points_4d.col(index), rotation, translation,
&point))
continue;
cv::Vec3d ray_a(normalized_a[index].x, normalized_a[index].y, 1.0);
cv::Vec3d ray_b(normalized_b[index].x, normalized_b[index].y, 1.0);
ray_a = ray_a / cv::norm(ray_a);
ray_b = ray_b / cv::norm(ray_b);
cv::Mat ray_b_input(3, 1, CV_64F);
ray_b_input.at<double>(0, 0) = ray_b[0];
ray_b_input.at<double>(1, 0) = ray_b[1];
ray_b_input.at<double>(2, 0) = ray_b[2];
cv::Mat ray_b_mat = rotation.t() * ray_b_input;
ray_b = cv::Vec3d(ray_b_mat.at<double>(0, 0),
ray_b_mat.at<double>(1, 0),
ray_b_mat.at<double>(2, 0));
double cosine = std::clamp(ray_a.dot(ray_b), -1.0, 1.0);
parallaxes.push_back(std::acos(cosine));
}
if (parallaxes.empty() ||
parallaxes.size() < static_cast<size_t>(
static_cast<double>(inliers) *
parameters->minimum_cheirality_ratio))
return LARDON3D_SPARSE_GEOMETRY_CHEIRALITY_FAILED;
std::sort(parallaxes.begin(), parallaxes.end());
double median = parallaxes[parallaxes.size() / 2];
result->median_parallax_rad = median;
if (median < parameters->minimum_parallax_rad)
return LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX;
copy_pose(rotation, translation, &result->pose_ba);
result->inlier_count = static_cast<uint32_t>(inliers);
result->inlier_ratio = static_cast<double>(inliers) / count_value;
if (result->inlier_mask)
for (size_t index = 0; index < count; ++index)
result->inlier_mask[index] = mask.at<unsigned char>(static_cast<int>(index));
return LARDON3D_SPARSE_GEOMETRY_OK;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}
extern "C" Lardon3DSparseGeometryResult
lardon3d_sparse_geometry_triangulate_two_view(
const Lardon3DSparseGeometryPoint2 *normalized_a,
const Lardon3DSparseGeometryPoint2 *normalized_b,
const Lardon3DSparseGeometryPose *pose_a,
const Lardon3DSparseGeometryPose *pose_b,
Lardon3DSparseGeometryPoint3 *point) {
if (!normalized_a || !normalized_b || !pose_a || !pose_b || !point ||
!pose_finite(*pose_a) || !pose_finite(*pose_b) ||
!rotation_valid(pose_rotation(*pose_a)) ||
!rotation_valid(pose_rotation(*pose_b)) ||
!finite_value(normalized_a->x) || !finite_value(normalized_a->y) ||
!finite_value(normalized_b->x) || !finite_value(normalized_b->y))
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
try {
cv::Mat points_a(2, 1, CV_64F);
cv::Mat points_b(2, 1, CV_64F);
points_a.at<double>(0, 0) = normalized_a->x;
points_a.at<double>(1, 0) = normalized_a->y;
points_b.at<double>(0, 0) = normalized_b->x;
points_b.at<double>(1, 0) = normalized_b->y;
cv::Mat homogeneous;
cv::triangulatePoints(pose_projection(*pose_a), pose_projection(*pose_b),
points_a, points_b, homogeneous);
cv::Mat candidate;
cv::Mat rotation_b = pose_rotation(*pose_b);
cv::Mat translation_b(3, 1, CV_64F);
for (int index = 0; index < 3; ++index)
translation_b.at<double>(index, 0) = pose_b->translation_cw[index];
if (!parallax_valid(*normalized_a, *normalized_b, rotation_b))
return LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX;
if (!triangulated_point_valid(homogeneous.col(0), rotation_b,
translation_b, &candidate))
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
point->x = candidate.at<double>(0, 0);
point->y = candidate.at<double>(1, 0);
point->z = candidate.at<double>(2, 0);
return LARDON3D_SPARSE_GEOMETRY_OK;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}
extern "C" Lardon3DSparseGeometryResult
lardon3d_sparse_geometry_triangulate_multi_view(
const Lardon3DSparseGeometryPoint2 *normalized_points,
const Lardon3DSparseGeometryPose *poses, size_t view_count,
Lardon3DSparseGeometryPoint3 *point) {
if (!normalized_points || !poses || !point || view_count < 2 ||
view_count > static_cast<size_t>(INT_MAX / 2))
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
try {
cv::Mat design = cv::Mat::zeros(static_cast<int>(view_count * 2), 4,
CV_64F);
for (size_t view = 0; view < view_count; ++view) {
if (!pose_finite(poses[view]) || !rotation_valid(pose_rotation(poses[view])) ||
!finite_value(normalized_points[view].x) ||
!finite_value(normalized_points[view].y))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
cv::Mat projection = pose_projection(poses[view]);
int row = static_cast<int>(view * 2);
design.row(row) = normalized_points[view].x * projection.row(2) -
projection.row(0);
design.row(row + 1) = normalized_points[view].y * projection.row(2) -
projection.row(1);
}
cv::SVD decomposition(design, cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
cv::Mat homogeneous = decomposition.vt.row(3).t();
cv::Mat candidate;
cv::Mat identity_rotation = cv::Mat::eye(3, 3, CV_64F);
cv::Mat zero_translation = cv::Mat::zeros(3, 1, CV_64F);
if (!triangulated_point_valid(homogeneous, identity_rotation,
zero_translation, &candidate))
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
for (size_t view = 0; view < view_count; ++view) {
cv::Mat camera_point = pose_rotation(poses[view]) * candidate;
for (int index = 0; index < 3; ++index)
camera_point.at<double>(index, 0) += poses[view].translation_cw[index];
if (!finite_value(camera_point.at<double>(2, 0)) ||
camera_point.at<double>(2, 0) <= 1e-9)
return LARDON3D_SPARSE_GEOMETRY_CHEIRALITY_FAILED;
}
if (!parallax_valid(normalized_points[0], normalized_points[1],
pose_rotation(poses[1])))
return LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX;
point->x = candidate.at<double>(0, 0);
point->y = candidate.at<double>(1, 0);
point->z = candidate.at<double>(2, 0);
return LARDON3D_SPARSE_GEOMETRY_OK;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}
extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_refine_point(
const Lardon3DSparseGeometryPoint2 *normalized_points,
const Lardon3DSparseGeometryPose *poses, size_t view_count,
const Lardon3DSparseGeometryPoint3 *initial_point,
const Lardon3DSparseGeometryPointRefinementParameters *parameters,
Lardon3DSparseGeometryPoint3 *refined_point) {
if (!normalized_points || !poses || !initial_point || !parameters ||
!refined_point || view_count < 2 || parameters->max_iterations == 0 ||
parameters->convergence_tolerance <= 0.0)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!finite_value(initial_point->x) || !finite_value(initial_point->y) ||
!finite_value(initial_point->z))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
cv::Mat point(3, 1, CV_64F);
point.at<double>(0, 0) = initial_point->x;
point.at<double>(1, 0) = initial_point->y;
point.at<double>(2, 0) = initial_point->z;
try {
auto objective = [&](const cv::Mat &candidate, cv::Mat *residual) {
cv::Mat values = cv::Mat::zeros(static_cast<int>(view_count * 2), 1,
CV_64F);
double sum = 0.0;
for (size_t view = 0; view < view_count; ++view) {
cv::Mat camera_point = pose_rotation(poses[view]) * candidate;
for (int index = 0; index < 3; ++index)
camera_point.at<double>(index, 0) += poses[view].translation_cw[index];
double z = camera_point.at<double>(2, 0);
if (!finite_value(z) || z <= 1e-9)
return std::numeric_limits<double>::infinity();
double x = camera_point.at<double>(0, 0) / z;
double y = camera_point.at<double>(1, 0) / z;
values.at<double>(static_cast<int>(view * 2), 0) =
x - normalized_points[view].x;
values.at<double>(static_cast<int>(view * 2 + 1), 0) =
y - normalized_points[view].y;
sum += values.at<double>(static_cast<int>(view * 2), 0) *
values.at<double>(static_cast<int>(view * 2), 0) +
values.at<double>(static_cast<int>(view * 2 + 1), 0) *
values.at<double>(static_cast<int>(view * 2 + 1), 0);
}
*residual = values;
return sum;
};
cv::Mat residual;
double current = objective(point, &residual);
if (!std::isfinite(current))
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
for (uint32_t iteration = 0; iteration < parameters->max_iterations;
++iteration) {
cv::Mat jacobian(static_cast<int>(view_count * 2), 3, CV_64F);
const double step = 1e-7;
for (int axis = 0; axis < 3; ++axis) {
cv::Mat perturbed = point.clone();
perturbed.at<double>(axis, 0) += step;
cv::Mat shifted;
if (!std::isfinite(objective(perturbed, &shifted)))
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
jacobian.col(axis) = (shifted - residual) / step;
}
cv::Mat normal = jacobian.t() * jacobian;
cv::Mat rhs = -jacobian.t() * residual;
cv::Mat delta;
if (!cv::solve(normal, rhs, delta, cv::DECOMP_SVD))
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
cv::Mat candidate = point + delta;
cv::Mat candidate_residual;
double next = objective(candidate, &candidate_residual);
if (!std::isfinite(next) || next > current)
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
point = candidate;
residual = candidate_residual;
if (cv::norm(delta) <= parameters->convergence_tolerance ||
std::abs(current - next) <= parameters->convergence_tolerance) {
refined_point->x = point.at<double>(0, 0);
refined_point->y = point.at<double>(1, 0);
refined_point->z = point.at<double>(2, 0);
return LARDON3D_SPARSE_GEOMETRY_OK;
}
current = next;
}
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}
extern "C" Lardon3DSparseGeometryResult lardon3d_sparse_geometry_pnp(
const Lardon3DSparseGeometryCalibration *calibration,
const Lardon3DSparseGeometryPoint3 *points,
const Lardon3DSparseGeometryPoint2 *pixels, size_t count,
const Lardon3DSparseGeometryPnPParameters *parameters,
Lardon3DSparseGeometryPnPResult *result) {
if (!calibration || !points || !pixels || !parameters || !result ||
count < 4 || count > static_cast<size_t>(INT_MAX) ||
parameters->max_iterations == 0 || parameters->confidence <= 0.0 ||
parameters->confidence >= 1.0 || parameters->reprojection_threshold_px <= 0.0 ||
parameters->minimum_inlier_ratio <= 0.0 ||
parameters->minimum_inlier_ratio > 1.0)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (!calibration_valid(*calibration))
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
if (result->inlier_mask && result->inlier_mask_capacity < count)
return LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT;
for (size_t index = 0; index < count; ++index)
if (!finite_value(points[index].x) || !finite_value(points[index].y) ||
!finite_value(points[index].z) || !finite_value(pixels[index].x) ||
!finite_value(pixels[index].y))
return LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT;
try {
std::vector<cv::Point3d> object_points;
std::vector<cv::Point2d> image_points;
object_points.reserve(count);
image_points.reserve(count);
for (size_t index = 0; index < count; ++index) {
object_points.emplace_back(points[index].x, points[index].y,
points[index].z);
image_points.emplace_back(pixels[index].x, pixels[index].y);
}
cv::Mat object_matrix(static_cast<int>(count), 3, CV_64F);
cv::Scalar object_mean = cv::mean(object_points);
for (size_t index = 0; index < count; ++index) {
object_matrix.at<double>(static_cast<int>(index), 0) =
points[index].x - object_mean[0];
object_matrix.at<double>(static_cast<int>(index), 1) =
points[index].y - object_mean[1];
object_matrix.at<double>(static_cast<int>(index), 2) =
points[index].z - object_mean[2];
}
cv::SVD object_svd(object_matrix, cv::SVD::NO_UV);
if (object_svd.w.at<double>(1, 0) < object_svd.w.at<double>(0, 0) * 1e-8)
return LARDON3D_SPARSE_GEOMETRY_DEGENERATE;
cv::Mat rvec, tvec, inliers;
RngGuard rng(parameters->deterministic_seed);
bool solved = cv::solvePnPRansac(
object_points, image_points, camera_matrix(*calibration),
distortion(*calibration), rvec, tvec, false, parameters->max_iterations,
static_cast<float>(parameters->reprojection_threshold_px),
parameters->confidence, inliers, cv::SOLVEPNP_EPNP);
uint32_t minimum = std::max<uint32_t>(parameters->minimum_inliers, 4);
double count_value = static_cast<double>(count);
if (!solved || inliers.rows < static_cast<int>(minimum) ||
static_cast<double>(inliers.rows) / count_value <
parameters->minimum_inlier_ratio)
return LARDON3D_SPARSE_GEOMETRY_ESTIMATION_FAILED;
cv::Mat rotation;
cv::Rodrigues(rvec, rotation);
if (!rotation_valid(rotation) || !cv::checkRange(tvec))
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
uint32_t positive_depth = 0;
for (int row = 0; row < inliers.rows; ++row) {
int point_index = inliers.at<int>(row, 0);
cv::Mat object(3, 1, CV_64F);
object.at<double>(0, 0) = points[point_index].x;
object.at<double>(1, 0) = points[point_index].y;
object.at<double>(2, 0) = points[point_index].z;
cv::Mat camera = rotation * object + tvec;
if (camera.at<double>(2, 0) > 1e-9)
++positive_depth;
}
if (static_cast<double>(positive_depth) /
static_cast<double>(inliers.rows) <
parameters->minimum_inlier_ratio)
return LARDON3D_SPARSE_GEOMETRY_CHEIRALITY_FAILED;
copy_pose(rotation, tvec, &result->pose_cw);
result->inlier_count = static_cast<uint32_t>(inliers.rows);
result->inlier_ratio = static_cast<double>(inliers.rows) / count_value;
if (result->inlier_mask) {
std::fill(result->inlier_mask, result->inlier_mask + count, 0);
for (int row = 0; row < inliers.rows; ++row)
result->inlier_mask[inliers.at<int>(row, 0)] = 1;
}
return LARDON3D_SPARSE_GEOMETRY_OK;
} catch (const cv::Exception &) {
return LARDON3D_SPARSE_GEOMETRY_NUMERIC_FAILURE;
}
}

View file

@ -0,0 +1,649 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <vector>
#include <lardon3d/sparse_sfm_geometry.h>
#define CHECK(value) \
do { \
if (!(value)) { \
std::fprintf(stderr, "geometry failure line %d: %s\n", __LINE__, #value); \
return 1; \
} \
} while (0)
static Lardon3DSparseGeometryPoint2 project(
const Lardon3DSparseGeometryCalibration &calibration,
const Lardon3DSparseGeometryPoint3 &point,
const Lardon3DSparseGeometryPose &pose) {
double x = pose.rotation_cw[0] * point.x + pose.rotation_cw[1] * point.y +
pose.rotation_cw[2] * point.z + pose.translation_cw[0];
double y = pose.rotation_cw[3] * point.x + pose.rotation_cw[4] * point.y +
pose.rotation_cw[5] * point.z + pose.translation_cw[1];
double z = pose.rotation_cw[6] * point.x + pose.rotation_cw[7] * point.y +
pose.rotation_cw[8] * point.z + pose.translation_cw[2];
Lardon3DSparseGeometryPoint2 projected = {calibration.fx * x / z + calibration.cx,
calibration.fy * y / z + calibration.cy};
return projected;
}
static double matrix_noise(size_t index, double amplitude) {
int value = static_cast<int>((index * 37U + 11U) % 17U) - 8;
return amplitude * static_cast<double>(value) / 8.0;
}
static double rotation_error(const Lardon3DSparseGeometryPose &pose) {
double trace = pose.rotation_cw[0] + pose.rotation_cw[4] +
pose.rotation_cw[8];
return std::acos(std::clamp((trace - 1.0) / 2.0, -1.0, 1.0));
}
static double translation_direction_error(
const Lardon3DSparseGeometryPose &pose, double x, double y, double z) {
double norm = std::sqrt(pose.translation_cw[0] * pose.translation_cw[0] +
pose.translation_cw[1] * pose.translation_cw[1] +
pose.translation_cw[2] * pose.translation_cw[2]);
double target_norm = std::sqrt(x * x + y * y + z * z);
double dot = (pose.translation_cw[0] * x + pose.translation_cw[1] * y +
pose.translation_cw[2] * z) /
(norm * target_norm);
return std::acos(std::clamp(dot, -1.0, 1.0));
}
static int matrix_test() {
Lardon3DSparseGeometryCalibration calibration =
{1280, 960, 800, 800, 640, 480, 0, 0, 0, 0};
Lardon3DSparseGeometryPose pose_a = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{0, 0, 0}};
Lardon3DSparseGeometryPose pose_b = pose_a;
pose_b.translation_cw[0] = 1.0;
double pnp_worst_rotation_error = 0.0;
double pnp_worst_translation_error = 0.0;
double pnp_worst_precision = 1.0;
double pnp_worst_recall = 1.0;
constexpr size_t count = 64;
Lardon3DSparseGeometryPoint2 pixels_a[count];
Lardon3DSparseGeometryPoint2 clean_b[count];
for (size_t index = 0; index < count; ++index) {
Lardon3DSparseGeometryPoint3 point = {
-1.0 + static_cast<double>(index % 8) * 0.28,
-0.7 + static_cast<double>(index / 8) * 0.18,
4.0 + static_cast<double>(index % 11) * 0.2};
pixels_a[index] = project(calibration, point, pose_a);
clean_b[index] = project(calibration, point, pose_b);
}
const double noises[] = {0.0, 0.25, 0.5, 1.5};
const double outlier_rates[] = {0.0, 0.1, 0.25, 0.4};
for (double noise : noises) {
for (double outlier_rate : outlier_rates) {
Lardon3DSparseGeometryPoint2 pixels_b[count];
size_t outliers = static_cast<size_t>(count * outlier_rate);
for (size_t index = 0; index < count; ++index) {
pixels_b[index] = clean_b[index];
pixels_b[index].x += matrix_noise(index, noise);
pixels_b[index].y += matrix_noise(index + 19, noise);
if (index >= count - outliers) {
pixels_b[index].x = 80.0 + static_cast<double>(index * 31U % 1100U);
pixels_b[index].y = 60.0 + static_cast<double>(index * 17U % 800U);
}
}
uint8_t mask[count] = {0};
Lardon3DSparseGeometryRelativePoseParameters parameters =
{1.5, 0.999, 1500, 24, 0.5, 1e-4, 0.5, 100 +
static_cast<uint64_t>(noise * 10)};
Lardon3DSparseGeometryRelativePoseResult result = {};
result.inlier_mask = mask;
result.inlier_mask_capacity = count;
Lardon3DSparseGeometryResult status =
lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, pixels_b, count,
&parameters, &result);
if (noise == 0.0 && outlier_rate == 0.0) {
CHECK(status == LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(rotation_error(result.pose_ba) < 0.02);
CHECK(translation_direction_error(result.pose_ba, 1, 0, 0) < 0.05);
}
if (status == LARDON3D_SPARSE_GEOMETRY_OK && noise <= 0.75 &&
outlier_rate <= 0.25) {
CHECK(rotation_error(result.pose_ba) < 0.25);
CHECK(translation_direction_error(result.pose_ba, 1, 0, 0) < 0.35);
CHECK(result.inlier_count >= 24);
}
CHECK(status != LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT);
}
}
Lardon3DSparseGeometryPoint2 collinear_a[count];
Lardon3DSparseGeometryPoint2 collinear_b[count];
for (size_t index = 0; index < count; ++index) {
collinear_a[index] = {500.0 + static_cast<double>(index), 480.0};
collinear_b[index] = {500.0 + static_cast<double>(index), 480.0};
}
uint8_t mask[count] = {0};
Lardon3DSparseGeometryRelativePoseParameters parameters =
{1.0, 0.999, 500, 8, 0.5, 1e-4, 0.5, 77};
Lardon3DSparseGeometryRelativePoseResult result = {};
result.inlier_mask = mask;
result.inlier_mask_capacity = count;
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, collinear_a, collinear_b, count,
&parameters, &result) != LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, clean_b, 4, &parameters,
&result) == LARDON3D_SPARSE_GEOMETRY_INSUFFICIENT_CORRESPONDENCES);
Lardon3DSparseGeometryPoint2 nan_point = {NAN, 0};
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &nan_point, 1,
&nan_point) ==
LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT);
Lardon3DSparseGeometryPose forward = pose_b;
forward.translation_cw[0] = 0;
forward.translation_cw[2] = 0.01;
for (size_t index = 0; index < count; ++index)
clean_b[index] = project(calibration,
{-1.0 + static_cast<double>(index % 8) * 0.28,
-0.7 + static_cast<double>(index / 8) * 0.18,
4.0 + static_cast<double>(index % 11) * 0.2},
forward);
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, clean_b, count, &parameters,
&result) != LARDON3D_SPARSE_GEOMETRY_OK);
const size_t pnp_count = 32;
Lardon3DSparseGeometryPoint3 pnp_points[pnp_count];
Lardon3DSparseGeometryPoint2 pnp_pixels[pnp_count];
for (size_t index = 0; index < pnp_count; ++index) {
pnp_points[index] = {-1.0 + static_cast<double>(index % 8) * 0.3,
-0.7 + static_cast<double>(index / 8) * 0.2,
4.0 + static_cast<double>(index % 5) * 0.3};
pnp_pixels[index] = project(calibration, pnp_points[index], pose_b);
}
uint8_t pnp_mask[pnp_count] = {0};
Lardon3DSparseGeometryPnPParameters pnp_parameters =
{1.5, 0.999, 1000, 12, 0.5, 404};
Lardon3DSparseGeometryPnPResult pnp_result = {};
pnp_result.inlier_mask = pnp_mask;
pnp_result.inlier_mask_capacity = pnp_count;
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, pnp_points, pnp_pixels, pnp_count,
&pnp_parameters, &pnp_result) == LARDON3D_SPARSE_GEOMETRY_OK);
const double pnp_noises[] = {0.0, 0.25, 0.75, 2.0};
const double pnp_outliers[] = {0.0, 0.125, 0.25, 0.4};
for (double noise : pnp_noises) {
for (double outlier_rate : pnp_outliers) {
Lardon3DSparseGeometryPoint2 altered[pnp_count];
size_t outlier_count = static_cast<size_t>(pnp_count * outlier_rate);
for (size_t index = 0; index < pnp_count; ++index) {
altered[index] = pnp_pixels[index];
altered[index].x += matrix_noise(index, noise);
altered[index].y += matrix_noise(index + 31, noise);
if (index >= pnp_count - outlier_count) {
altered[index].x = 100.0 + static_cast<double>(index * 41U % 1000U);
altered[index].y = 100.0 + static_cast<double>(index * 23U % 700U);
}
}
Lardon3DSparseGeometryResult status = lardon3d_sparse_geometry_pnp(
&calibration, pnp_points, altered, pnp_count, &pnp_parameters,
&pnp_result);
CHECK(status != LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT);
if (status == LARDON3D_SPARSE_GEOMETRY_OK) {
size_t true_positive = 0;
size_t false_positive = 0;
size_t false_negative = 0;
for (size_t index = 0; index < pnp_count; ++index) {
bool expected_inlier = index < pnp_count - outlier_count;
bool actual_inlier = pnp_mask[index] != 0;
if (expected_inlier && actual_inlier)
++true_positive;
else if (!expected_inlier && actual_inlier)
++false_positive;
else if (expected_inlier)
++false_negative;
}
double precision = true_positive == 0
? 0.0
: static_cast<double>(true_positive) /
static_cast<double>(true_positive + false_positive);
double recall = static_cast<double>(true_positive) /
static_cast<double>(true_positive + false_negative);
pnp_worst_precision = std::min(pnp_worst_precision, precision);
pnp_worst_recall = std::min(pnp_worst_recall, recall);
pnp_worst_rotation_error =
std::max(pnp_worst_rotation_error, rotation_error(pnp_result.pose_cw));
pnp_worst_translation_error = std::max(
pnp_worst_translation_error,
std::sqrt((pnp_result.pose_cw.translation_cw[0] - 1.0) *
(pnp_result.pose_cw.translation_cw[0] - 1.0) +
pnp_result.pose_cw.translation_cw[1] *
pnp_result.pose_cw.translation_cw[1] +
pnp_result.pose_cw.translation_cw[2] *
pnp_result.pose_cw.translation_cw[2]));
}
}
}
Lardon3DSparseGeometryPoint3 collinear_points[pnp_count];
Lardon3DSparseGeometryPoint2 collinear_pixels[pnp_count];
for (size_t index = 0; index < pnp_count; ++index) {
collinear_points[index] = {static_cast<double>(index) * 0.1, 0, 4};
collinear_pixels[index] = project(calibration, collinear_points[index], pose_b);
}
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, collinear_points, collinear_pixels, pnp_count,
&pnp_parameters, &pnp_result) ==
LARDON3D_SPARSE_GEOMETRY_DEGENERATE);
Lardon3DSparseGeometryPoint3 duplicate_points[pnp_count];
Lardon3DSparseGeometryPoint2 duplicate_pixels[pnp_count];
for (size_t index = 0; index < pnp_count; ++index) {
duplicate_points[index] = pnp_points[0];
duplicate_pixels[index] = pnp_pixels[0];
}
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, duplicate_points, duplicate_pixels, pnp_count,
&pnp_parameters, &pnp_result) ==
LARDON3D_SPARSE_GEOMETRY_DEGENERATE);
Lardon3DSparseGeometryPoint3 planar_points[pnp_count];
Lardon3DSparseGeometryPoint3 near_planar_points[pnp_count];
Lardon3DSparseGeometryPoint3 far_points[pnp_count];
Lardon3DSparseGeometryPoint2 planar_pixels[pnp_count];
Lardon3DSparseGeometryPoint2 near_planar_pixels[pnp_count];
Lardon3DSparseGeometryPoint2 far_pixels[pnp_count];
for (size_t index = 0; index < pnp_count; ++index) {
double x = -1.0 + static_cast<double>(index % 8) * 0.3;
double y = -0.7 + static_cast<double>(index / 8) * 0.2;
planar_points[index] = {x, y, 4.0};
near_planar_points[index] = {x, y, 4.0 + static_cast<double>(index % 3) * 1e-4};
far_points[index] = {x, y, 1000.0 + static_cast<double>(index % 3)};
planar_pixels[index] = project(calibration, planar_points[index], pose_b);
near_planar_pixels[index] =
project(calibration, near_planar_points[index], pose_b);
far_pixels[index] = project(calibration, far_points[index], pose_b);
}
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, planar_points, planar_pixels, pnp_count,
&pnp_parameters, &pnp_result) == LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, near_planar_points, near_planar_pixels, pnp_count,
&pnp_parameters, &pnp_result) !=
LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT);
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, far_points, far_pixels, pnp_count, &pnp_parameters,
&pnp_result) != LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT);
for (size_t view_count : {size_t(2), size_t(3), size_t(5), size_t(10)}) {
Lardon3DSparseGeometryPoint2 observations[10];
Lardon3DSparseGeometryPose poses[10];
Lardon3DSparseGeometryPoint3 truth_point = {0.2, -0.1, 4.0};
for (size_t view = 0; view < view_count; ++view) {
poses[view] = pose_a;
poses[view].translation_cw[0] = static_cast<double>(view) * 0.5;
observations[view] = project(calibration, truth_point, poses[view]);
CHECK(lardon3d_sparse_geometry_normalize(
&calibration, &observations[view], 1, &observations[view]) ==
LARDON3D_SPARSE_GEOMETRY_OK);
}
Lardon3DSparseGeometryPoint3 multi;
CHECK(lardon3d_sparse_geometry_triangulate_multi_view(
observations, poses, view_count, &multi) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(multi.z - truth_point.z) < 1e-7);
}
Lardon3DSparseGeometryPoint2 planar_a[count];
Lardon3DSparseGeometryPoint2 planar_b[count];
Lardon3DSparseGeometryPoint2 far_a[count];
Lardon3DSparseGeometryPoint2 far_b[count];
Lardon3DSparseGeometryPose far_pose = pose_b;
for (size_t index = 0; index < count; ++index) {
Lardon3DSparseGeometryPoint3 planar_point = {
-1.0 + static_cast<double>(index % 8) * 0.28,
-0.7 + static_cast<double>(index / 8) * 0.18, 4.0};
Lardon3DSparseGeometryPoint3 far_point = {planar_point.x, planar_point.y,
10000.0};
planar_a[index] = project(calibration, planar_point, pose_a);
planar_b[index] = project(calibration, planar_point, pose_b);
far_a[index] = project(calibration, far_point, pose_a);
far_b[index] = project(calibration, far_point, far_pose);
}
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, planar_a, planar_b, count, &parameters,
&result) != LARDON3D_SPARSE_GEOMETRY_INVALID_ARGUMENT);
parameters.minimum_parallax_rad = 1e-4;
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, far_a, far_b, count, &parameters,
&result) != LARDON3D_SPARSE_GEOMETRY_OK);
std::printf("pnp_worst_rotation=%.17g pnp_worst_translation=%.17g "
"pnp_worst_precision=%.17g pnp_worst_recall=%.17g\n",
pnp_worst_rotation_error, pnp_worst_translation_error,
pnp_worst_precision, pnp_worst_recall);
return 0;
}
static int multiview_matrix_test() {
const Lardon3DSparseGeometryCalibration calibration =
{1280, 960, 800, 800, 640, 480, 0, 0, 0, 0};
const Lardon3DSparseGeometryPoint3 truth = {0.35, -0.2, 4.0};
const size_t view_counts[] = {2, 3, 5, 10};
double worst_error = 0.0;
double worst_mean_reprojection = 0.0;
double worst_max_reprojection = 0.0;
double lowest_accepted_parallax = std::numeric_limits<double>::max();
double highest_rejected_parallax = 0.0;
for (size_t view_count : view_counts) {
const double noise_levels[] = {0.0, 0.25, 0.75, 1.5};
for (size_t noise_index = 0; noise_index < 4; ++noise_index) {
Lardon3DSparseGeometryPoint2 observations[10];
Lardon3DSparseGeometryPose poses[10];
for (size_t view = 0; view < view_count; ++view) {
poses[view] = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{static_cast<double>(view) * 0.5,
static_cast<double>(view % 2) * 0.2, 0}};
observations[view] = project(calibration, truth, poses[view]);
observations[view].x +=
matrix_noise(view + noise_index * 17, noise_levels[noise_index]);
observations[view].y +=
matrix_noise(view + noise_index * 23, noise_levels[noise_index]);
CHECK(lardon3d_sparse_geometry_normalize(
&calibration, &observations[view], 1, &observations[view]) ==
LARDON3D_SPARSE_GEOMETRY_OK);
}
Lardon3DSparseGeometryPoint3 output;
Lardon3DSparseGeometryResult status =
lardon3d_sparse_geometry_triangulate_multi_view(
observations, poses, view_count, &output);
CHECK(status == LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::isfinite(output.x) && std::isfinite(output.y) &&
std::isfinite(output.z));
double error = std::sqrt((output.x - truth.x) * (output.x - truth.x) +
(output.y - truth.y) * (output.y - truth.y) +
(output.z - truth.z) * (output.z - truth.z));
double mean_reprojection = 0.0;
double max_reprojection = 0.0;
for (size_t view = 0; view < view_count; ++view) {
Lardon3DSparseGeometryPoint3 camera_point = output;
camera_point.x += poses[view].translation_cw[0];
camera_point.y += poses[view].translation_cw[1];
camera_point.z += poses[view].translation_cw[2];
double dx = camera_point.x / camera_point.z - observations[view].x;
double dy = camera_point.y / camera_point.z - observations[view].y;
double reprojection = std::hypot(dx, dy);
mean_reprojection += reprojection;
max_reprojection = std::max(max_reprojection, reprojection);
}
mean_reprojection /= static_cast<double>(view_count);
worst_error = std::max(worst_error, error);
worst_mean_reprojection =
std::max(worst_mean_reprojection, mean_reprojection);
worst_max_reprojection = std::max(worst_max_reprojection, max_reprojection);
Lardon3DSparseGeometryPoint3 repeated;
CHECK(lardon3d_sparse_geometry_triangulate_multi_view(
observations, poses, view_count, &repeated) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::memcmp(&output, &repeated, sizeof(output)) == 0);
}
const double boundary_baselines[] = {0.001, 0.00045, 0.00035, 0.00005};
for (double baseline : boundary_baselines) {
Lardon3DSparseGeometryPoint2 observations[10];
Lardon3DSparseGeometryPose poses[10];
for (size_t view = 0; view < view_count; ++view) {
poses[view] = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{static_cast<double>(view) * baseline, 0, 0}};
observations[view] = project(calibration, truth, poses[view]);
CHECK(lardon3d_sparse_geometry_normalize(
&calibration, &observations[view], 1, &observations[view]) ==
LARDON3D_SPARSE_GEOMETRY_OK);
}
Lardon3DSparseGeometryPoint3 output;
Lardon3DSparseGeometryResult status =
lardon3d_sparse_geometry_triangulate_multi_view(
observations, poses, view_count, &output);
double effective_parallax = baseline / truth.z;
if (effective_parallax >= 1e-4) {
CHECK(status == LARDON3D_SPARSE_GEOMETRY_OK);
lowest_accepted_parallax =
std::min(lowest_accepted_parallax, effective_parallax);
} else {
CHECK(status == LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX);
highest_rejected_parallax =
std::max(highest_rejected_parallax, effective_parallax);
}
}
Lardon3DSparseGeometryPoint2 observations[10];
Lardon3DSparseGeometryPose poses[10];
Lardon3DSparseGeometryPoint3 far_truth = {0.35, -0.2, 10000.0};
for (size_t view = 0; view < view_count; ++view) {
poses[view] = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{static_cast<double>(view) * 2.0, 0, 0}};
observations[view] = project(calibration, far_truth, poses[view]);
CHECK(lardon3d_sparse_geometry_normalize(
&calibration, &observations[view], 1, &observations[view]) ==
LARDON3D_SPARSE_GEOMETRY_OK);
}
Lardon3DSparseGeometryPoint3 output;
CHECK(lardon3d_sparse_geometry_triangulate_multi_view(
observations, poses, view_count, &output) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::isfinite(output.z));
}
std::printf("multi_view_worst_error=%.17g mean_reprojection=%.17g "
"max_reprojection=%.17g lowest_parallax=%.17g "
"highest_rejected_parallax=%.17g\n",
worst_error, worst_mean_reprojection, worst_max_reprojection,
lowest_accepted_parallax, highest_rejected_parallax);
return 0;
}
static int resource_test() {
Lardon3DSparseGeometryCalibration calibration =
{1280, 960, 800, 800, 640, 480, 0, 0, 0, 0};
Lardon3DSparseGeometryPose pose_a = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{0, 0, 0}};
Lardon3DSparseGeometryPose pose_b = pose_a;
pose_b.translation_cw[0] = 1.0;
const size_t relative_count = 8192;
std::vector<Lardon3DSparseGeometryPoint2> pixels_a(relative_count);
std::vector<Lardon3DSparseGeometryPoint2> pixels_b(relative_count);
for (size_t index = 0; index < relative_count; ++index) {
Lardon3DSparseGeometryPoint3 point = {
-1.0 + static_cast<double>(index % 128) * 0.015,
-0.8 + static_cast<double>((index / 128) % 64) * 0.025,
4.0 + static_cast<double>(index % 17) * 0.1};
pixels_a[index] = project(calibration, point, pose_a);
pixels_b[index] = project(calibration, point, pose_b);
}
std::vector<uint8_t> mask(relative_count);
Lardon3DSparseGeometryRelativePoseParameters relative_parameters =
{1.0, 0.999, 1000, 32, 0.1, 1e-5, 0.5, 99};
Lardon3DSparseGeometryRelativePoseResult relative = {};
relative.inlier_mask = mask.data();
relative.inlier_mask_capacity = mask.size();
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a.data(), pixels_b.data(),
relative_count, &relative_parameters, &relative) ==
LARDON3D_SPARSE_GEOMETRY_OK);
const size_t pnp_count = 2000;
std::vector<Lardon3DSparseGeometryPoint3> points(pnp_count);
std::vector<Lardon3DSparseGeometryPoint2> pnp_pixels(pnp_count);
for (size_t index = 0; index < pnp_count; ++index) {
points[index] = {-1.0 + static_cast<double>(index % 100) * 0.02,
-0.8 + static_cast<double>((index / 100) % 20) * 0.03,
4.0 + static_cast<double>(index % 13) * 0.1};
pnp_pixels[index] = project(calibration, points[index], pose_b);
}
std::vector<uint8_t> pnp_mask(pnp_count);
Lardon3DSparseGeometryPnPParameters pnp_parameters =
{1.0, 0.999, 1000, 32, 0.1, 99};
Lardon3DSparseGeometryPnPResult pnp = {};
pnp.inlier_mask = pnp_mask.data();
pnp.inlier_mask_capacity = pnp_mask.size();
CHECK(lardon3d_sparse_geometry_pnp(
&calibration, points.data(), pnp_pixels.data(), pnp_count,
&pnp_parameters, &pnp) == LARDON3D_SPARSE_GEOMETRY_OK);
Lardon3DSparseGeometryPoint2 normalized_a;
Lardon3DSparseGeometryPoint2 normalized_b;
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &pixels_a[0], 1,
&normalized_a) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &pixels_b[0], 1,
&normalized_b) ==
LARDON3D_SPARSE_GEOMETRY_OK);
Lardon3DSparseGeometryPoint3 output;
for (size_t index = 0; index < 100000; ++index)
CHECK(lardon3d_sparse_geometry_triangulate_two_view(
&normalized_a, &normalized_b, &pose_a, &pose_b, &output) ==
LARDON3D_SPARSE_GEOMETRY_OK);
std::printf("relative=%zu inliers=%u pose=%.17g,%.17g,%.17g pnp=%zu "
"pnp_inliers=%u pnp_pose=%.17g,%.17g,%.17g triangulations=%d\n",
relative_count, relative.inlier_count,
relative.pose_ba.translation_cw[0],
relative.pose_ba.translation_cw[1],
relative.pose_ba.translation_cw[2], pnp_count,
pnp.inlier_count, pnp.pose_cw.translation_cw[0],
pnp.pose_cw.translation_cw[1], pnp.pose_cw.translation_cw[2],
100000);
return 0;
}
int main(int argc, char **argv) {
if (argc > 1)
if (std::strcmp(argv[1], "matrix") == 0)
return matrix_test();
if (argc > 1)
if (std::strcmp(argv[1], "multiview-matrix") == 0)
return multiview_matrix_test();
if (argc > 1)
return resource_test();
Lardon3DSparseGeometryCalibration calibration =
{1280, 960, 800, 800, 640, 480, 0, 0, 0, 0};
Lardon3DSparseGeometryPoint2 pixel = {720, 520};
Lardon3DSparseGeometryPoint2 normalized;
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &pixel, 1,
&normalized) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(normalized.x - 0.1) < 1e-12);
CHECK(std::abs(normalized.y - 0.05) < 1e-12);
Lardon3DSparseGeometryCalibration distorted_calibration = calibration;
distorted_calibration.k1 = 0.08;
distorted_calibration.k2 = -0.01;
distorted_calibration.p1 = 0.001;
distorted_calibration.p2 = -0.002;
const double ideal_x = 0.2;
const double ideal_y = -0.15;
const double radius_squared = ideal_x * ideal_x + ideal_y * ideal_y;
const double radial = 1.0 + distorted_calibration.k1 * radius_squared +
distorted_calibration.k2 * radius_squared * radius_squared;
const double distorted_x = ideal_x * radial +
2.0 * distorted_calibration.p1 * ideal_x * ideal_y +
distorted_calibration.p2 *
(radius_squared + 2.0 * ideal_x * ideal_x);
const double distorted_y = ideal_y * radial +
distorted_calibration.p1 *
(radius_squared + 2.0 * ideal_y * ideal_y) +
2.0 * distorted_calibration.p2 * ideal_x * ideal_y;
Lardon3DSparseGeometryPoint2 distorted_pixel = {
distorted_calibration.fx * distorted_x + distorted_calibration.cx,
distorted_calibration.fy * distorted_y + distorted_calibration.cy};
CHECK(lardon3d_sparse_geometry_normalize(
&distorted_calibration, &distorted_pixel, 1, &normalized) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(normalized.x - ideal_x) < 1e-8);
CHECK(std::abs(normalized.y - ideal_y) < 1e-8);
Lardon3DSparseGeometryPose pose_a = {{1, 0, 0, 0, 1, 0, 0, 0, 1},
{0, 0, 0}};
Lardon3DSparseGeometryPose pose_b = pose_a;
pose_b.translation_cw[0] = 1.0;
Lardon3DSparseGeometryPoint3 truth = {0.2, -0.1, 4.0};
Lardon3DSparseGeometryPoint2 pixels_a[8];
Lardon3DSparseGeometryPoint2 pixels_b[8];
for (size_t index = 0; index < 8; ++index) {
Lardon3DSparseGeometryPoint3 point = {
truth.x + static_cast<double>(index) * 0.17,
truth.y + static_cast<double>(index % 3) * 0.13,
truth.z + static_cast<double>(index) * 0.21};
pixels_a[index] = project(calibration, point, pose_a);
pixels_b[index] = project(calibration, point, pose_b);
}
uint8_t mask[8] = {0};
Lardon3DSparseGeometryRelativePoseParameters relative_parameters =
{1.0, 0.999, 1000, 6, 0.75, 1e-4, 0.5, 1234};
Lardon3DSparseGeometryRelativePoseResult relative = {};
relative.inlier_mask = mask;
relative.inlier_mask_capacity = 8;
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, pixels_b, 8,
&relative_parameters, &relative) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(relative.inlier_count >= 6);
CHECK(relative.median_parallax_rad > 1e-4);
Lardon3DSparseGeometryRelativePoseResult repeated = {};
repeated.inlier_mask = mask;
repeated.inlier_mask_capacity = 8;
for (int run = 0; run < 20; ++run) {
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, pixels_b, 8,
&relative_parameters, &repeated) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(repeated.inlier_count == relative.inlier_count);
CHECK(std::memcmp(mask, relative.inlier_mask, sizeof(mask)) == 0);
}
Lardon3DSparseGeometryPoint2 nonfinite_pixel = {NAN, 0};
CHECK(lardon3d_sparse_geometry_normalize(
&calibration, &nonfinite_pixel, 1, &normalized) ==
LARDON3D_SPARSE_GEOMETRY_NONFINITE_INPUT);
Lardon3DSparseGeometryPoint2 pure_pixels_b[8];
for (size_t index = 0; index < 8; ++index)
pure_pixels_b[index] = pixels_a[index];
CHECK(lardon3d_sparse_geometry_relative_pose(
&calibration, &calibration, pixels_a, pure_pixels_b, 8,
&relative_parameters, &repeated) !=
LARDON3D_SPARSE_GEOMETRY_OK);
Lardon3DSparseGeometryPoint2 normalized_a;
Lardon3DSparseGeometryPoint2 normalized_b;
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &pixels_a[0], 1,
&normalized_a) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(lardon3d_sparse_geometry_normalize(&calibration, &pixels_b[0], 1,
&normalized_b) ==
LARDON3D_SPARSE_GEOMETRY_OK);
Lardon3DSparseGeometryPoint3 triangulated;
CHECK(lardon3d_sparse_geometry_triangulate_two_view(
&normalized_a, &normalized_b, &pose_a, &pose_b, &triangulated) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(triangulated.x - truth.x) < 1e-8);
CHECK(std::abs(triangulated.y - truth.y) < 1e-8);
CHECK(std::abs(triangulated.z - truth.z) < 1e-8);
Lardon3DSparseGeometryPoint2 multi_points[3] = {
normalized_a, normalized_b, normalized_b};
Lardon3DSparseGeometryPose multi_poses[3] = {pose_a, pose_b, pose_b};
Lardon3DSparseGeometryPoint3 multi_point;
CHECK(lardon3d_sparse_geometry_triangulate_multi_view(
multi_points, multi_poses, 3, &multi_point) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(multi_point.z - truth.z) < 1e-8);
Lardon3DSparseGeometryPointRefinementParameters refinement_parameters =
{30, 1e-12};
Lardon3DSparseGeometryPoint3 initial = {truth.x + 0.1, truth.y - 0.1,
truth.z + 0.2};
Lardon3DSparseGeometryPoint3 refined;
CHECK(lardon3d_sparse_geometry_refine_point(
multi_points, multi_poses, 3, &initial, &refinement_parameters,
&refined) == LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(std::abs(refined.x - truth.x) < 1e-7);
CHECK(std::abs(refined.y - truth.y) < 1e-7);
CHECK(std::abs(refined.z - truth.z) < 1e-7);
Lardon3DSparseGeometryPnPParameters pnp_parameters =
{1.0, 0.999, 1000, 6, 0.75, 1234};
Lardon3DSparseGeometryPoint3 points[8];
for (size_t index = 0; index < 8; ++index)
points[index] = {truth.x + static_cast<double>(index) * 0.17,
truth.y + static_cast<double>(index % 3) * 0.13,
truth.z + static_cast<double>(index) * 0.21};
Lardon3DSparseGeometryPnPResult pnp = {};
pnp.inlier_mask = mask;
pnp.inlier_mask_capacity = 8;
CHECK(lardon3d_sparse_geometry_pnp(&calibration, points, pixels_b, 8,
&pnp_parameters, &pnp) ==
LARDON3D_SPARSE_GEOMETRY_OK);
CHECK(pnp.inlier_count >= 6);
return 0;
}