1364 lines
62 KiB
C++
1364 lines
62 KiB
C++
#include <lardon3d/sparse_sfm_incremental.h>
|
|
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
#define CHECK(value) \
|
|
do { \
|
|
if (!(value)) { \
|
|
std::fprintf(stderr, "incremental check failed at line %d: %s\n", \
|
|
__LINE__, #value); \
|
|
return false; \
|
|
} \
|
|
} while (0)
|
|
|
|
static Lardon3DSparseGeometryPoint2 project(
|
|
const Lardon3DSparseGeometryCalibration &calibration,
|
|
const Lardon3DSparseGeometryPose &pose,
|
|
const Lardon3DSparseGeometryPoint3 &point) {
|
|
const double x = pose.rotation_cw[0] * point.x + pose.rotation_cw[1] * point.y +
|
|
pose.rotation_cw[2] * point.z + pose.translation_cw[0];
|
|
const double y = pose.rotation_cw[3] * point.x + pose.rotation_cw[4] * point.y +
|
|
pose.rotation_cw[5] * point.z + pose.translation_cw[1];
|
|
const double z = pose.rotation_cw[6] * point.x + pose.rotation_cw[7] * point.y +
|
|
pose.rotation_cw[8] * point.z + pose.translation_cw[2];
|
|
return {calibration.fx * x / z + calibration.cx,
|
|
calibration.fy * y / z + calibration.cy};
|
|
}
|
|
|
|
static bool run_test() {
|
|
Lardon3DSparseIncrementalParameters parameters;
|
|
CHECK(lardon3d_sparse_incremental_parameters_default(¶meters));
|
|
parameters.minimum_seed_tracks = 6;
|
|
parameters.minimum_seed_landmarks = 6;
|
|
parameters.minimum_pnp_correspondences = 6;
|
|
parameters.maximum_registration_rounds = 8;
|
|
|
|
const Lardon3DSparseGeometryCalibration calibration = {
|
|
4000, 3000, 2000.0, 2000.0, 2000.0, 1500.0, 0.0, 0.0, 0.0, 0.0};
|
|
const Lardon3DSparseGeometryPose poses[3] = {
|
|
{{1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 0, 0}},
|
|
{{1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 0, 0}},
|
|
{{1, 0, 0, 0, 1, 0, 0, 0, 1}, {2, 0, 0}},
|
|
};
|
|
Lardon3DSparseIncrementalImage images[3] = {
|
|
{10, calibration}, {20, calibration}, {30, calibration}};
|
|
std::vector<Lardon3DSparseIncrementalObservation> observations;
|
|
for (uint64_t track = 1; track <= 12; ++track) {
|
|
Lardon3DSparseGeometryPoint3 point = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5,
|
|
-0.8 + static_cast<double>(track / 4) * 0.4,
|
|
5.0 + static_cast<double>(track % 3) * 0.25};
|
|
for (size_t camera = 0; camera < 3; ++camera) {
|
|
Lardon3DSparseGeometryPoint2 pixel = project(calibration, poses[camera], point);
|
|
observations.push_back({track, images[camera].image_id, 100 + camera,
|
|
static_cast<uint32_t>(track), 64, pixel.x, pixel.y});
|
|
}
|
|
}
|
|
Lardon3DSparseIncrementalInput input = {
|
|
77, 88, images, 3, observations.data(), observations.size()};
|
|
Lardon3DSparseIncrementalResult result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&input, ¶meters, &result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(result.component_count == 1);
|
|
CHECK(result.camera_count == 3);
|
|
CHECK(result.landmark_count >= 6);
|
|
CHECK(result.unregistered_image_count == 0);
|
|
CHECK(result.components[0].component_key == 10);
|
|
for (size_t index = 0; index < result.camera_count; ++index)
|
|
CHECK(result.cameras[index].component_key == 10);
|
|
for (size_t index = 1; index < result.camera_count; ++index)
|
|
CHECK(result.cameras[index - 1].image_id < result.cameras[index].image_id);
|
|
for (size_t index = 1; index < result.landmark_count; ++index)
|
|
CHECK(result.landmarks[index - 1].track_id < result.landmarks[index].track_id);
|
|
|
|
Lardon3DSparseIncrementalResult repeated = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&input, ¶meters, &repeated) ==
|
|
result.status);
|
|
CHECK(repeated.camera_count == result.camera_count);
|
|
CHECK(repeated.landmark_count == result.landmark_count);
|
|
for (size_t index = 0; index < result.camera_count; ++index) {
|
|
CHECK(repeated.cameras[index].image_id == result.cameras[index].image_id);
|
|
for (size_t value = 0; value < 9; ++value)
|
|
CHECK(repeated.cameras[index].pose_cw.rotation_cw[value] ==
|
|
result.cameras[index].pose_cw.rotation_cw[value]);
|
|
}
|
|
lardon3d_sparse_incremental_result_destroy(&repeated);
|
|
|
|
Lardon3DSparseIncrementalInput disconnected = input;
|
|
Lardon3DSparseIncrementalImage singleton = {99, calibration};
|
|
std::vector<Lardon3DSparseIncrementalImage> more_images = {images[0], images[1],
|
|
images[2], singleton};
|
|
disconnected.images = more_images.data();
|
|
disconnected.image_count = more_images.size();
|
|
Lardon3DSparseIncrementalResult partial = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&disconnected, ¶meters, &partial) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(partial.unregistered_image_count == 1);
|
|
CHECK(partial.unregistered_images[0].image_id == 99);
|
|
lardon3d_sparse_incremental_result_destroy(&partial);
|
|
|
|
Lardon3DSparseIncrementalResult invalid = {};
|
|
Lardon3DSparseIncrementalInput invalid_input = input;
|
|
invalid_input.observations = nullptr;
|
|
CHECK(lardon3d_sparse_incremental_run(&invalid_input, ¶meters, &invalid) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
CHECK(invalid.camera_count == 0 && invalid.landmark_count == 0);
|
|
|
|
auto expect_invalid = [&](const Lardon3DSparseIncrementalInput &candidate,
|
|
const Lardon3DSparseIncrementalParameters &limits) {
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
const auto status = lardon3d_sparse_incremental_run(
|
|
&candidate, &limits, &output);
|
|
const bool valid = status == LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT &&
|
|
output.camera_count == 0 && output.landmark_count == 0;
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
return valid;
|
|
};
|
|
Lardon3DSparseIncrementalInput malformed = input;
|
|
malformed.images = nullptr;
|
|
CHECK(expect_invalid(malformed, parameters));
|
|
malformed = input;
|
|
malformed.observations = observations.data();
|
|
std::vector<Lardon3DSparseIncrementalObservation> bad_observations = observations;
|
|
bad_observations[0].feature_count = 1;
|
|
malformed.observations = bad_observations.data();
|
|
CHECK(expect_invalid(malformed, parameters));
|
|
bad_observations = observations;
|
|
bad_observations[0].x = NAN;
|
|
malformed.observations = bad_observations.data();
|
|
CHECK(expect_invalid(malformed, parameters));
|
|
bad_observations = observations;
|
|
bad_observations.back().track_id = bad_observations.front().track_id;
|
|
bad_observations.back().image_id = bad_observations.front().image_id;
|
|
malformed.observations = bad_observations.data();
|
|
CHECK(expect_invalid(malformed, parameters));
|
|
Lardon3DSparseIncrementalParameters bounded = parameters;
|
|
bounded.maximum_images = 2;
|
|
CHECK(expect_invalid(input, bounded));
|
|
bounded = parameters;
|
|
bounded.maximum_observations = observations.size() - 1;
|
|
CHECK(expect_invalid(input, bounded));
|
|
bounded = parameters;
|
|
bounded.maximum_tracks = 1;
|
|
Lardon3DSparseIncrementalResult bounded_result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&input, &bounded, &bounded_result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
lardon3d_sparse_incremental_result_destroy(&bounded_result);
|
|
Lardon3DSparseIncrementalParameters invalid_limits = parameters;
|
|
invalid_limits.maximum_registration_rounds = 0;
|
|
CHECK(expect_invalid(input, invalid_limits));
|
|
|
|
std::vector<Lardon3DSparseIncrementalObservation> flat = observations;
|
|
for (auto &observation : flat) {
|
|
observation.x = 2100.0;
|
|
observation.y = 1500.0;
|
|
}
|
|
malformed = input;
|
|
malformed.observations = flat.data();
|
|
Lardon3DSparseIncrementalResult exhausted = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&malformed, ¶meters, &exhausted) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(exhausted.seed_candidates_considered > 0);
|
|
lardon3d_sparse_incremental_result_destroy(&exhausted);
|
|
|
|
lardon3d_sparse_incremental_result_destroy(&result);
|
|
return true;
|
|
}
|
|
|
|
struct BatchFixture {
|
|
Lardon3DSparseGeometryCalibration calibration;
|
|
std::vector<Lardon3DSparseIncrementalImage> images;
|
|
std::vector<Lardon3DSparseIncrementalObservation> observations;
|
|
};
|
|
|
|
static BatchFixture make_batch_fixture(size_t camera_count, size_t track_count,
|
|
bool reject_first_seed, bool noisy,
|
|
bool outliers, bool fail_pnp) {
|
|
BatchFixture fixture;
|
|
fixture.calibration = {4000, 3000, 2000.0, 2000.0, 2000.0, 1500.0,
|
|
0.0, 0.0, 0.0, 0.0};
|
|
for (size_t camera = 0; camera < camera_count; ++camera)
|
|
fixture.images.push_back({10 + camera, fixture.calibration});
|
|
for (size_t track = 0; track < track_count; ++track) {
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5,
|
|
-0.8 + static_cast<double>(track / 4) * 0.4,
|
|
5.0 + static_cast<double>(track % 3) * 0.25};
|
|
for (size_t camera = 0; camera < camera_count; ++camera) {
|
|
double translation = static_cast<double>(camera);
|
|
if (reject_first_seed && camera == 1) translation = 1e-3;
|
|
const Lardon3DSparseGeometryPose pose = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {translation, 0, 0}};
|
|
Lardon3DSparseGeometryPoint2 pixel =
|
|
project(fixture.calibration, pose, point);
|
|
if (noisy && camera == 2) {
|
|
pixel.x += static_cast<double>((track * 17) % 5) * 0.12 - 0.24;
|
|
pixel.y += static_cast<double>((track * 11) % 5) * 0.10 - 0.20;
|
|
}
|
|
if (outliers && camera == 2 && track < 3) {
|
|
pixel.x += 900.0 + static_cast<double>(track) * 30.0;
|
|
pixel.y -= 700.0;
|
|
}
|
|
if (fail_pnp && camera == 2) {
|
|
pixel.x = 300.0 + static_cast<double>((track * 613) % 3300);
|
|
pixel.y = 200.0 + static_cast<double>((track * 977) % 2300);
|
|
}
|
|
fixture.observations.push_back(
|
|
{track + 1, 10 + camera, 100 + camera,
|
|
static_cast<uint32_t>(track),
|
|
static_cast<uint32_t>(track_count + 1), pixel.x, pixel.y});
|
|
}
|
|
}
|
|
return fixture;
|
|
}
|
|
|
|
static BatchFixture make_growth_fixture(int kind) {
|
|
BatchFixture fixture = make_batch_fixture(3, 12, false, false, false, false);
|
|
fixture.images[2].image_id = 13;
|
|
for (auto &observation : fixture.observations)
|
|
if (observation.image_id == 12) observation.image_id = 13;
|
|
fixture.observations.erase(
|
|
std::remove_if(fixture.observations.begin(), fixture.observations.end(),
|
|
[](const auto &observation) {
|
|
return observation.image_id == 13 && observation.track_id > 6;
|
|
}),
|
|
fixture.observations.end());
|
|
const Lardon3DSparseGeometryPose growth_pose = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1},
|
|
{2, 0, kind == 19 ? -4.5 : 0}};
|
|
if (kind == 19) {
|
|
for (auto &observation : fixture.observations) {
|
|
if (observation.image_id != 13) continue;
|
|
const size_t track = static_cast<size_t>(observation.track_id - 1);
|
|
const Lardon3DSparseGeometryPoint3 support = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5,
|
|
-0.8 + static_cast<double>(track / 4) * 0.4,
|
|
5.0 + static_cast<double>(track % 3) * 0.25};
|
|
const auto pixel = project(fixture.calibration, growth_pose, support);
|
|
observation.x = pixel.x;
|
|
observation.y = pixel.y;
|
|
}
|
|
}
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
0.25, 0.15, kind == 19 ? 4.0 : 5.5};
|
|
const Lardon3DSparseGeometryPose pose_a = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 0, 0}};
|
|
const auto pixel_a = project(fixture.calibration, pose_a, point);
|
|
auto pixel_b = project(fixture.calibration, growth_pose, point);
|
|
if (kind == 21) pixel_b = pixel_a;
|
|
if (kind == 19 || kind == 20 || kind == 21 || kind == 25) {
|
|
const Lardon3DSparseGeometryPose pose_mid = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 0, 0}};
|
|
const auto pixel_mid = project(fixture.calibration, pose_mid, point);
|
|
fixture.observations.push_back({
|
|
13, 11, 101, 12, 64,
|
|
kind == 21 ? pixel_a.x
|
|
: kind == 20 ? pixel_mid.x + 700.0 : pixel_mid.x,
|
|
kind == 21 ? pixel_a.y
|
|
: kind == 20 ? pixel_mid.y - 500.0 : pixel_mid.y});
|
|
}
|
|
fixture.observations.push_back({13, 10, 100, 12, 64, pixel_a.x, pixel_a.y});
|
|
fixture.observations.push_back({13, 13, 102, 12, 64, pixel_b.x, pixel_b.y});
|
|
return fixture;
|
|
}
|
|
|
|
static bool run_batch_cases() {
|
|
Lardon3DSparseIncrementalParameters parameters;
|
|
CHECK(lardon3d_sparse_incremental_parameters_default(¶meters));
|
|
parameters.minimum_seed_tracks = 6;
|
|
parameters.minimum_seed_landmarks = 6;
|
|
parameters.minimum_pnp_correspondences = 6;
|
|
parameters.maximum_registration_rounds = 8;
|
|
|
|
/* CASE 01: minimal valid two-view reconstruction. */
|
|
{
|
|
const BatchFixture minimal =
|
|
make_batch_fixture(2, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
301, 302, minimal.images.data(), minimal.images.size(),
|
|
minimal.observations.data(), minimal.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.camera_count == 2);
|
|
CHECK(output.landmark_count >= parameters.minimum_seed_landmarks);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 02: deterministic seed selection. */
|
|
{
|
|
const BatchFixture deterministic =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
303, 304, deterministic.images.data(), deterministic.images.size(),
|
|
deterministic.observations.data(), deterministic.observations.size()};
|
|
Lardon3DSparseIncrementalResult first = {}, second = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &first) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &second) ==
|
|
first.status);
|
|
CHECK(first.seed_image_a == 10 && first.seed_image_b == 11);
|
|
CHECK(second.seed_image_a == first.seed_image_a);
|
|
CHECK(second.seed_image_b == first.seed_image_b);
|
|
CHECK(std::memcmp(first.cameras, second.cameras,
|
|
first.camera_count * sizeof(*first.cameras)) == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&second);
|
|
lardon3d_sparse_incremental_result_destroy(&first);
|
|
}
|
|
|
|
/* CASE 03: multiple eligible candidates use canonical ordering. */
|
|
{
|
|
const BatchFixture multiple =
|
|
make_batch_fixture(4, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
305, 306, multiple.images.data(), multiple.images.size(),
|
|
multiple.observations.data(), multiple.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.seed_candidates_available == 6);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.seed_image_a == 10 && output.seed_image_b == 11);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 04: first seed rejected, later seed accepted. */
|
|
const BatchFixture fixture = make_batch_fixture(4, 12, true, false, false, false);
|
|
const Lardon3DSparseIncrementalInput input = {
|
|
401, 402, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&input, ¶meters, &result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(result.seed_candidates_considered >= 2);
|
|
CHECK(result.seed_image_a == 10 && result.seed_image_b == 12);
|
|
CHECK(result.camera_count == 4);
|
|
lardon3d_sparse_incremental_result_destroy(&result);
|
|
|
|
/* CASE 05: equal-support cameras are added by image ID, one per round. */
|
|
{
|
|
const BatchFixture ordered =
|
|
make_batch_fixture(5, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters one_round = parameters;
|
|
one_round.maximum_registration_rounds = 1;
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
307, 308, ordered.images.data(), ordered.images.size(),
|
|
ordered.observations.data(), ordered.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, &one_round, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.registration_rounds == 1);
|
|
CHECK(output.registration_successes == 1);
|
|
CHECK(output.camera_count == 3);
|
|
CHECK(output.cameras[2].image_id == 12);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 06: clean calibrated PnP registers one camera. */
|
|
{
|
|
const BatchFixture clean =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
309, 310, clean.images.data(), clean.images.size(),
|
|
clean.observations.data(), clean.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.registration_attempts == 1);
|
|
CHECK(output.registration_successes == 1);
|
|
CHECK(output.last_pnp_inlier_count == 12);
|
|
CHECK(output.camera_count == 3);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 07: deterministic noisy PnP registration. */
|
|
{
|
|
const BatchFixture noisy = make_batch_fixture(3, 12, false, true, false, false);
|
|
const Lardon3DSparseIncrementalInput noisy_input = {
|
|
403, 404, noisy.images.data(), noisy.images.size(),
|
|
noisy.observations.data(), noisy.observations.size()};
|
|
Lardon3DSparseIncrementalResult noisy_result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&noisy_input, ¶meters,
|
|
&noisy_result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(noisy_result.registration_attempts >= 1);
|
|
CHECK(noisy_result.registration_successes >= 1);
|
|
CHECK(noisy_result.last_pnp_inlier_count >=
|
|
parameters.minimum_pnp_correspondences);
|
|
CHECK(noisy_result.camera_count == 3);
|
|
for (size_t index = 0; index < noisy_result.camera_count; ++index)
|
|
for (double value : noisy_result.cameras[index].pose_cw.rotation_cw)
|
|
CHECK(std::isfinite(value));
|
|
lardon3d_sparse_incremental_result_destroy(&noisy_result);
|
|
}
|
|
|
|
/* CASE 08: deterministic PnP outliers. */
|
|
{
|
|
const BatchFixture outliers = make_batch_fixture(3, 12, false, false, true, false);
|
|
const Lardon3DSparseIncrementalInput outlier_input = {
|
|
405, 406, outliers.images.data(), outliers.images.size(),
|
|
outliers.observations.data(), outliers.observations.size()};
|
|
Lardon3DSparseIncrementalResult outlier_result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&outlier_input, ¶meters,
|
|
&outlier_result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(outlier_result.registration_attempts >= 1);
|
|
CHECK(outlier_result.registration_successes == 1);
|
|
CHECK(outlier_result.last_pnp_inlier_count >= 9);
|
|
CHECK(outlier_result.camera_count == 3);
|
|
lardon3d_sparse_incremental_result_destroy(&outlier_result);
|
|
}
|
|
|
|
/* CASE 09: PnP-stage registration failure. */
|
|
{
|
|
const BatchFixture failed = make_batch_fixture(3, 12, false, false, false, true);
|
|
const Lardon3DSparseIncrementalInput failed_input = {
|
|
407, 408, failed.images.data(), failed.images.size(),
|
|
failed.observations.data(), failed.observations.size()};
|
|
Lardon3DSparseIncrementalResult failed_result = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&failed_input, ¶meters,
|
|
&failed_result) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(failed_result.registration_attempts >= 1);
|
|
CHECK(failed_result.registration_failures >= 1);
|
|
CHECK(failed_result.registration_successes == 0);
|
|
CHECK(failed_result.camera_count == 2);
|
|
CHECK(failed_result.unregistered_image_count == 1);
|
|
CHECK(failed_result.unregistered_images[0].image_id == 12);
|
|
lardon3d_sparse_incremental_result_destroy(&failed_result);
|
|
}
|
|
|
|
/* CASE 10: insufficient PnP support skips the solver. */
|
|
{
|
|
const BatchFixture fixture = make_batch_fixture(3, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters insufficient = parameters;
|
|
insufficient.minimum_pnp_correspondences = 20;
|
|
insufficient.pnp.minimum_inliers = 20;
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
409, 410, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, &insufficient, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.camera_count == 2);
|
|
CHECK(output.unregistered_image_count == 1);
|
|
CHECK(output.registration_attempts == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 11: valid low-parallax seed reaches Gate C and is rejected. */
|
|
{
|
|
const BatchFixture fixture = make_batch_fixture(2, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters low_parallax = parameters;
|
|
low_parallax.relative_pose.minimum_parallax_rad = 1.0;
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
411, 412, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, &low_parallax, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.last_seed_geometry_status == LARDON3D_SPARSE_GEOMETRY_LOW_PARALLAX ||
|
|
output.last_seed_geometry_status == LARDON3D_SPARSE_GEOMETRY_DEGENERATE);
|
|
CHECK(output.camera_count == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 12: pure rotation reaches relative pose handling and rejects. */
|
|
{
|
|
BatchFixture fixture = make_batch_fixture(2, 12, true, false, false, false);
|
|
const double c = std::cos(0.25), s = std::sin(0.25);
|
|
for (size_t track = 0; track < 12; ++track) {
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5,
|
|
-0.8 + static_cast<double>(track / 4) * 0.4,
|
|
5.0 + static_cast<double>(track % 3) * 0.25};
|
|
const Lardon3DSparseGeometryPose rotation = {
|
|
{c, -s, 0, s, c, 0, 0, 0, 1}, {0, 0, 0}};
|
|
const auto pixel = project(fixture.calibration, rotation, point);
|
|
fixture.observations[track * 2 + 1].x = pixel.x;
|
|
fixture.observations[track * 2 + 1].y = pixel.y;
|
|
}
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
413, 414, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.last_seed_geometry_status != LARDON3D_SPARSE_GEOMETRY_OK ||
|
|
output.landmark_count == 0);
|
|
CHECK(output.camera_count == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 13: planar seed is consumed as a Gate C degeneracy rejection. */
|
|
{
|
|
BatchFixture fixture = make_batch_fixture(2, 12, false, false, false, false);
|
|
for (size_t track = 0; track < 12; ++track) {
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5, 0.0, 5.0};
|
|
const Lardon3DSparseGeometryPose shifted = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 0, 0}};
|
|
const auto pixel = project(fixture.calibration, shifted, point);
|
|
fixture.observations[track * 2 + 1].x = pixel.x;
|
|
fixture.observations[track * 2 + 1].y = pixel.y;
|
|
}
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
415, 416, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.last_seed_geometry_status != LARDON3D_SPARSE_GEOMETRY_OK);
|
|
CHECK(output.camera_count == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 14: far but recoverable scene remains finite and bounded. */
|
|
{
|
|
BatchFixture fixture = make_batch_fixture(2, 12, false, false, false, false);
|
|
for (size_t track = 0; track < 12; ++track) {
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
-1.0 + static_cast<double>(track % 4) * 0.5,
|
|
-0.8 + static_cast<double>(track / 4) * 0.4, 100.0};
|
|
const Lardon3DSparseGeometryPose shifted = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 0, 0}};
|
|
const auto pixel = project(fixture.calibration, shifted, point);
|
|
fixture.observations[track * 2 + 1].x = pixel.x;
|
|
fixture.observations[track * 2 + 1].y = pixel.y;
|
|
}
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
417, 418, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.seed_image_a == 10 && output.seed_image_b == 11);
|
|
CHECK(output.camera_count == 2);
|
|
CHECK(output.landmark_count >= 6);
|
|
for (size_t index = 0; index < output.landmark_count; ++index) {
|
|
CHECK(std::isfinite(output.landmarks[index].point.x));
|
|
CHECK(std::isfinite(output.landmarks[index].point.y));
|
|
CHECK(std::isfinite(output.landmarks[index].point.z));
|
|
}
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 15: disconnected graph discovery retains a singleton component. */
|
|
{
|
|
BatchFixture disconnected =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
disconnected.images.push_back({99, disconnected.calibration});
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4171, 4172, disconnected.images.data(), disconnected.images.size(),
|
|
disconnected.observations.data(), disconnected.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.component_count == 2);
|
|
CHECK(output.components[1].component_key == 99);
|
|
CHECK(output.components[1].registered_image_count == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 16: two independently reconstructable components. */
|
|
{
|
|
BatchFixture fixture = make_batch_fixture(4, 24, false, false, false, false);
|
|
std::vector<Lardon3DSparseIncrementalObservation> split;
|
|
for (const auto &observation : fixture.observations) {
|
|
const bool first = observation.track_id <= 12;
|
|
const bool keep = first ? observation.image_id <= 11 : observation.image_id >= 12;
|
|
if (!keep) continue;
|
|
auto copy = observation;
|
|
if (!first) {
|
|
copy.image_id += 10;
|
|
copy.feature_set_id += 10;
|
|
}
|
|
split.push_back(copy);
|
|
}
|
|
fixture.images = {{10, fixture.calibration}, {11, fixture.calibration},
|
|
{22, fixture.calibration}, {23, fixture.calibration}};
|
|
fixture.observations = split;
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
419, 420, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.component_count == 2);
|
|
CHECK(output.components[0].component_key == 10);
|
|
CHECK(output.components[1].component_key == 22);
|
|
CHECK(output.components[0].registered_image_count == 2);
|
|
CHECK(output.components[1].registered_image_count == 2);
|
|
CHECK(output.components[0].landmark_count >= 6);
|
|
CHECK(output.components[1].landmark_count >= 6);
|
|
/* CASE 17: both independent component seed cameras fix identity gauge. */
|
|
size_t identity_gauges = 0;
|
|
for (size_t index = 0; index < output.camera_count; ++index) {
|
|
if (output.cameras[index].image_id != output.cameras[index].component_key)
|
|
continue;
|
|
const Lardon3DSparseGeometryPose identity = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1}, {0, 0, 0}};
|
|
CHECK(std::memcmp(&output.cameras[index].pose_cw, &identity,
|
|
sizeof(identity)) == 0);
|
|
++identity_gauges;
|
|
}
|
|
CHECK(identity_gauges == 2);
|
|
/* CASE 31: component output is canonical. */
|
|
for (size_t index = 1; index < output.component_count; ++index)
|
|
CHECK(output.components[index - 1].component_key <
|
|
output.components[index].component_key);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 18: every unreconstructed image is explicit and component-keyed. */
|
|
{
|
|
BatchFixture incomplete =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
incomplete.images.push_back({99, incomplete.calibration});
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4191, 4192, incomplete.images.data(), incomplete.images.size(),
|
|
incomplete.observations.data(), incomplete.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.unregistered_image_count == 1);
|
|
CHECK(output.unregistered_images[0].image_id == 99);
|
|
CHECK(output.unregistered_images[0].component_key == 99);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 19: Gate C returns exact cheirality failure for a finite point that
|
|
* is in front of the seed cameras and behind the newly registered camera. */
|
|
{
|
|
const BatchFixture fixture = make_growth_fixture(19);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
421, 422, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.registration_successes >= 1);
|
|
CHECK(output.triangulation_attempts > 0);
|
|
CHECK(output.last_triangulation_status ==
|
|
LARDON3D_SPARSE_GEOMETRY_CHEIRALITY_FAILED);
|
|
CHECK(output.triangulation_failures == 0);
|
|
CHECK(output.rejected_behind_camera > 0);
|
|
CHECK(output.rejected_reprojection == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 20: a finite growth candidate is rejected by reprojection error. */
|
|
{
|
|
const BatchFixture fixture = make_growth_fixture(20);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
423, 424, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.registration_successes >= 1);
|
|
CHECK(output.triangulation_attempts > 0);
|
|
CHECK(output.rejected_reprojection > 0);
|
|
CHECK(output.rejected_behind_camera == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 21: a growth Track reaches and fails triangulation. */
|
|
{
|
|
const BatchFixture fixture = make_growth_fixture(21);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
425, 426, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.registration_successes >= 1);
|
|
CHECK(output.triangulation_attempts > 0);
|
|
CHECK(output.triangulation_failures > 0);
|
|
CHECK(output.rejected_behind_camera == 0);
|
|
CHECK(output.rejected_reprojection == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 22: repeated image observations and feature references are rejected. */
|
|
{
|
|
const BatchFixture valid =
|
|
make_batch_fixture(2, 12, false, false, false, false);
|
|
std::vector<Lardon3DSparseIncrementalObservation> duplicate_image =
|
|
valid.observations;
|
|
duplicate_image.push_back(duplicate_image.front());
|
|
duplicate_image.back().feature_index += 1;
|
|
Lardon3DSparseIncrementalInput candidate = {
|
|
4251, 4252, valid.images.data(), valid.images.size(),
|
|
duplicate_image.data(), duplicate_image.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
|
|
std::vector<Lardon3DSparseIncrementalObservation> duplicate_feature =
|
|
valid.observations;
|
|
duplicate_feature[2].feature_set_id = duplicate_feature[0].feature_set_id;
|
|
duplicate_feature[2].feature_index = duplicate_feature[0].feature_index;
|
|
candidate.observations = duplicate_feature.data();
|
|
candidate.observation_count = duplicate_feature.size();
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 23: one seed Track is atomically updated to all six cameras. */
|
|
{
|
|
const BatchFixture fixture = make_batch_fixture(6, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
425, 426, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
size_t found = 0;
|
|
for (size_t index = 0; index < output.landmark_count; ++index)
|
|
if (output.landmarks[index].track_id == 1) {
|
|
++found;
|
|
CHECK(output.landmarks[index].landmark_id == 1);
|
|
CHECK(output.landmarks[index].observation_count == 6);
|
|
CHECK(std::isfinite(output.landmarks[index].point.x));
|
|
CHECK(std::isfinite(output.landmarks[index].point.y));
|
|
CHECK(std::isfinite(output.landmarks[index].point.z));
|
|
}
|
|
CHECK(found == 1);
|
|
size_t observation_count = 0;
|
|
uint64_t previous_image_id = 0;
|
|
for (size_t index = 0; index < output.observation_count; ++index) {
|
|
if (output.observations[index].track_id != 1) continue;
|
|
CHECK(output.observations[index].image_id > previous_image_id);
|
|
CHECK(output.observations[index].position_in_track == observation_count);
|
|
previous_image_id = output.observations[index].image_id;
|
|
++observation_count;
|
|
}
|
|
CHECK(observation_count == 6);
|
|
CHECK(output.landmark_update_attempts > 0);
|
|
CHECK(output.landmark_update_successes > 0);
|
|
|
|
Lardon3DSparseIncrementalResult repeated = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &repeated) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(repeated.landmark_count == output.landmark_count);
|
|
CHECK(repeated.observation_count == output.observation_count);
|
|
CHECK(std::memcmp(repeated.landmarks, output.landmarks,
|
|
output.landmark_count * sizeof(*output.landmarks)) == 0);
|
|
CHECK(std::memcmp(repeated.observations, output.observations,
|
|
output.observation_count * sizeof(*output.observations)) == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&repeated);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 23 rollback: one bad newly registered observation cannot replace the
|
|
* valid two-view landmark. */
|
|
{
|
|
BatchFixture rollback =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
BatchFixture reference = rollback;
|
|
reference.images.pop_back();
|
|
reference.observations.erase(
|
|
std::remove_if(reference.observations.begin(),
|
|
reference.observations.end(),
|
|
[](const auto &observation) {
|
|
return observation.image_id == 12;
|
|
}),
|
|
reference.observations.end());
|
|
const Lardon3DSparseIncrementalInput reference_input = {
|
|
427, 428, reference.images.data(), reference.images.size(),
|
|
reference.observations.data(), reference.observations.size()};
|
|
Lardon3DSparseIncrementalResult reference_output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(
|
|
&reference_input, ¶meters, &reference_output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
Lardon3DSparseGeometryPoint3 reference_point = {};
|
|
for (size_t index = 0; index < reference_output.landmark_count; ++index)
|
|
if (reference_output.landmarks[index].track_id == 1)
|
|
reference_point = reference_output.landmarks[index].point;
|
|
for (auto &observation : rollback.observations)
|
|
if (observation.track_id == 1 && observation.image_id == 12) {
|
|
observation.x += 900.0;
|
|
observation.y -= 700.0;
|
|
}
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
427, 428, rollback.images.data(), rollback.images.size(),
|
|
rollback.observations.data(), rollback.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.landmark_update_failures > 0);
|
|
size_t found = 0;
|
|
for (size_t index = 0; index < output.landmark_count; ++index)
|
|
if (output.landmarks[index].track_id == 1) {
|
|
++found;
|
|
CHECK(output.landmarks[index].observation_count == 2);
|
|
CHECK(std::memcmp(&output.landmarks[index].point, &reference_point,
|
|
sizeof(reference_point)) == 0);
|
|
}
|
|
CHECK(found == 1);
|
|
lardon3d_sparse_incremental_result_destroy(&reference_output);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 24: a Track with no seed landmark becomes eligible after PnP. */
|
|
{
|
|
const BatchFixture growth = make_growth_fixture(0);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4281, 4282, growth.images.data(), growth.images.size(),
|
|
growth.observations.data(), growth.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.registration_successes == 1);
|
|
size_t found = 0;
|
|
for (size_t index = 0; index < output.landmark_count; ++index)
|
|
if (output.landmarks[index].track_id == 13) ++found;
|
|
CHECK(found == 1);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 25: a new growth landmark consumes all three registered views. */
|
|
{
|
|
const BatchFixture growth = make_growth_fixture(25);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4283, 4284, growth.images.data(), growth.images.size(),
|
|
growth.observations.data(), growth.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
size_t found = 0;
|
|
for (size_t index = 0; index < output.landmark_count; ++index)
|
|
if (output.landmarks[index].track_id == 13) {
|
|
++found;
|
|
CHECK(output.landmarks[index].observation_count == 3);
|
|
}
|
|
CHECK(found == 1);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 26: point-only refinement is attempted and accepted. */
|
|
{
|
|
const BatchFixture growth = make_growth_fixture(25);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4285, 4286, growth.images.data(), growth.images.size(),
|
|
growth.observations.data(), growth.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.point_refinement_attempts > 0);
|
|
CHECK(output.point_refinement_successes > 0);
|
|
for (size_t index = 0; index < output.landmark_count; ++index) {
|
|
CHECK(std::isfinite(output.landmarks[index].point.x));
|
|
CHECK(std::isfinite(output.landmarks[index].point.y));
|
|
CHECK(std::isfinite(output.landmarks[index].point.z));
|
|
}
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 27: a valid seed remains intact when a full registration attempt
|
|
* makes no progress. */
|
|
{
|
|
const BatchFixture blocked =
|
|
make_batch_fixture(3, 12, false, false, false, true);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
429, 430, blocked.images.data(), blocked.images.size(),
|
|
blocked.observations.data(), blocked.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.camera_count == 2);
|
|
CHECK(output.landmark_count >= parameters.minimum_seed_landmarks);
|
|
CHECK(output.registration_rounds == 1);
|
|
CHECK(output.registration_attempts > 0);
|
|
CHECK(output.registration_successes == 0);
|
|
CHECK(output.no_growth_terminations == 1);
|
|
CHECK(output.round_limit_terminations == 0);
|
|
CHECK(output.unregistered_image_count == 1);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 28: natural completion registers every image without stop reason. */
|
|
{
|
|
const BatchFixture complete =
|
|
make_batch_fixture(5, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4301, 4302, complete.images.data(), complete.images.size(),
|
|
complete.observations.data(), complete.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.camera_count == 5);
|
|
CHECK(output.registration_rounds == 3);
|
|
CHECK(output.unregistered_image_count == 0);
|
|
CHECK(output.no_growth_terminations == 0);
|
|
CHECK(output.round_limit_terminations == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 29: the only available seed is attempted and exhausted. */
|
|
{
|
|
BatchFixture exhausted =
|
|
make_batch_fixture(2, 12, false, false, false, false);
|
|
for (auto &observation : exhausted.observations) {
|
|
observation.x = 2100.0;
|
|
observation.y = 1500.0;
|
|
}
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
4303, 4304, exhausted.images.data(), exhausted.images.size(),
|
|
exhausted.observations.data(), exhausted.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(output.seed_candidates_available == 1);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
CHECK(output.camera_count == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 30: one permitted round registers exactly one of three remaining
|
|
* cameras and stops at the configured bound. */
|
|
{
|
|
const BatchFixture bounded =
|
|
make_batch_fixture(5, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters one_round = parameters;
|
|
one_round.maximum_registration_rounds = 1;
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
431, 432, bounded.images.data(), bounded.images.size(),
|
|
bounded.observations.data(), bounded.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, &one_round, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_PARTIAL);
|
|
CHECK(output.registration_rounds == 1);
|
|
CHECK(output.registration_successes == 1);
|
|
CHECK(output.camera_count == 3);
|
|
CHECK(output.unregistered_image_count == 2);
|
|
CHECK(output.no_growth_terminations == 0);
|
|
CHECK(output.round_limit_terminations == 1);
|
|
|
|
Lardon3DSparseIncrementalResult repeated = {};
|
|
CHECK(lardon3d_sparse_incremental_run(
|
|
&candidate, &one_round, &repeated) == output.status);
|
|
CHECK(repeated.registration_rounds == output.registration_rounds);
|
|
CHECK(repeated.camera_count == output.camera_count);
|
|
CHECK(repeated.unregistered_image_count == output.unregistered_image_count);
|
|
CHECK(std::memcmp(repeated.cameras, output.cameras,
|
|
output.camera_count * sizeof(*output.cameras)) == 0);
|
|
CHECK(std::memcmp(repeated.unregistered_images,
|
|
output.unregistered_images,
|
|
output.unregistered_image_count *
|
|
sizeof(*output.unregistered_images)) == 0);
|
|
lardon3d_sparse_incremental_result_destroy(&repeated);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* CASE 32: camera output is strictly ordered by image identity. */
|
|
/* CASE 33: landmark output is strictly ordered by Track identity. */
|
|
/* CASE 34: a complete in-process rerun has identical scientific arrays and
|
|
* deterministic diagnostics. */
|
|
{
|
|
BatchFixture canonical =
|
|
make_batch_fixture(5, 12, false, false, false, false);
|
|
std::reverse(canonical.images.begin(), canonical.images.end());
|
|
std::reverse(canonical.observations.begin(), canonical.observations.end());
|
|
const Lardon3DSparseIncrementalInput candidate = {
|
|
433, 434, canonical.images.data(), canonical.images.size(),
|
|
canonical.observations.data(), canonical.observations.size()};
|
|
Lardon3DSparseIncrementalResult first = {}, second = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &first) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(lardon3d_sparse_incremental_run(&candidate, ¶meters, &second) ==
|
|
first.status);
|
|
for (size_t index = 1; index < first.camera_count; ++index)
|
|
CHECK(first.cameras[index - 1].image_id < first.cameras[index].image_id);
|
|
for (size_t index = 1; index < first.landmark_count; ++index)
|
|
CHECK(first.landmarks[index - 1].component_key <
|
|
first.landmarks[index].component_key ||
|
|
(first.landmarks[index - 1].component_key ==
|
|
first.landmarks[index].component_key &&
|
|
first.landmarks[index - 1].track_id <
|
|
first.landmarks[index].track_id));
|
|
CHECK(first.component_count == second.component_count);
|
|
CHECK(first.camera_count == second.camera_count);
|
|
CHECK(first.landmark_count == second.landmark_count);
|
|
CHECK(first.observation_count == second.observation_count);
|
|
CHECK(first.unregistered_image_count == second.unregistered_image_count);
|
|
CHECK(std::memcmp(first.components, second.components,
|
|
first.component_count * sizeof(*first.components)) == 0);
|
|
CHECK(std::memcmp(first.cameras, second.cameras,
|
|
first.camera_count * sizeof(*first.cameras)) == 0);
|
|
CHECK(std::memcmp(first.landmarks, second.landmarks,
|
|
first.landmark_count * sizeof(*first.landmarks)) == 0);
|
|
CHECK(std::memcmp(first.observations, second.observations,
|
|
first.observation_count * sizeof(*first.observations)) == 0);
|
|
CHECK(first.seed_candidates_considered ==
|
|
second.seed_candidates_considered);
|
|
CHECK(first.registration_rounds == second.registration_rounds);
|
|
CHECK(first.triangulation_attempts == second.triangulation_attempts);
|
|
CHECK(first.landmark_update_successes ==
|
|
second.landmark_update_successes);
|
|
lardon3d_sparse_incremental_result_destroy(&second);
|
|
lardon3d_sparse_incremental_result_destroy(&first);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool run_boundary_cases() {
|
|
Lardon3DSparseIncrementalParameters parameters;
|
|
CHECK(lardon3d_sparse_incremental_parameters_default(¶meters));
|
|
parameters.minimum_seed_tracks = 6;
|
|
parameters.minimum_seed_landmarks = 6;
|
|
parameters.minimum_pnp_correspondences = 6;
|
|
|
|
/* Input count validation: maximum_images LIMIT-1, LIMIT, LIMIT+1. */
|
|
for (size_t camera_count : {size_t{2}, size_t{3}, size_t{4}}) {
|
|
const BatchFixture fixture = make_batch_fixture(
|
|
camera_count, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters bounded = parameters;
|
|
bounded.maximum_images = 3;
|
|
const Lardon3DSparseIncrementalInput input = {
|
|
501, 502, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
const auto status =
|
|
lardon3d_sparse_incremental_run(&input, &bounded, &output);
|
|
CHECK(camera_count <= 3 ? status == LARDON3D_SPARSE_INCREMENTAL_COMPLETE
|
|
: status ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* Input count validation: maximum_tracks LIMIT-1, LIMIT, LIMIT+1. */
|
|
for (size_t track_count : {size_t{11}, size_t{12}, size_t{13}}) {
|
|
const BatchFixture fixture = make_batch_fixture(
|
|
2, track_count, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters bounded = parameters;
|
|
bounded.maximum_tracks = 12;
|
|
const Lardon3DSparseIncrementalInput input = {
|
|
503, 504, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
const auto status =
|
|
lardon3d_sparse_incremental_run(&input, &bounded, &output);
|
|
CHECK(track_count <= 12 ? status == LARDON3D_SPARSE_INCREMENTAL_COMPLETE
|
|
: status == LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* Input count validation: maximum_observations LIMIT-1, LIMIT, LIMIT+1. */
|
|
const BatchFixture observation_fixture =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
for (size_t observation_count : {size_t{34}, size_t{35}, size_t{36}}) {
|
|
std::vector<Lardon3DSparseIncrementalObservation> observations =
|
|
observation_fixture.observations;
|
|
if (observation_count <= 35) observations.erase(observations.begin() + 35);
|
|
if (observation_count == 34) observations.erase(observations.begin() + 32);
|
|
Lardon3DSparseIncrementalParameters bounded = parameters;
|
|
bounded.maximum_observations = 35;
|
|
bounded.maximum_tracks = 12;
|
|
const Lardon3DSparseIncrementalInput input = {
|
|
505, 506, observation_fixture.images.data(),
|
|
observation_fixture.images.size(), observations.data(),
|
|
observations.size()};
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
const auto status =
|
|
lardon3d_sparse_incremental_run(&input, &bounded, &output);
|
|
CHECK(observation_count <= 35
|
|
? status == LARDON3D_SPARSE_INCREMENTAL_COMPLETE
|
|
: status == LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
}
|
|
|
|
/* Policy boundary: one seed candidate fails, two reach the valid fallback. */
|
|
const BatchFixture seeds =
|
|
make_batch_fixture(4, 12, true, false, false, false);
|
|
const Lardon3DSparseIncrementalInput seed_input = {
|
|
507, 508, seeds.images.data(), seeds.images.size(),
|
|
seeds.observations.data(), seeds.observations.size()};
|
|
Lardon3DSparseIncrementalParameters one_seed = parameters;
|
|
one_seed.maximum_seed_candidates = 1;
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(&seed_input, &one_seed, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_FAILED);
|
|
CHECK(output.seed_candidates_considered == 1);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
one_seed.maximum_seed_candidates = 2;
|
|
CHECK(lardon3d_sparse_incremental_run(&seed_input, &one_seed, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.seed_candidates_considered == 2);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
|
|
/* Policy boundary: at most two new landmarks are admitted in one round. */
|
|
BatchFixture growth =
|
|
make_batch_fixture(3, 12, false, false, false, false);
|
|
growth.observations.erase(
|
|
std::remove_if(
|
|
growth.observations.begin(), growth.observations.end(),
|
|
[](const auto &observation) {
|
|
return observation.image_id == 12 && observation.track_id > 6;
|
|
}),
|
|
growth.observations.end());
|
|
for (uint64_t track = 13; track <= 15; ++track) {
|
|
const Lardon3DSparseGeometryPoint3 point = {
|
|
-0.5 + static_cast<double>(track - 13) * 0.3, 0.2, 6.0};
|
|
for (size_t camera : {size_t{0}, size_t{2}}) {
|
|
const Lardon3DSparseGeometryPose pose = {
|
|
{1, 0, 0, 0, 1, 0, 0, 0, 1},
|
|
{static_cast<double>(camera), 0, 0}};
|
|
const auto pixel = project(growth.calibration, pose, point);
|
|
growth.observations.push_back(
|
|
{track, 10 + camera, 100 + camera,
|
|
static_cast<uint32_t>(track), 16, pixel.x, pixel.y});
|
|
}
|
|
}
|
|
Lardon3DSparseIncrementalParameters two_landmarks = parameters;
|
|
two_landmarks.maximum_landmarks_per_round = 2;
|
|
const Lardon3DSparseIncrementalInput growth_input = {
|
|
509, 510, growth.images.data(), growth.images.size(),
|
|
growth.observations.data(), growth.observations.size()};
|
|
CHECK(lardon3d_sparse_incremental_run(
|
|
&growth_input, &two_landmarks, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
CHECK(output.landmark_count == 14);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
return true;
|
|
}
|
|
|
|
static bool run_failure_cases() {
|
|
Lardon3DSparseIncrementalParameters parameters;
|
|
CHECK(lardon3d_sparse_incremental_parameters_default(¶meters));
|
|
parameters.minimum_seed_tracks = 6;
|
|
parameters.minimum_seed_landmarks = 6;
|
|
parameters.minimum_pnp_correspondences = 6;
|
|
const BatchFixture base =
|
|
make_batch_fixture(2, 12, false, false, false, false);
|
|
const Lardon3DSparseIncrementalInput valid = {
|
|
601, 602, base.images.data(), base.images.size(),
|
|
base.observations.data(), base.observations.size()};
|
|
|
|
auto expect_invalid = [&](const Lardon3DSparseIncrementalInput &input) {
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
const bool accepted = lardon3d_sparse_incremental_run(
|
|
&input, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT &&
|
|
output.camera_count == 0 && output.landmark_count == 0;
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
return accepted;
|
|
};
|
|
|
|
Lardon3DSparseIncrementalResult output = {};
|
|
CHECK(lardon3d_sparse_incremental_run(nullptr, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_sparse_incremental_run(&valid, nullptr, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_sparse_incremental_run(&valid, ¶meters, nullptr) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_INVALID_ARGUMENT);
|
|
|
|
Lardon3DSparseIncrementalInput invalid = valid;
|
|
invalid.images = nullptr;
|
|
CHECK(expect_invalid(invalid));
|
|
invalid = valid;
|
|
invalid.observations = nullptr;
|
|
CHECK(expect_invalid(invalid));
|
|
invalid = valid;
|
|
invalid.observation_count = 0;
|
|
CHECK(expect_invalid(invalid));
|
|
invalid = valid;
|
|
invalid.track_set_id = 0;
|
|
CHECK(expect_invalid(invalid));
|
|
invalid = valid;
|
|
invalid.calibration_scope_id = 0;
|
|
CHECK(expect_invalid(invalid));
|
|
|
|
for (int kind = 0; kind < 5; ++kind) {
|
|
std::vector<Lardon3DSparseIncrementalImage> images = base.images;
|
|
if (kind == 0) images[0].calibration.fx = 0.0;
|
|
if (kind == 1) images[0].calibration.fx = NAN;
|
|
if (kind == 2) images[0].calibration.k1 = INFINITY;
|
|
if (kind == 3) images[0].calibration.cx = -1.0;
|
|
if (kind == 4) images[0].image_id = 0;
|
|
invalid = valid;
|
|
invalid.images = images.data();
|
|
CHECK(expect_invalid(invalid));
|
|
}
|
|
|
|
for (int kind = 0; kind < 6; ++kind) {
|
|
std::vector<Lardon3DSparseIncrementalObservation> observations =
|
|
base.observations;
|
|
if (kind == 0) observations[0].feature_set_id = 0;
|
|
if (kind == 1)
|
|
observations[0].feature_index = observations[0].feature_count;
|
|
if (kind == 2) observations[0].x = NAN;
|
|
if (kind == 3) observations[0].y = INFINITY;
|
|
if (kind == 4) observations[0].image_id = 999;
|
|
if (kind == 5) observations[0].track_id = 0;
|
|
invalid = valid;
|
|
invalid.observations = observations.data();
|
|
CHECK(expect_invalid(invalid));
|
|
}
|
|
|
|
std::vector<Lardon3DSparseIncrementalObservation> singleton = {
|
|
base.observations.front()};
|
|
invalid = valid;
|
|
invalid.observations = singleton.data();
|
|
invalid.observation_count = singleton.size();
|
|
CHECK(expect_invalid(invalid));
|
|
|
|
/* Repeated failure, safe destruction, and a valid call after failure. */
|
|
invalid = valid;
|
|
invalid.calibration_scope_id = 0;
|
|
CHECK(expect_invalid(invalid));
|
|
CHECK(expect_invalid(invalid));
|
|
CHECK(lardon3d_sparse_incremental_run(&valid, ¶meters, &output) ==
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
lardon3d_sparse_incremental_result_destroy(&output);
|
|
lardon3d_sparse_incremental_result_destroy(nullptr);
|
|
return true;
|
|
}
|
|
|
|
static void signature_mix(uint64_t *hash, const void *data, size_t size) {
|
|
const auto *bytes = static_cast<const unsigned char *>(data);
|
|
for (size_t index = 0; index < size; ++index) {
|
|
*hash ^= bytes[index];
|
|
*hash *= UINT64_C(1099511628211);
|
|
}
|
|
}
|
|
|
|
static bool scientific_signature(uint64_t *signature) {
|
|
const BatchFixture fixture =
|
|
make_batch_fixture(5, 12, false, false, false, false);
|
|
Lardon3DSparseIncrementalParameters parameters;
|
|
if (!lardon3d_sparse_incremental_parameters_default(¶meters)) return false;
|
|
parameters.minimum_seed_tracks = 6;
|
|
parameters.minimum_seed_landmarks = 6;
|
|
parameters.minimum_pnp_correspondences = 6;
|
|
const Lardon3DSparseIncrementalInput input = {
|
|
701, 702, fixture.images.data(), fixture.images.size(),
|
|
fixture.observations.data(), fixture.observations.size()};
|
|
Lardon3DSparseIncrementalResult result = {};
|
|
if (lardon3d_sparse_incremental_run(&input, ¶meters, &result) !=
|
|
LARDON3D_SPARSE_INCREMENTAL_COMPLETE)
|
|
return false;
|
|
|
|
uint64_t hash = UINT64_C(1469598103934665603);
|
|
#define MIX(value) signature_mix(&hash, &(value), sizeof(value))
|
|
MIX(result.status);
|
|
MIX(result.track_set_id);
|
|
MIX(result.calibration_scope_id);
|
|
for (size_t index = 0; index < result.component_count; ++index) {
|
|
MIX(result.components[index].component_key);
|
|
MIX(result.components[index].image_count);
|
|
MIX(result.components[index].registered_image_count);
|
|
MIX(result.components[index].landmark_count);
|
|
}
|
|
for (size_t index = 0; index < result.camera_count; ++index) {
|
|
MIX(result.cameras[index].image_id);
|
|
MIX(result.cameras[index].component_key);
|
|
signature_mix(&hash, result.cameras[index].pose_cw.rotation_cw,
|
|
sizeof(result.cameras[index].pose_cw.rotation_cw));
|
|
signature_mix(&hash, result.cameras[index].pose_cw.translation_cw,
|
|
sizeof(result.cameras[index].pose_cw.translation_cw));
|
|
}
|
|
for (size_t index = 0; index < result.landmark_count; ++index) {
|
|
MIX(result.landmarks[index].landmark_id);
|
|
MIX(result.landmarks[index].track_id);
|
|
MIX(result.landmarks[index].component_key);
|
|
MIX(result.landmarks[index].point.x);
|
|
MIX(result.landmarks[index].point.y);
|
|
MIX(result.landmarks[index].point.z);
|
|
MIX(result.landmarks[index].reprojection_rmse_px);
|
|
MIX(result.landmarks[index].reprojection_median_px);
|
|
MIX(result.landmarks[index].observation_count);
|
|
}
|
|
for (size_t index = 0; index < result.observation_count; ++index) {
|
|
MIX(result.observations[index].landmark_id);
|
|
MIX(result.observations[index].track_id);
|
|
MIX(result.observations[index].image_id);
|
|
MIX(result.observations[index].feature_set_id);
|
|
MIX(result.observations[index].feature_index);
|
|
MIX(result.observations[index].position_in_track);
|
|
}
|
|
MIX(result.seed_candidates_available);
|
|
MIX(result.seed_candidates_considered);
|
|
MIX(result.seed_image_a);
|
|
MIX(result.seed_image_b);
|
|
MIX(result.registration_rounds);
|
|
MIX(result.registration_attempts);
|
|
MIX(result.registration_successes);
|
|
MIX(result.triangulation_attempts);
|
|
MIX(result.landmark_update_attempts);
|
|
MIX(result.landmark_update_successes);
|
|
MIX(result.point_refinement_attempts);
|
|
MIX(result.point_refinement_successes);
|
|
#undef MIX
|
|
lardon3d_sparse_incremental_result_destroy(&result);
|
|
*signature = hash;
|
|
return true;
|
|
}
|
|
|
|
static bool run_case_35(const char *executable) {
|
|
/* CASE 35: twenty fresh executable images emit one scientific signature. */
|
|
uint64_t expected = 0;
|
|
for (size_t run = 0; run < 20; ++run) {
|
|
int descriptors[2];
|
|
CHECK(pipe(descriptors) == 0);
|
|
const pid_t child = fork();
|
|
CHECK(child >= 0);
|
|
if (child == 0) {
|
|
close(descriptors[0]);
|
|
CHECK(dup2(descriptors[1], STDOUT_FILENO) >= 0);
|
|
close(descriptors[1]);
|
|
execl(executable, executable, "--scientific-signature", nullptr);
|
|
_exit(127);
|
|
}
|
|
close(descriptors[1]);
|
|
uint64_t actual = 0;
|
|
size_t received = 0;
|
|
while (received < sizeof(actual)) {
|
|
const ssize_t count = read(
|
|
descriptors[0], reinterpret_cast<unsigned char *>(&actual) + received,
|
|
sizeof(actual) - received);
|
|
if (count <= 0) break;
|
|
received += static_cast<size_t>(count);
|
|
}
|
|
close(descriptors[0]);
|
|
int status = 0;
|
|
CHECK(waitpid(child, &status, 0) == child);
|
|
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
CHECK(received == sizeof(actual));
|
|
if (run == 0)
|
|
expected = actual;
|
|
else
|
|
CHECK(actual == expected);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc == 2 && std::strcmp(argv[1], "--scientific-signature") == 0) {
|
|
uint64_t signature = 0;
|
|
if (!scientific_signature(&signature)) return EXIT_FAILURE;
|
|
return write(STDOUT_FILENO, &signature, sizeof(signature)) ==
|
|
static_cast<ssize_t>(sizeof(signature))
|
|
? EXIT_SUCCESS
|
|
: EXIT_FAILURE;
|
|
}
|
|
return run_test() && run_batch_cases() && run_boundary_cases() &&
|
|
run_failure_cases() && run_case_35(argv[0])
|
|
? EXIT_SUCCESS
|
|
: EXIT_FAILURE;
|
|
}
|