// Copyright 2026, lbgos // SPDX-License-Identifier: BSL-1.0 #include "catch_amalgamated.hpp" #include "internal/constrained_pose.h" #include "internal/sample.h" #include "internal/correspondence_search.h" #include #include #include #include #include namespace { double distance(xrt_vec3 a, xrt_vec3 b) { return std::sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z)); } double angle(xrt_quat a, xrt_quat b) { // Relative rotation conj(a) * b in double; atan2 stays accurate near zero where acos of a float dot does not. const double aw = a.w, ax = -a.x, ay = -a.y, az = -a.z, bw = b.w, bx = b.x, by = b.y, bz = b.z; const double w = aw * bw - ax * bx - ay * by - az * bz; const double x = aw * bx + ax * bw + ay * bz - az * by; const double y = aw * by - ax * bz + ay * bw + az * bx; const double z = aw * bz + ax * by - ay * bx + az * bw; return 2 * std::atan2(std::sqrt(x * x + y * y + z * z), std::abs(w)); } xrt_quat rotation(double angle, xrt_vec3 axis) { xrt_quat q; math_vec3_normalize(&axis); math_quat_from_angle_vector(angle, &axis, &q); return q; } xrt_quat conjugate(const xrt_quat &q) { return {-q.x, -q.y, -q.z, q.w}; } xrt_quat multiply(const xrt_quat &a, const xrt_quat &b) { xrt_quat q; math_quat_rotate(&a, &b, &q); return q; } struct Scene { std::array leds = {{{11, {-0.047f, -0.033f, -0.017f}, {0, 0, -1}, 3.5f}, {7, {0.039f, -0.009f, 0.027f}, {0, 0, -1}, 3.5f}, {23, {0.009f, 0.051f, -0.011f}, {0, 0, -1}, 3.5f}}}; t_constellation_led_model model{}; camera_model camera{}; std::array observations{}; std::array views{}; xrt_pose truth = XRT_POSE_IDENTITY; xrt_pose tilt = XRT_POSE_IDENTITY; constellation_constrained_config config; Scene() { model.id = 2; model.leds = leds.data(); model.num_leds = leds.size(); camera.width = 640; camera.height = 480; camera.calib.fx = 300; camera.calib.fy = 310; camera.calib.cx = 320; camera.calib.cy = 240; camera.calib.model = T_DISTORTION_FISHEYE_KB4; camera.calib.fisheye.k1 = 0.02; camera.calib.fisheye.k2 = -0.005; truth.position = {0.07f, 0.02f, 0.68f}; truth.orientation = rotation(0.4, {0, 1, 0}); for (unsigned k = 0; k < 2; k++) { views[k].camera_index = k + 3; views[k].P_world_cam = XRT_POSE_IDENTITY; views[k].calib = &camera; views[k].observation = &observations[k]; } views[1].P_world_cam.position = {0.12f, 0.02f, 0.015f}; views[1].P_world_cam.orientation = rotation(-0.21, {0.1f, 1, 0.03f}); for (auto &v : views) math_pose_invert(&v.P_world_cam, &v.P_cam_world); constellation_constrained_default_config(&config); config.time_budget_us = 2000000; // These fixtures exercise the recovered scorer minimum (inliers > sample size) with three // LEDs, so they relax Monado's stricter cold-acquisition and tracking policy. config.minimum_cold_inliers = 3; config.minimum_cold_distinct_leds = 0; config.minimum_tracking_inliers = 0; config.minimum_tracking_distinct_leds = 0; } void observe(unsigned camera_index, std::initializer_list indices) { auto &o = observations[camera_index]; o = {}; for (unsigned led : indices) { xrt_vec3 world, local; math_pose_transform_point(&truth, &leds[led].pos, &world); math_pose_transform_point(&views[camera_index].P_cam_world, &world, &local); blob &b = o.blobs[o.num_blobs++]; REQUIRE(t_camera_models_project(&camera.calib, local.x, local.y, local.z, &b.x, &b.y)); b.led_id = LED_MAKE_ID(99, 42); // Temporal labels must not force an assignment. } } }; } // namespace TEST_CASE("A held HMD cannot turn accurate camera pixels into false controller fixes") { for (bool right : {false, true}) { Scene s; if (right) { for (auto &led : s.leds) led.pos.x = -led.pos.x; s.truth.position.x = -s.truth.position.x; } s.observe(0, {0, 1}); s.observe(1, {2}); constellation_constrained_result live; REQUIRE(constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &live)); CHECK(distance(live.P_world_model.position, s.truth.position) < 0.0003); // The same pixels fit a wrong world pose perfectly if the camera's held world frame is used. xrt_pose delta{rotation(0.5, {0, 1, 0}), {0.6f, 0, -0.2f}}, wrong; math_pose_transform(&delta, &s.truth, &wrong); for (auto &view : s.views) { xrt_pose current = view.P_world_cam; math_pose_transform(&delta, ¤t, &view.P_world_cam); math_pose_invert(&view.P_world_cam, &view.P_cam_world); } constellation_constrained_result stale; REQUIRE(constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &stale)); CHECK(distance(stale.P_world_model.position, wrong.position) < 0.0003); CHECK(distance(stale.P_world_model.position, s.truth.position) > 0.2); CHECK(stale.cost_m < 0.001); constellation_tracking_sample sample{}; xrt_space_relation held = XRT_SPACE_RELATION_ZERO; held.pose = delta; held.relation_flags = static_cast(XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT); CHECK_FALSE(constellation_tracking_sample_set_hmd_pose(&sample, XRT_SUCCESS, &held)); CHECK_FALSE(sample.have_hmd_pose); held.relation_flags = static_cast(held.relation_flags | XRT_SPACE_RELATION_POSITION_TRACKED_BIT); CHECK_FALSE(constellation_tracking_sample_set_hmd_pose(&sample, XRT_ERROR_POSE_NOT_ACTIVE, &held)); held.pose = XRT_POSE_IDENTITY; CHECK(constellation_tracking_sample_set_hmd_pose(&sample, XRT_SUCCESS, &held)); CHECK(sample.have_hmd_pose); CHECK(distance(sample.P_xrworld_hmd.position, held.pose.position) == 0); } } TEST_CASE("Constrained two-point geometry recovers 1000 randomized valid poses") { std::mt19937 rng(0x534f4c); std::uniform_real_distribution unit(-1, 1); unsigned accepted = 0, two_roots = 0; for (unsigned attempt = 0; accepted < 1000 && attempt < 10000; attempt++) { xrt_vec3 points[2] = {{unit(rng) * 0.06f, unit(rng) * 0.06f, unit(rng) * 0.06f}, {unit(rng) * 0.06f, unit(rng) * 0.06f, unit(rng) * 0.06f}}; xrt_vec3 gravity = {unit(rng), unit(rng), unit(rng)}; math_vec3_normalize(&gravity); xrt_quat tilt = rotation(unit(rng), {unit(rng), unit(rng), unit(rng)}); xrt_pose truth = XRT_POSE_IDENTITY; truth.orientation = multiply(rotation(unit(rng) * 3.1, gravity), tilt); truth.position = {unit(rng) * 0.2f, unit(rng) * 0.2f, 0.8f + unit(rng) * 0.4f}; xrt_vec3 bearings[2]; for (unsigned i = 0; i < 2; i++) { math_pose_transform_point(&truth, &points[i], &bearings[i]); math_vec3_normalize(&bearings[i]); } xrt_pose heading; if (!constellation_constrained_heading_two_point(points, bearings, &truth.orientation, 0.001745329, &heading, nullptr)) continue; REQUIRE(distance(heading.position, truth.position) < 0.0002); xrt_pose roots[2]; unsigned count = constellation_constrained_gravity_two_point(points, bearings, &tilt, &gravity, 0.001745329, roots, nullptr); REQUIRE(count >= 1); bool recovered = false; for (unsigned i = 0; i < count; i++) { if (distance(roots[i].position, truth.position) < 0.0003 && angle(roots[i].orientation, truth.orientation) < 0.001) recovered = true; for (unsigned p = 0; p < 2; p++) { xrt_vec3 projected; math_pose_transform_point(&roots[i], &points[p], &projected); REQUIRE(projected.z > 0); math_vec3_normalize(&projected); CHECK(distance(projected, bearings[p]) < 0.00001); } } REQUIRE(recovered); two_roots += count == 2; accepted++; } CHECK(accepted == 1000); CHECK(two_roots > 100); } TEST_CASE("Constrained solvers reject parallel rays negative depth and unresolved yaw") { xrt_vec3 points[2] = {{0, 0, 0}, {0.04f, 0.03f, 0.01f}}; xrt_vec3 bearings[2] = {{0, 0, 1}, {0, 0, 1}}; xrt_quat q = {0, 0, 0, 1}; xrt_vec3 gravity = {0, 1, 0}; xrt_pose out[2]; CHECK_FALSE(constellation_constrained_heading_two_point(points, bearings, &q, 0.001745329, out, nullptr)); CHECK(constellation_constrained_gravity_two_point(points, bearings, &q, &gravity, 0.001745329, out, nullptr) == 0); xrt_pose behind = XRT_POSE_IDENTITY; behind.position.z = -0.6f; for (unsigned i = 0; i < 2; i++) { math_pose_transform_point(&behind, &points[i], &bearings[i]); math_vec3_normalize(&bearings[i]); } CHECK_FALSE(constellation_constrained_heading_two_point(points, bearings, &q, 0.001745329, out, nullptr)); CHECK(constellation_constrained_gravity_two_point(points, bearings, &q, &gravity, 0.001745329, out, nullptr) == 0); points[1] = {0, 0.05f, 0}; bearings[0] = {0, 0, 1}; bearings[1] = {0, 0.05f, 0.6f}; math_vec3_normalize(&bearings[1]); CHECK(constellation_constrained_gravity_two_point(points, bearings, &q, &gravity, 0.001745329, out, nullptr) == 0); } TEST_CASE("Gravity acquisition jointly verifies two plus one calibrated observations") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); constellation_constrained_result result; REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &result)); CHECK(result.inliers == 3); CHECK(result.distinct_leds == 3); CHECK(result.information_rank == 4); CHECK(result.vision_determines_yaw); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.0003); CHECK(angle(result.P_world_model.orientation, s.truth.orientation) < 0.001); CHECK(result.camera_diagnostics[0].inliers == 2); CHECK(result.camera_diagnostics[1].inliers == 1); CHECK(result.assignments[0].led_id == 11); CHECK(result.assignments[1].led_id == 7); CHECK(result.assignments[2].led_id == 23); constellation_constrained_result reversed; std::reverse(s.views.begin(), s.views.end()); REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &reversed)); CHECK(distance(result.P_world_model.position, reversed.P_world_model.position) < 1e-6); CHECK(result.cost_m == Catch::Approx(reversed.cost_m).margin(1e-9)); CHECK(result.pair_trials == reversed.pair_trials); } TEST_CASE("Known capture heading gives translation with three observations") { Scene s; // A vertical model baseline observes no heading: the gravity path must report unresolved yaw. s.leds[0].pos = {0, -0.02f, 0}; s.leds[1].pos = {0, 0.03f, 0}; s.observe(0, {0, 1}); s.observe(1, {0}); s.tilt.orientation = s.truth.orientation; constellation_constrained_result result; CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &result)); CHECK(result.unresolved_yaw_pairs > 0); REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, true, false, &s.config, &result)); CHECK(result.origin == CONSTELLATION_CONSTRAINED_HEADING); CHECK(result.inliers == 3); CHECK(result.distinct_leds == 2); CHECK(result.information_rank == 3); CHECK_FALSE(result.vision_determines_yaw); CHECK(std::isinf(result.covariance[15])); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.0001); } TEST_CASE("Heading hypotheses claim optical yaw only when the fit observes it") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); s.tilt.orientation = s.truth.orientation; constellation_constrained_result result; REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, true, false, &s.config, &result)); CHECK(result.heading_hypotheses > 0); CHECK(result.information_rank == 4); CHECK(result.vision_determines_yaw); CHECK(std::isfinite(result.covariance[15])); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.0001); } TEST_CASE("Gravity hypotheses recover three observations despite incorrect heading yaw") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); s.tilt.orientation = rotation(-0.8, {0, 1, 0}); constellation_constrained_result result; CHECK_FALSE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.tilt, CONSTELLATION_CONSTRAINED_HEADING, &s.config, &result)); REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, true, false, &s.config, &result)); CHECK(result.origin == CONSTELLATION_CONSTRAINED_GRAVITY); CHECK(result.inliers == 3); CHECK(result.information_rank == 4); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.0003); CHECK(angle(result.P_world_model.orientation, s.truth.orientation) < 0.001); } TEST_CASE("Prior path counts repeated LED camera observations and requires geometric rank") { Scene s; s.observe(0, {0}); s.observe(1, {0}); constellation_constrained_result result; REQUIRE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.truth, true, true, &s.config, &result)); CHECK(result.origin == CONSTELLATION_CONSTRAINED_PRIOR); CHECK(result.inliers == 2); CHECK(result.distinct_leds == 1); CHECK(result.information_rank == 3); CHECK(result.assignments[0].led_index == result.assignments[1].led_index); CHECK(result.assignments[0].camera_index != result.assignments[1].camera_index); CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 1, &s.truth, true, true, &s.config, &result)); } TEST_CASE("Gravity verification counts repeated physical LEDs across calibrated cameras") { Scene s; s.observe(0, {0, 1}); s.observe(1, {0}); constellation_constrained_result result; REQUIRE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_GRAVITY, &s.config, &result)); CHECK(result.inliers == 3); CHECK(result.distinct_leds == 2); CHECK(result.information_rank == 4); CHECK(result.num_assignments == 3); } TEST_CASE("Wrong geometry normal visibility and duplicate LED identities do not pass low-count verification") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); constellation_constrained_result result; s.leds[2].pos.y += 0.02f; CHECK_FALSE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_GRAVITY, &s.config, &result)); s.leds[2].pos.y -= 0.02f; for (auto &led : s.leds) led.dir.z = 1; CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &result)); for (auto &led : s.leds) led.dir.z = -1; s.leds[2].id = s.leds[0].id; CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &result)); CHECK(result.rejection == CONSTELLATION_CONSTRAINED_INVALID_INPUT); } TEST_CASE("Mirrored geometry cannot acquire from the calibrated three-observation fixture") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); for (auto &led : s.leds) { led.pos.x = -led.pos.x; led.dir.x = -led.dir.x; } constellation_constrained_result result; CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 2, &s.tilt, false, false, &s.config, &result)); } TEST_CASE("Competing P3P poses and shared blobs retain ambiguity and ownership evidence") { Scene s; s.observe(0, {0, 1}); s.observe(1, {0, 2}); std::array hypotheses; REQUIRE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_P3P, &s.config, &hypotheses[0])); hypotheses[1] = hypotheses[0]; hypotheses[1].P_world_model.position.x += 0.1f; hypotheses[1].assignments[0].led_index = 1; constellation_constrained_result selected; CHECK_FALSE(constellation_constrained_select_results(hypotheses.data(), 2, &s.config, &selected)); CHECK(selected.rejection == CONSTELLATION_CONSTRAINED_AMBIGUOUS); CHECK(selected.ambiguity_likelihood_gap == Catch::Approx(0)); CHECK(constellation_constrained_results_share_blob(&hypotheses[0], &hypotheses[1])); for (unsigned i = 0; i < hypotheses[1].num_assignments; i++) hypotheses[1].assignments[i].camera_index += 10; CHECK_FALSE(constellation_constrained_results_share_blob(&hypotheses[0], &hypotheses[1])); hypotheses[1] = hypotheses[0]; REQUIRE(constellation_constrained_select_results(hypotheses.data(), 2, &s.config, &selected)); } TEST_CASE("P3P joint verification requires more observations than its three-point sample") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); constellation_constrained_result result; CHECK_FALSE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_P3P, &s.config, &result)); CHECK(result.rejection == CONSTELLATION_CONSTRAINED_TOO_FEW_INLIERS); s.observe(1, {0, 2}); REQUIRE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_P3P, &s.config, &result)); CHECK(result.inliers == 4); } TEST_CASE("Symmetric anonymous low-count acquisition rejects competing assignments") { Scene s; std::array symmetric{}; for (unsigned i = 0; i < 6; i++) { symmetric[i] = s.leds[i % 3]; symmetric[i].id = i; if (i >= 3) symmetric[i].pos.x += 0.2f; } s.model.leds = symmetric.data(); s.model.num_leds = symmetric.size(); s.truth.orientation = {0, 0, 0, 1}; s.truth.position = {0, 0, 0.6f}; for (unsigned i = 0; i < 3; i++) { blob &b = s.observations[0].blobs[i]; REQUIRE(t_camera_models_project(&s.camera.calib, symmetric[i].pos.x, symmetric[i].pos.y, symmetric[i].pos.z + 0.6f, &b.x, &b.y)); } s.observations[0].num_blobs = 3; constellation_constrained_result result; CHECK_FALSE( constellation_constrained_search(&s.model, s.views.data(), 1, &s.tilt, false, false, &s.config, &result)); CHECK(result.rejection == CONSTELLATION_CONSTRAINED_AMBIGUOUS); } TEST_CASE("Coincident different LED identities cannot be silently assigned by a greedy score") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); std::array leds = {s.leds[0], s.leds[1], s.leds[2], s.leds[2]}; leds[3].id = 99; s.model.leds = leds.data(); s.model.num_leds = leds.size(); constellation_constrained_result result; CHECK_FALSE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &s.truth, CONSTELLATION_CONSTRAINED_GRAVITY, &s.config, &result)); CHECK(result.rejection == CONSTELLATION_CONSTRAINED_AMBIGUOUS); } namespace { #include "constellation_rift_s_data.hpp" struct Rig { camera_model camera{}; std::vector observations; std::vector views; explicit Rig(unsigned count) { camera.width = 640; camera.height = 480; camera.calib.fx = rift_s_ring_intrinsics[0]; camera.calib.fy = rift_s_ring_intrinsics[1]; camera.calib.cx = rift_s_ring_intrinsics[2]; camera.calib.cy = rift_s_ring_intrinsics[3]; camera.calib.model = T_DISTORTION_FISHEYE_KB4; camera.calib.fisheye = {rift_s_ring_kb4[0], rift_s_ring_kb4[1], rift_s_ring_kb4[2], rift_s_ring_kb4[3]}; observations.resize(count); views.resize(count); const float yaw[] = {0, -0.6f, 0.6f, 0.3f, -0.3f}; const float pitch[] = {0, 0.1f, 0.1f, -0.7f, 0.5f}; for (unsigned k = 0; k < count; k++) { auto &v = views[k]; v.camera_index = k; v.calib = &camera; v.observation = &observations[k]; v.P_world_cam.orientation = multiply(rotation(yaw[k], {0, 1, 0}), rotation(pitch[k], {1, 0, 0})); v.P_world_cam.position = {0.04f * k - 0.05f, 0.01f * k, 0}; math_pose_invert(&v.P_world_cam, &v.P_cam_world); } } void clear() { for (auto &o : observations) o = {}; } /* Project an LED into a view; false when it faces away or leaves the frame. */ bool project(unsigned k, const xrt_pose &P_world_model, const t_constellation_led &led, float &x, float &y, double facing_cos) const { xrt_vec3 world, local, normal_world, normal; math_pose_transform_point(&P_world_model, &led.pos, &world); math_pose_transform_point(&views[k].P_cam_world, &world, &local); math_quat_rotate_vec3(&P_world_model.orientation, &led.dir, &normal_world); math_quat_rotate_vec3(&views[k].P_cam_world.orientation, &normal_world, &normal); xrt_vec3 to_camera = {-local.x, -local.y, -local.z}; math_vec3_normalize(&to_camera); if (local.z < 0.05f || m_vec3_dot(normal, to_camera) < facing_cos) return false; if (!t_camera_models_project(&camera.calib, local.x, local.y, local.z, &x, &y)) return false; return x >= 0 && y >= 0 && x < camera.width && y < camera.height; } void add_blob(unsigned k, float x, float y) { auto &o = observations[k]; blob &b = o.blobs[o.num_blobs++]; b = {}; b.x = x; b.y = y; b.width = b.height = 3; b.led_id = LED_INVALID_ID; } }; xrt_pose world_tilt_prior(const xrt_quat &truth, double yaw, double tilt_error, std::mt19937 &rng) { std::uniform_real_distribution unit(-1, 1); xrt_pose prior = XRT_POSE_IDENTITY; prior.position = {42, 42, 42}; xrt_quat tilt_noise = rotation(tilt_error, {unit(rng), 0, unit(rng)}); prior.orientation = multiply(tilt_noise, multiply(rotation(yaw, {0, 1, 0}), truth)); return prior; } struct MonteCarlo { unsigned accepted = 0, wrong = 0, rejected = 0, trials = 0; std::array reasons{}; double worst_error_m = 0; double worst_identity_error_m = 0; double worst_identity_error_sigma = 0; }; /* Random worn-like views: a partial ring of 3..6 observations spread over two cameras, * background blobs, centroid noise and a small IMU tilt error. Cold acquisition, unknown yaw. */ MonteCarlo run_cold_acquisition(bool mirror_model, unsigned distractors, unsigned trials, uint32_t seed, bool independent_tilt = false, bool owner_contests = false, bool recovered_minimum_only = false) { std::mt19937 rng(seed); std::uniform_real_distribution unit(-1, 1), positive(0, 1); std::normal_distribution noise(0, 0.3f); auto truth_leds = rift_s_ring_leds; auto model_leds = rift_s_ring_leds; if (mirror_model) for (auto &l : model_leds) { l.pos.x = -l.pos.x; l.dir.x = -l.dir.x; } t_constellation_led_model model{}; model.id = 3; model.leds = model_leds.data(); model.num_leds = model_leds.size(); t_constellation_led_model owner{}; owner.id = 2; owner.leds = truth_leds.data(); owner.num_leds = truth_leds.size(); Rig rig(2); constellation_constrained_config config; constellation_constrained_default_config(&config); config.time_budget_us = 200000; if (recovered_minimum_only) { config.minimum_cold_inliers = 3; config.minimum_cold_distinct_leds = 0; config.maximum_cold_unexplained_blobs = 1000; config.bearing_noise_std_rad = 0; } MonteCarlo stats; while (stats.trials < trials) { xrt_pose truth = XRT_POSE_IDENTITY; truth.orientation = rotation(3.14159f * positive(rng), {unit(rng), unit(rng), unit(rng)}); float depth = 0.25f + 0.45f * positive(rng); truth.position = {unit(rng) * 0.4f * depth, unit(rng) * 0.3f * depth, depth}; rig.clear(); std::vector> visible; for (unsigned k = 0; k < 2; k++) for (unsigned l = 0; l < truth_leds.size(); l++) { float x, y; if (rig.project(k, truth, truth_leds[l], x, y, std::cos(70 * M_PI / 180))) visible.push_back({k, l}); } if (visible.size() < 3) continue; std::shuffle(visible.begin(), visible.end(), rng); unsigned keep = std::min(visible.size(), 3 + rng() % 4); visible.resize(keep); std::array, 2> truth_led; for (auto [k, l] : visible) { float x, y; rig.project(k, truth, truth_leds[l], x, y, -1); rig.add_blob(k, x + noise(rng), y + noise(rng)); truth_led[k].push_back(int(l)); } for (unsigned k = 0; k < 2; k++) for (unsigned d = 0; d < distractors; d++) { rig.add_blob(k, 640 * positive(rng), 480 * positive(rng)); truth_led[k].push_back(-1); } stats.trials++; xrt_pose prior = world_tilt_prior(truth.orientation, 6.28 * positive(rng), 0.015, rng); if (independent_tilt) prior.orientation = rotation(3.14159f * positive(rng), {unit(rng), unit(rng), unit(rng)}); constellation_constrained_result result; bool ok = constellation_constrained_search(&model, rig.views.data(), 2, &prior, false, false, &config, &result); if (owner_contests) { // The ring's own controller searches the same exposure with its own tilt. xrt_pose owner_prior = world_tilt_prior(truth.orientation, 6.28 * positive(rng), 0.015, rng); constellation_constrained_result owner_result; bool owner_ok = constellation_constrained_search(&owner, rig.views.data(), 2, &owner_prior, false, false, &config, &owner_result); const constellation_constrained_result *claims[2] = {&result, &owner_result}; bool published[2] = {false, false}, candidates[2] = {ok, owner_ok}; constellation_constrained_claim blocked[2]; constellation_constrained_arbitrate(claims, published, candidates, blocked, 2, 2); if (ok && !candidates[0]) result.rejection = CONSTELLATION_CONSTRAINED_SHARED_BLOB; ok = candidates[0]; } if (!ok) { stats.rejected++; stats.reasons[result.rejection]++; continue; } bool identity = true; for (unsigned i = 0; i < result.num_assignments; i++) { const auto &a = result.assignments[i]; identity &= truth_led[a.camera_index][a.blob_index] == int(a.led_index); } double error = distance(result.P_world_model.position, truth.position); double sigma = 0; for (unsigned i = 0; i < 3; i++) sigma += result.covariance[5 * i]; sigma = std::sqrt(sigma); if (!identity) { stats.wrong++; stats.worst_error_m = std::max(stats.worst_error_m, error); } else { stats.accepted++; stats.worst_identity_error_sigma = std::max(stats.worst_identity_error_sigma, error / sigma); stats.worst_identity_error_m = std::max(stats.worst_identity_error_m, error); } } return stats; } struct Scenario { float depth_min = 0.25f, depth_max = 0.70f; //! IMU tilt error of the prior, radians. double tilt_error = 0.015; //! Trusted prior: position within prior_error_m of the truth, yaw known within heading_error. bool trusted = false; float prior_error_m = 0.03f; bool heading = false; double heading_error = 0.03; unsigned min_keep = 3, max_keep = 6; float pixel_noise = 0.3f; }; /* Like run_cold_acquisition for the owner's own model, over a chosen depth range and IMU error. */ MonteCarlo run_scenario(const Scenario &sc, unsigned trials, uint32_t seed, const constellation_constrained_config *override = nullptr) { std::mt19937 rng(seed); std::uniform_real_distribution unit(-1, 1), positive(0, 1); std::normal_distribution noise(0, sc.pixel_noise); auto leds = rift_s_ring_leds; t_constellation_led_model model{}; model.id = 3; model.leds = leds.data(); model.num_leds = leds.size(); Rig rig(2); constellation_constrained_config config; constellation_constrained_default_config(&config); config.time_budget_us = 200000; if (override) config = *override; MonteCarlo stats; while (stats.trials < trials) { xrt_pose truth = XRT_POSE_IDENTITY; truth.orientation = rotation(3.14159f * positive(rng), {unit(rng), unit(rng), unit(rng)}); float depth = sc.depth_min + (sc.depth_max - sc.depth_min) * positive(rng); truth.position = {unit(rng) * 0.4f * depth, unit(rng) * 0.3f * depth, depth}; rig.clear(); std::vector> visible; for (unsigned k = 0; k < 2; k++) for (unsigned l = 0; l < leds.size(); l++) { float x, y; if (rig.project(k, truth, leds[l], x, y, std::cos(70 * M_PI / 180))) visible.push_back({k, l}); } if (visible.size() < sc.min_keep) continue; std::shuffle(visible.begin(), visible.end(), rng); unsigned keep = std::min(visible.size(), sc.min_keep + rng() % (sc.max_keep - sc.min_keep + 1)); visible.resize(keep); std::array, 2> truth_led; for (auto [k, l] : visible) { float x = 0, y = 0; rig.project(k, truth, leds[l], x, y, -1); rig.add_blob(k, x + noise(rng), y + noise(rng)); truth_led[k].push_back(int(l)); } stats.trials++; double yaw = sc.heading ? sc.heading_error * unit(rng) : 6.28 * positive(rng); xrt_pose prior = world_tilt_prior(truth.orientation, yaw, sc.tilt_error, rng); if (sc.trusted) { xrt_vec3 dir = {unit(rng), unit(rng), unit(rng)}; math_vec3_normalize(&dir); prior.position = {truth.position.x + sc.prior_error_m * dir.x, truth.position.y + sc.prior_error_m * dir.y, truth.position.z + sc.prior_error_m * dir.z}; } constellation_constrained_result result; if (!constellation_constrained_search(&model, rig.views.data(), 2, &prior, sc.heading, sc.trusted, &config, &result)) { stats.rejected++; stats.reasons[result.rejection]++; continue; } bool identity = true; for (unsigned i = 0; i < result.num_assignments; i++) { const auto &a = result.assignments[i]; identity &= truth_led[a.camera_index][a.blob_index] == int(a.led_index); } double error = distance(result.P_world_model.position, truth.position); if (!identity) { stats.wrong++; stats.worst_error_m = std::max(stats.worst_error_m, error); } else { stats.accepted++; stats.worst_identity_error_m = std::max(stats.worst_identity_error_m, error); } } return stats; } void print(const char *name, const MonteCarlo &s) { std::printf("%s: trials=%u correct_identity=%u wrong_identity=%u rejected=%u worst_wrong_m=%.4f " "worst_correct_m=%.4f worst_correct_sigma=%.2f reasons:", name, s.trials, s.accepted, s.wrong, s.rejected, s.worst_error_m, s.worst_identity_error_m, s.worst_identity_error_sigma); for (unsigned r = 0; r < s.reasons.size(); r++) if (s.reasons[r]) std::printf(" %s=%u", constellation_constrained_rejection_name((constellation_constrained_rejection)r), s.reasons[r]); std::printf("\n"); } } // namespace TEST_CASE("Recorded Rift S frame acquires through the gravity two-point path") { auto leds = rift_s_ring_leds; t_constellation_led_model model{}; model.id = 3; model.leds = leds.data(); model.num_leds = leds.size(); Rig rig(1); for (const auto &b : rift_s_ring_blobs) rig.add_blob(0, b[0], b[1]); // Camera gravity from the recording; world +Y must map onto it. xrt_vec3 up = {0, 1, 0}, gravity = rift_s_ring_gravity; math_quat_from_vec_a_to_vec_b(&up, &gravity, &rig.views[0].P_cam_world.orientation); rig.views[0].P_cam_world.position = {0, 0, 0}; math_pose_invert(&rig.views[0].P_cam_world, &rig.views[0].P_world_cam); xrt_pose prior_cam = XRT_POSE_IDENTITY; math_quat_from_vec_a_to_vec_b(&rift_s_ring_down, &gravity, &prior_cam.orientation); xrt_pose prior; math_pose_transform(&rig.views[0].P_world_cam, &prior_cam, &prior); prior.position = {42, 42, 42}; constellation_constrained_config config; constellation_constrained_default_config(&config); config.time_budget_us = 2000000; constellation_constrained_result result; bool acquired = constellation_constrained_search(&model, rig.views.data(), 1, &prior, false, false, &config, &result); INFO("rejection " << constellation_constrained_rejection_name(result.rejection) << " inliers " << result.inliers << " unexplained " << result.unexplained_blobs << " chi2 " << result.fit_chi_square << "/" << result.fit_chi_square_limit); REQUIRE(acquired); CHECK(result.origin == CONSTELLATION_CONSTRAINED_GRAVITY); CHECK(result.inliers == 5); CHECK(result.distinct_leds == 5); CHECK(result.vision_determines_yaw); xrt_pose cam_model; math_pose_transform(&rig.views[0].P_cam_world, &result.P_world_model, &cam_model); // The independent P3P search in tests_constellation finds the same ring at 0.220 m. This fit keeps // the measured accelerometer tilt and minimizes object-space ray distance, so allow a few mm. CHECK(cam_model.position.z == Catch::Approx(0.220).margin(0.008)); CHECK(result.cost_m < config.score_gate_m); CHECK_FALSE(result.budget_exhausted); // Upside-down tilt has no consistent solution. xrt_vec3 opposite = {-rift_s_ring_down.x, -rift_s_ring_down.y, -rift_s_ring_down.z}; math_quat_from_vec_a_to_vec_b(&opposite, &gravity, &prior_cam.orientation); math_pose_transform(&rig.views[0].P_world_cam, &prior_cam, &prior); CHECK_FALSE( constellation_constrained_search(&model, rig.views.data(), 1, &prior, false, false, &config, &result)); // The mirrored model must not explain the left ring. for (auto &l : leds) { l.pos.x = -l.pos.x; l.dir.x = -l.dir.x; } math_quat_from_vec_a_to_vec_b(&rift_s_ring_down, &gravity, &prior_cam.orientation); math_pose_transform(&rig.views[0].P_world_cam, &prior_cam, &prior); CHECK_FALSE( constellation_constrained_search(&model, rig.views.data(), 1, &prior, false, false, &config, &result)); } TEST_CASE("Cold low-count acquisition never accepts a wrong pose in randomized partial-ring views") { MonteCarlo clean = run_cold_acquisition(false, 0, 300, 0x5eed1); print("clean", clean); CHECK(clean.wrong == 0); CHECK(clean.accepted > clean.trials / 2); CHECK(clean.worst_identity_error_sigma < 4); // Background lights: an identity slip is tolerated only when it leaves the pose within 1 cm. MonteCarlo cluttered = run_cold_acquisition(false, 4, 300, 0x5eed2); print("cluttered", cluttered); CHECK(cluttered.wrong <= cluttered.trials / 100); CHECK(cluttered.worst_error_m < 0.01); CHECK(cluttered.accepted > cluttered.trials / 3); // The other hand's (mirrored) model on this ring. Few observations of a self-similar ring cannot // always separate handedness geometrically, so most fits must be rejected on their own and the // rest by blob ownership once the ring's own controller searches the same exposure. MonteCarlo same_tilt = run_cold_acquisition(true, 2, 300, 0x5eed3); print("other-hand-same-tilt", same_tilt); CHECK(same_tilt.wrong <= same_tilt.trials / 4); MonteCarlo other_hand = run_cold_acquisition(true, 2, 300, 0x5eed4, true); print("other-hand-independent-tilt", other_hand); CHECK(other_hand.wrong <= other_hand.trials / 10); MonteCarlo arbitrated = run_cold_acquisition(true, 2, 300, 0x5eed5, true, true); print("other-hand-after-arbitration", arbitrated); CHECK(arbitrated.wrong == 0); MonteCarlo arbitrated_same_tilt = run_cold_acquisition(true, 2, 300, 0x5eed6, false, true); print("other-hand-same-tilt-after-arbitration", arbitrated_same_tilt); CHECK(arbitrated_same_tilt.wrong == 0); } TEST_CASE("Recovered minimum count alone admits wrong identities that the cold policy rejects") { // Documents why the cold policy exists: inliers > sample size is necessary, not sufficient. constellation_constrained_config minimum; constellation_constrained_default_config(&minimum); CHECK(minimum.minimum_cold_inliers == 4); MonteCarlo strict = run_cold_acquisition(false, 4, 200, 0x5eed7); MonteCarlo relaxed = run_cold_acquisition(false, 4, 200, 0x5eed7, false, false, true); print("cluttered-recovered-minimum-only", relaxed); CHECK(relaxed.wrong > strict.wrong); CHECK(strict.wrong <= 2); } namespace { struct RingScene { std::array leds = rift_s_ring_leds; t_constellation_led_model model{}; Rig rig; xrt_pose truth = XRT_POSE_IDENTITY; std::array, CONSTELLATION_CONSTRAINED_MAX_VIEWS> truth_led; constellation_constrained_config config; explicit RingScene(unsigned cameras) : rig(cameras) { model.id = 3; model.leds = leds.data(); model.num_leds = leds.size(); constellation_constrained_default_config(&config); config.time_budget_us = 2000000; } unsigned observe(unsigned camera, unsigned max_leds) { unsigned n = 0; for (unsigned l = 0; l < leds.size() && n < max_leds; l++) { float x, y; if (!rig.project(camera, truth, leds[l], x, y, std::cos(70 * M_PI / 180))) continue; rig.add_blob(camera, x, y); truth_led[camera].push_back(int(l)); n++; } return n; } //! Deterministically pick an orientation where each camera sees at least need[k] LEDs. void choose_pose(xrt_vec3 position, std::initializer_list need, uint32_t seed) { std::mt19937 rng(seed); std::uniform_real_distribution unit(-1, 1); truth.position = position; for (unsigned attempt = 0; attempt < 10000; attempt++) { truth.orientation = rotation(3.14159f * (unit(rng) + 1) / 2, {unit(rng), unit(rng), unit(rng)}); unsigned k = 0; bool ok = true; for (unsigned n : need) { unsigned seen = 0; for (const auto &led : leds) { float x, y; seen += rig.project(k, truth, led, x, y, std::cos(70 * M_PI / 180)); } ok &= seen >= n; k++; } if (ok) return; } FAIL("no orientation satisfies the visibility request"); } bool identity(const constellation_constrained_result &r) const { for (unsigned i = 0; i < r.num_assignments; i++) if (truth_led[r.assignments[i].camera_index][r.assignments[i].blob_index] != int(r.assignments[i].led_index)) return false; return r.num_assignments > 0; } }; } // namespace TEST_CASE("Trusted prior reacquires after hand motion with the tracking minimum count") { RingScene s(2); // This fixture checks full reacquisition, including every competing prior pair. s.config.max_prior_pair_trials = 10000; s.choose_pose({0.03f, 0.06f, 0.42f}, {4}, 1); REQUIRE(s.observe(0, 3) == 3); xrt_pose prior = s.truth; prior.position.x += 0.05f; // Held optical position lags a moving hand. prior.position.z -= 0.03f; constellation_constrained_result result; // Three observations from one camera no longer publish, even with a trusted prior. CHECK_FALSE( constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, true, &s.config, &result)); CHECK(result.rejection == CONSTELLATION_CONSTRAINED_TOO_FEW_INLIERS); CHECK(result.inliers == 3); // Four observations of distinct LEDs meet the tracking minimum. s.rig.observations[0].num_blobs = 0; s.truth_led[0].clear(); REQUIRE(s.observe(0, 4) == 4); REQUIRE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, true, &s.config, &result)); CHECK(s.identity(result)); CHECK(result.inliers == 4); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.005); // With heading, the same frame also yields translation hypotheses. REQUIRE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, true, true, &s.config, &result)); CHECK(result.heading_hypotheses > 0); CHECK(s.identity(result)); // A prior far from the ring finds no edges inside the broad association gate. prior.position.x += 0.5f; CHECK_FALSE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, true, true, &s.config, &result)); CHECK(result.pair_trials == 0); } TEST_CASE("Joint camera result does not depend on camera iteration order") { RingScene s(3); s.choose_pose({0.0f, 0.05f, 0.45f}, {2, 2, 2}, 2); unsigned total = 0; for (unsigned k = 0; k < 3; k++) total += s.observe(k, 2); REQUIRE(total >= 4); std::mt19937 rng(9); xrt_pose prior = world_tilt_prior(s.truth.orientation, 2.0, 0, rng); constellation_constrained_result reference; REQUIRE(constellation_constrained_search(&s.model, s.rig.views.data(), 3, &prior, false, false, &s.config, &reference)); CHECK(s.identity(reference)); std::array order = {0, 1, 2}; unsigned permutations = 0; do { std::vector views; for (unsigned k : order) views.push_back(s.rig.views[k]); constellation_constrained_result result; REQUIRE(constellation_constrained_search(&s.model, views.data(), 3, &prior, false, false, &s.config, &result)); CHECK(distance(result.P_world_model.position, reference.P_world_model.position) < 1e-6); CHECK(angle(result.P_world_model.orientation, reference.P_world_model.orientation) < 1e-5); CHECK(result.cost_m == Catch::Approx(reference.cost_m).margin(1e-12)); CHECK(result.inliers == reference.inliers); CHECK(result.pair_trials == reference.pair_trials); for (unsigned i = 0; i < result.num_assignments; i++) { CHECK(result.assignments[i].camera_index == reference.assignments[i].camera_index); CHECK(result.assignments[i].blob_index == reference.assignments[i].blob_index); CHECK(result.assignments[i].led_index == reference.assignments[i].led_index); } permutations++; } while (std::next_permutation(order.begin(), order.end())); CHECK(permutations == 6); } TEST_CASE("Cross-controller arbitration resolves blob ownership before fusion") { constellation_constrained_result left{}, right{}, idle{}; left.num_assignments = right.num_assignments = 2; left.assignments[0] = {0, 4, 1, 1, 0}; left.assignments[1] = {1, 7, 2, 2, 0}; right.assignments[0] = {0, 5, 3, 3, 0}; right.assignments[1] = {1, 7, 4, 4, 0}; // Same blob 7 in camera 1. left.inliers = right.inliers = 5; left.distinct_leds = right.distinct_leds = 4; const constellation_constrained_result *claims[3] = {&left, &right, &idle}; bool published[3] = {false, false, false}, candidates[3] = {true, true, false}; constellation_constrained_claim blocked[3]; // Equal support: neither candidate can own the blob. constellation_constrained_arbitrate(claims, published, candidates, blocked, 3, 2); CHECK_FALSE(candidates[0]); CHECK_FALSE(candidates[1]); CHECK(blocked[0] == CONSTELLATION_CLAIM_CANDIDATE); CHECK(blocked[1] == CONSTELLATION_CLAIM_CANDIDATE); CHECK(blocked[2] == CONSTELLATION_CLAIM_NONE); // Clearly more support keeps the blob, independent of device order. for (bool reversed : {false, true}) { left.inliers = 7; left.distinct_leds = 5; const constellation_constrained_result *ordered[2] = {reversed ? &right : &left, reversed ? &left : &right}; bool pub[2] = {false, false}, cand[2] = {true, true}; constellation_constrained_claim why[2]; constellation_constrained_arbitrate(ordered, pub, cand, why, 2, 2); CHECK(cand[reversed ? 1 : 0]); CHECK_FALSE(cand[reversed ? 0 : 1]); } left.inliers = 5; left.distinct_leds = 4; // A published fast-path result keeps its blobs; only the new claim loses. published[0] = true; candidates[0] = false; candidates[1] = true; constellation_constrained_arbitrate(claims, published, candidates, blocked, 3, 2); CHECK(blocked[0] == CONSTELLATION_CLAIM_NONE); CHECK(blocked[1] == CONSTELLATION_CLAIM_PUBLISHED); CHECK_FALSE(candidates[1]); // The published pose keeps the blob this exposure, but a clearly better-supported candidate // contests it so its track can end; an equal one does not. CHECK_FALSE(constellation_constrained_contests_published(&right, &left, 2)); right.inliers = 7; right.distinct_leds = 5; CHECK(constellation_constrained_contests_published(&right, &left, 2)); CHECK_FALSE(constellation_constrained_contests_published(&right, &idle, 2)); right.inliers = 5; right.distinct_leds = 4; // A rejected hypothesis contests only when its fit was consistent and at least as supported. published[0] = false; for (auto reason : {CONSTELLATION_CONSTRAINED_INCONSISTENT, CONSTELLATION_CONSTRAINED_NO_PAIR, CONSTELLATION_CONSTRAINED_TOO_FEW_INLIERS, CONSTELLATION_CONSTRAINED_AMBIGUOUS}) { for (unsigned inliers : {4u, 5u}) { left.rejection = reason; left.inliers = inliers; candidates[0] = false; candidates[1] = true; constellation_constrained_arbitrate(claims, published, candidates, blocked, 3, 2); bool competitive = reason == CONSTELLATION_CONSTRAINED_TOO_FEW_INLIERS || reason == CONSTELLATION_CONSTRAINED_AMBIGUOUS; bool contests = competitive && inliers >= right.inliers; CHECK(candidates[1] == !contests); CHECK((blocked[1] == CONSTELLATION_CLAIM_REJECTED) == contests); } } // No overlap, no conflict; an empty rejected result claims nothing. left.inliers = 5; right.assignments[1].blob_index = 8; candidates[0] = candidates[1] = true; constellation_constrained_arbitrate(claims, published, candidates, blocked, 3, 2); CHECK(candidates[0]); CHECK(candidates[1]); left.num_assignments = 0; right.assignments[1].blob_index = 7; candidates[0] = false; candidates[1] = true; constellation_constrained_arbitrate(claims, published, candidates, blocked, 3, 2); CHECK(candidates[1]); } TEST_CASE("Motion continuity rejects identity jumps and rotations the IMU did not see") { constellation_motion_limits limits; constellation_constrained_default_motion_limits(&limits); constellation_track_point a{}, b{}; a.P_world_model = XRT_POSE_IDENTITY; a.P_world_model.position = {0.1f, -0.2f, 0.5f}; a.imu_world = rotation(0.3, {0, 1, 0}); a.P_world_model.orientation = rotation(1.0, {1, 0.2f, 0}); a.capture_ns = 1000000000; b = a; b.capture_ns = a.capture_ns + 33000000; // one controller exposure later double excess, rotation_error; CHECK(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); CHECK(rotation_error < 1e-4); // A fast hand: 2.5 m/s for 33 ms is fine, a 30 cm hop is not. b.P_world_model.position.x += 0.08f; CHECK(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); b.P_world_model.position.x += 0.22f; CHECK_FALSE(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); CHECK(excess > 0.1); b.P_world_model.position = a.P_world_model.position; // Optics and IMU turning together is consistent; optics turning alone is a wrong identity. xrt_quat turn = rotation(0.5, {0, 0, 1}); b.P_world_model.orientation = multiply(a.P_world_model.orientation, turn); CHECK_FALSE(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); CHECK(rotation_error == Catch::Approx(0.5).margin(1e-4)); b.imu_world = multiply(a.imu_world, turn); CHECK(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); // The IMU's world yaw lags the optical one (the driver corrects 10% of a large error per // pose). A 40 degree wrist pitch then looks like an error of tens of degrees in world coordinates, but // the controller turned by the same amount in its own frame. xrt_quat optical = rotation(0.4, {0.3f, 1, 0.2f}); xrt_quat yaw_offset = rotation(70 * M_PI / 180, {0, 1, 0}); xrt_quat pitch = rotation(40 * M_PI / 180, {1, 0, 0}); a.P_world_model.orientation = optical; a.imu_world = multiply(yaw_offset, optical); b.P_world_model.orientation = multiply(optical, pitch); b.imu_world = multiply(a.imu_world, pitch); CHECK(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); CHECK(rotation_error < 1e-4); xrt_quat world_optical = multiply(b.P_world_model.orientation, conjugate(a.P_world_model.orientation)); xrt_quat world_imu = multiply(b.imu_world, conjugate(a.imu_world)); CHECK(angle(world_optical, world_imu) > 25 * M_PI / 180); // Stale references and time running backwards never vouch for a pose. b.capture_ns = a.capture_ns + 400000000; CHECK_FALSE(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); b.capture_ns = a.capture_ns - 1; CHECK_FALSE(constellation_constrained_motion_consistent(&a, &b, &limits, &excess, &rotation_error)); } namespace { constellation_track_point track_point(xrt_vec3 position, xrt_quat optical, xrt_quat imu, int64_t capture_ns) { constellation_track_point p{}; p.P_world_model.position = position; p.P_world_model.orientation = optical; p.imu_world = imu; p.capture_ns = capture_ns; return p; } } // namespace TEST_CASE("Track publishes only confirmed, continuous results") { constellation_track track; constellation_track_init(&track); const int64_t frame = 33000000; xrt_quat q = rotation(0.7, {0.2f, 1, 0}); xrt_vec3 p = {0.1f, -0.3f, 0.5f}; auto at = [&](int k, xrt_vec3 position, xrt_quat optical, bool observed = true) { constellation_track_point next = track_point(position, optical, q, k * frame); xrt_quat ref_imu = q; return constellation_track_update(&track, &next, observed, &ref_imu, nullptr, nullptr); }; // A cold acquisition alone is never published. CHECK(at(1, p, q) == CONSTELLATION_TRACK_HOLD_CONFIRMING); CHECK(track.state == CONSTELLATION_TRACK_CONFIRMING); // A wrong identity cannot confirm it: it replaces the held result instead. xrt_vec3 off = {p.x + 0.15f, p.y, p.z}; CHECK(at(2, off, multiply(rotation(0.5, {0, 0, 1}), q)) == CONSTELLATION_TRACK_HOLD_UNCONFIRMED); CHECK(at(3, p, q) == CONSTELLATION_TRACK_HOLD_UNCONFIRMED); CHECK(at(4, p, q) == CONSTELLATION_TRACK_PUBLISH); CHECK(track.state == CONSTELLATION_TRACK_TRACKING); // One wrong result while tracking is held and does not move the reference. CHECK(at(5, off, q) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(track.ref.capture_ns == 4 * frame); CHECK(at(6, p, q) == CONSTELLATION_TRACK_PUBLISH); // An optical flip the gyro did not see is a jump even at the same position. CHECK(at(7, p, multiply(rotation(M_PI, {1, 0, 0}), q)) == CONSTELLATION_TRACK_HOLD_JUMP); // A position-only fit echoes the IMU, so its (unobserved) orientation is not tested. CHECK(at(8, p, rotation(2.0, {0, 1, 0}), false) == CONSTELLATION_TRACK_PUBLISH); // Repeated disagreement ends the track; the latest result must then confirm itself. xrt_vec3 far = {p.x - 0.4f, p.y, p.z}; CHECK(at(9, far, q) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(at(10, far, q) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(at(11, far, q) == CONSTELLATION_TRACK_HOLD_RESTART); CHECK(track.state == CONSTELLATION_TRACK_CONFIRMING); CHECK(at(12, far, q) == CONSTELLATION_TRACK_PUBLISH); // Stale and gap handling. CHECK(at(12, far, q) == CONSTELLATION_TRACK_HOLD_STALE); CHECK_FALSE(constellation_track_expire(&track, 12 * frame + 300000000)); CHECK(constellation_track_expire(&track, 12 * frame + 300000001)); CHECK(track.state == CONSTELLATION_TRACK_LOST); CHECK(at(30, off, q) == CONSTELLATION_TRACK_HOLD_CONFIRMING); CHECK(at(31, off, q) == CONSTELLATION_TRACK_PUBLISH); constellation_track_reset(&track); CHECK(track.state == CONSTELLATION_TRACK_LOST); CHECK(at(32, off, q) == CONSTELLATION_TRACK_HOLD_CONFIRMING); // Without the reference attitude nothing vouches for the next result. constellation_track_point next = track_point(off, q, q, 33 * frame); CHECK(constellation_track_update(&track, &next, true, nullptr, nullptr, nullptr) == CONSTELLATION_TRACK_HOLD_UNCONFIRMED); } TEST_CASE("Track continuity compares IMU attitudes read after a world-yaw correction") { constellation_track track; constellation_track_init(&track); const int64_t frame = 33000000; xrt_quat optical0 = rotation(0.3, {1, 0, 0}), turn = rotation(0.05, {0, 1, 1}); xrt_quat optical1 = multiply(turn, optical0); // The IMU heading is 30 degrees off until the published optical yaw corrects its history. xrt_quat yaw_error = rotation(30 * M_PI / 180, {0, 1, 0}); xrt_vec3 p = {0, 0, 0.6f}; constellation_track_point first = track_point(p, optical0, multiply(yaw_error, optical0), frame); xrt_quat stale_ref = first.imu_world; CHECK(constellation_track_update(&track, &first, true, nullptr, nullptr, nullptr) == CONSTELLATION_TRACK_HOLD_CONFIRMING); constellation_track_point second = track_point(p, optical1, multiply(yaw_error, optical1), 2 * frame); CHECK(constellation_track_update(&track, &second, true, &stale_ref, nullptr, nullptr) == CONSTELLATION_TRACK_PUBLISH); // Publishing applies the correction to the whole history: both reads now agree with optics. xrt_quat optical2 = multiply(turn, optical1); constellation_track_point third = track_point(p, optical2, optical2, 3 * frame); double excess, rotation_error; xrt_quat uncorrected = multiply(yaw_error, optical1); constellation_track copy = track; CHECK(constellation_track_update(©, &third, true, &uncorrected, &excess, &rotation_error) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(rotation_error == Catch::Approx(30 * M_PI / 180).margin(1e-3)); CHECK(constellation_track_update(&track, &third, true, &optical1, &excess, &rotation_error) == CONSTELLATION_TRACK_PUBLISH); CHECK(rotation_error < 1e-3); } TEST_CASE("Track changes yaw observation source without comparing different world headings") { const int64_t frame = 33000000; const xrt_vec3 position = {0, 0, 0.6f}; const xrt_quat optical = rotation(0.3, {1, 0, 0}); const xrt_quat imu = multiply(rotation(0.7, {0, 1, 0}), optical); constellation_track track; constellation_track_init(&track); auto update = [&](int k, bool observed, xrt_vec3 p = {0, 0, 0.6f}) { auto next = track_point(p, optical, imu, k * frame); return constellation_track_update(&track, &next, observed, &imu, nullptr, nullptr); }; CHECK(update(1, true) == CONSTELLATION_TRACK_HOLD_CONFIRMING); CHECK(update(2, true) == CONSTELLATION_TRACK_PUBLISH); CHECK(update(3, false) == CONSTELLATION_TRACK_PUBLISH); CHECK(update(4, true) == CONSTELLATION_TRACK_PUBLISH); CHECK(update(5, false) == CONSTELLATION_TRACK_PUBLISH); // Changing yaw sources must not hide a positional identity slip. CHECK(update(6, true, {0.4f, 0, 0.6f}) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(update(7, true) == CONSTELLATION_TRACK_PUBLISH); auto flipped = track_point(position, multiply(rotation(M_PI, {1, 0, 0}), optical), imu, 8 * frame); CHECK(constellation_track_update(&track, &flipped, true, &imu, nullptr, nullptr) == CONSTELLATION_TRACK_HOLD_JUMP); } TEST_CASE("Track never publishes randomized wrong identities on a moving hand") { std::mt19937 rng(0x7ac4); std::uniform_real_distribution unit(-1, 1), positive(0, 1); constellation_track track; constellation_track_init(&track); const int64_t frame = 16666667; xrt_vec3 p = {0, 0, 0.5f}, v = {0, 0, 0}; xrt_quat q = XRT_QUAT_IDENTITY; std::vector imu_history; unsigned published = 0, published_wrong = 0, correct_results = 0, wrong_results = 0, dropped = 0; for (int k = 1; k < 6000; k++) { // A hand moving up to about 2 m/s and turning up to about 6 rad/s. v = {0.9f * v.x + 0.4f * unit(rng), 0.9f * v.y + 0.4f * unit(rng), 0.9f * v.z + 0.4f * unit(rng)}; float speed = std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); if (speed > 2) v = {v.x * 2 / speed, v.y * 2 / speed, v.z * 2 / speed}; p = {p.x + v.x * 0.016667f, p.y + v.y * 0.016667f, p.z + v.z * 0.016667f}; q = multiply(rotation(0.1 * positive(rng), {unit(rng), unit(rng), unit(rng)}), q); imu_history.push_back(q); float draw = positive(rng); if (draw < 0.3f) { dropped++; constellation_track_expire(&track, k * frame); continue; } bool wrong = draw < 0.6f; xrt_vec3 position = p; xrt_quat orientation = q; if (wrong) { // Identity slips: one LED spacing or more and a rotation about an arbitrary axis. float d = 0.04f + 0.2f * positive(rng); xrt_vec3 dir = {unit(rng), unit(rng), unit(rng)}; math_vec3_normalize(&dir); position = {p.x + d * dir.x, p.y + d * dir.y, p.z + d * dir.z}; orientation = multiply(rotation(0.35 + 2.8 * positive(rng), {unit(rng), unit(rng), unit(rng)}), q); wrong_results++; } else { correct_results++; } constellation_track_expire(&track, k * frame); constellation_track_point next = track_point(position, orientation, q, k * frame); const xrt_quat *ref_imu = nullptr; if (track.state != CONSTELLATION_TRACK_LOST) ref_imu = &imu_history[track.ref.capture_ns / frame - 1]; if (constellation_track_update(&track, &next, true, ref_imu, nullptr, nullptr) == CONSTELLATION_TRACK_PUBLISH) { published++; published_wrong += wrong; } } printf("track: correct_results=%u wrong_results=%u dropped=%u published=%u published_wrong=%u\n", correct_results, wrong_results, dropped, published, published_wrong); CHECK(published_wrong == 0); CHECK(published > correct_results * 3 / 4); } TEST_CASE("Known-orientation translation stays bounded near the bearing-separation limit") { std::mt19937 rng(17); std::normal_distribution noise(0, 1e-5); const xrt_vec3 points[2] = {{0, 0, 0}, {0.004f, 0.001f, 0.0f}}; xrt_quat q = rotation(0.3, {0.2f, 1, 0.1f}); for (double depth : {0.3, 0.8, 1.6}) { xrt_pose truth = {q, {0.01f, -0.02f, float(depth)}}; xrt_vec3 bearings[2]; for (unsigned i = 0; i < 2; i++) { math_pose_transform_point(&truth, &points[i], &bearings[i]); math_vec3_normalize(&bearings[i]); } double separation = std::acos(std::clamp(m_vec3_dot(bearings[0], bearings[1]), -1, 1)); enum constellation_constrained_solver_status status; xrt_pose out; bool ok = constellation_constrained_heading_two_point(points, bearings, &q, 0.001745329, &out, &status); if (separation < 0.001745329) { CHECK_FALSE(ok); CHECK(status == CONSTELLATION_SOLVER_DEGENERATE_BEARINGS); continue; } REQUIRE(ok); CHECK(distance(out.position, truth.position) < 1e-4 * depth); // Perturbed bearings must give a finite answer whose error scales with depth/separation. for (unsigned trial = 0; trial < 50; trial++) { xrt_vec3 noisy[2] = {bearings[0], bearings[1]}; for (auto &b : noisy) { b.x += noise(rng); b.y += noise(rng); math_vec3_normalize(&b); } if (!constellation_constrained_heading_two_point(points, noisy, &q, 0.001745329, &out, &status)) continue; CHECK(std::isfinite(out.position.z)); CHECK(distance(out.position, truth.position) < 10 * 1e-5 * depth / separation + 1e-4); } } } TEST_CASE("Dense clutter exhausts the bounded budget instead of stalling, and adjacency finds the ring first") { RingScene s(2); s.choose_pose({-0.02f, 0.04f, 0.4f}, {4, 2}, 3); REQUIRE(s.observe(0, 4) + s.observe(1, 2) >= 5); std::mt19937 rng(3); std::uniform_real_distribution unit(0, 1); for (unsigned k = 0; k < 2; k++) for (unsigned d = 0; d < 25; d++) { s.rig.add_blob(k, 640 * unit(rng), 480 * unit(rng)); s.truth_led[k].push_back(-1); } xrt_pose prior = world_tilt_prior(s.truth.orientation, 1.0, 0, rng); constellation_constrained_result result; s.config.max_pair_trials = 4000; bool ok = constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, false, &s.config, &result); CHECK(result.budget_exhausted); CHECK(result.pair_trials == 4000); if (ok) CHECK(s.identity(result)); s.config.time_budget_us = 1; CHECK_FALSE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, false, &s.config, &result)); CHECK(result.budget_exhausted); CHECK(result.pair_trials <= 1); } TEST_CASE("Two controllers in one exposure acquire their own rings and never swap") { std::mt19937 rng(0x7a11); std::uniform_real_distribution unit(-1, 1), positive(0, 1); std::normal_distribution noise(0, 0.3f); auto left_leds = rift_s_ring_leds, right_leds = rift_s_ring_leds; for (auto &l : right_leds) { l.pos.x = -l.pos.x; l.dir.x = -l.dir.x; } t_constellation_led_model models[2] = {}; models[0].id = 3; models[0].leds = left_leds.data(); models[0].num_leds = left_leds.size(); models[1].id = 2; models[1].leds = right_leds.data(); models[1].num_leds = right_leds.size(); Rig rig(2); constellation_constrained_config config; constellation_constrained_default_config(&config); config.time_budget_us = 200000; unsigned trials = 0, acquired[2] = {0, 0}, wrong = 0; while (trials < 150) { xrt_pose truth[2]; std::array, 2> owner, led_of; rig.clear(); unsigned visible_total[2] = {0, 0}; for (unsigned hand = 0; hand < 2; hand++) { truth[hand] = XRT_POSE_IDENTITY; truth[hand].orientation = rotation(3.14159f * positive(rng), {unit(rng), unit(rng), unit(rng)}); float depth = 0.3f + 0.3f * positive(rng); // Hands side by side, sometimes close together. truth[hand].position = {(hand ? 1 : -1) * (0.03f + 0.12f * positive(rng)), 0.1f * unit(rng), depth}; const auto &leds = hand ? right_leds : left_leds; for (unsigned k = 0; k < 2; k++) for (unsigned l = 0; l < leds.size(); l++) { float x, y; if (!rig.project(k, truth[hand], leds[l], x, y, std::cos(70 * M_PI / 180)) || positive(rng) < 0.3f) // partial occlusion by the hand continue; rig.add_blob(k, x + noise(rng), y + noise(rng)); owner[k].push_back(int(hand)); led_of[k].push_back(int(l)); visible_total[hand]++; } } if (visible_total[0] < 3 && visible_total[1] < 3) continue; trials++; constellation_constrained_result results[2]; bool candidates[2], published[2] = {false, false}; constellation_constrained_claim blocked[2]; for (unsigned hand = 0; hand < 2; hand++) { xrt_pose prior = world_tilt_prior(truth[hand].orientation, 6.28 * positive(rng), 0.015, rng); candidates[hand] = constellation_constrained_search(&models[hand], rig.views.data(), 2, &prior, false, false, &config, &results[hand]); } const constellation_constrained_result *claims[2] = {&results[0], &results[1]}; constellation_constrained_arbitrate(claims, published, candidates, blocked, 2, 2); for (unsigned hand = 0; hand < 2; hand++) { if (!candidates[hand]) continue; bool own = true; for (unsigned i = 0; i < results[hand].num_assignments; i++) { const auto &a = results[hand].assignments[i]; own &= owner[a.camera_index][a.blob_index] == int(hand) && led_of[a.camera_index][a.blob_index] == int(a.led_index); } if (own && distance(results[hand].P_world_model.position, truth[hand].position) < 0.03) acquired[hand]++; else wrong++; } } std::printf("two-hands: trials=%u left_acquired=%u right_acquired=%u wrong=%u\n", trials, acquired[0], acquired[1], wrong); CHECK(wrong == 0); // The mirrored right model must acquire as readily as the left one. CHECK(acquired[0] > trials / 4); CHECK(acquired[1] > trials / 4); } TEST_CASE("Trusted prior search stays near the prior and finds distant rings") { // Arm's length and below the hips: 0.7-1.0 m from the cameras, IMU yaw unknown. The prior // only bounds the search; a truncated, yaw-ordered pair list used to miss the correct pair // and let a hypothesis half a metre away win. Scenario sc; sc.depth_min = 0.7f; sc.depth_max = 1.0f; sc.trusted = true; sc.min_keep = 8; sc.max_keep = 11; MonteCarlo many = run_scenario(sc, 200, 0x1234 + 8); print("trusted-far-many", many); CHECK(many.wrong == 0); CHECK(many.accepted >= 170); sc.min_keep = 4; sc.max_keep = 7; sc.depth_min = 0.3f; sc.depth_max = 0.6f; MonteCarlo few = run_scenario(sc, 200, 0x1234 + 4); print("trusted-near-few", few); CHECK(few.wrong == 0); CHECK(few.accepted >= 150); CHECK(few.worst_identity_error_m < 0.05); } TEST_CASE("Fast-path budget does not turn one solution into an ambiguity") { // The fast path has 3 ms. When the pair search used up the budget, refinement stopped too and // left candidates with identical LED assignments at slightly different poses, which the // ambiguity test then treated as competing identities. constellation_constrained_config fast; constellation_constrained_default_config(&fast); fast.time_budget_us = 3000; #if defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__) // Instrumentation changes throughput. The native build checks the 3 ms fast path. fast.time_budget_us = 200000; #endif Scenario sc; sc.depth_min = 0.7f; sc.depth_max = 1.0f; sc.trusted = true; sc.min_keep = 8; sc.max_keep = 11; MonteCarlo stats = run_scenario(sc, 200, 0x1234 + 8, &fast); print("trusted-far-many-3ms", stats); CHECK(stats.wrong == 0); CHECK(stats.reasons[CONSTELLATION_CONSTRAINED_AMBIGUOUS] <= 20); CHECK(stats.accepted >= 160); } TEST_CASE("Bounded tilt adjustment absorbs an IMU tilt error of a few degrees") { RingScene s(2); s.choose_pose({0.02f, 0.04f, 0.40f}, {5, 4}, 11); REQUIRE(s.observe(0, 6) >= 5); REQUIRE(s.observe(1, 6) >= 4); auto tilted_prior = [&](double degrees) { xrt_pose prior = s.truth; prior.orientation = multiply(rotation(degrees * M_PI / 180, {1, 0, 1}), s.truth.orientation); return prior; }; // Angle between the world gravity directions the two orientations put in the model frame. auto tilt_error = [&](const constellation_constrained_result &r) { xrt_vec3 gravity = {0, 1, 0}, truth, fit; xrt_quat truth_inverse = conjugate(s.truth.orientation), fit_inverse = conjugate(r.P_world_model.orientation); math_quat_rotate_vec3(&truth_inverse, &gravity, &truth); math_quat_rotate_vec3(&fit_inverse, &gravity, &fit); return std::acos(std::clamp(double(m_vec3_dot(truth, fit)), -1.0, 1.0)) * 180 / M_PI; }; constellation_constrained_config fixed = s.config; fixed.tilt_prior_std_rad = 0; constellation_constrained_result result; // 4 degrees off: the fixed-tilt fit keeps the error and absorbs it in its residuals; the // adjusted fit moves most of the way to the true tilt. One small ring observes tilt to about // 2 degrees, so the 3 degree prior keeps part of the error. xrt_pose prior = tilted_prior(4); REQUIRE( constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, false, &fixed, &result)); CHECK(tilt_error(result) == Catch::Approx(4).margin(0.1)); CHECK(result.tilt_correction_rad == 0); double fixed_chi_square = result.fit_chi_square; REQUIRE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, false, &s.config, &result)); CHECK(s.identity(result)); CHECK(tilt_error(result) < 1.5); CHECK(result.tilt_correction_rad * 180 / M_PI > 2.5); CHECK(result.fit_chi_square < fixed_chi_square / 4); CHECK(distance(result.P_world_model.position, s.truth.position) < 0.003); // The adjustment never exceeds its bound, however far off the IMU is. prior = tilted_prior(12); constellation_constrained_search(&s.model, s.rig.views.data(), 2, &prior, false, false, &s.config, &result); CHECK(result.tilt_correction_rad <= s.config.maximum_tilt_correction_rad + 1e-6); // Three distinct LEDs keep the prior tilt. RingScene few(1); few.choose_pose({0.02f, 0.04f, 0.40f}, {3}, 12); REQUIRE(few.observe(0, 3) == 3); xrt_pose few_prior = few.truth; few_prior.orientation = multiply(rotation(2 * M_PI / 180, {1, 0, 1}), few.truth.orientation); few_prior.position.x += 0.01f; few.config.minimum_tracking_inliers = 3; constellation_constrained_search(&few.model, few.rig.views.data(), 1, &few_prior, true, true, &few.config, &result); CHECK(result.num_assignments == 3); CHECK(result.tilt_correction_rad == 0); } TEST_CASE("Tilt adjustment recovers acquisitions lost to IMU tilt error without wrong identities") { constellation_constrained_config fixed; constellation_constrained_default_config(&fixed); fixed.time_budget_us = 200000; fixed.tilt_prior_std_rad = 0; Scenario sc; sc.min_keep = 6; sc.max_keep = 11; sc.tilt_error = 5 * M_PI / 180; MonteCarlo adjusted = run_scenario(sc, 300, 0x7117); MonteCarlo kept = run_scenario(sc, 300, 0x7117, &fixed); print("tilt-5deg-adjusted", adjusted); print("tilt-5deg-fixed", kept); CHECK(adjusted.wrong <= kept.wrong); CHECK(adjusted.accepted >= kept.accepted + 10); CHECK(adjusted.reasons[CONSTELLATION_CONSTRAINED_INCONSISTENT] < kept.reasons[CONSTELLATION_CONSTRAINED_INCONSISTENT] / 2); // No tilt error: the extra freedom costs at most a few acquisitions. sc.tilt_error = 0; MonteCarlo exact = run_scenario(sc, 300, 0x7118); MonteCarlo exact_fixed = run_scenario(sc, 300, 0x7118, &fixed); print("tilt-0deg-adjusted", exact); print("tilt-0deg-fixed", exact_fixed); CHECK(exact.wrong == 0); CHECK(exact.accepted + 6 >= exact_fixed.accepted); } TEST_CASE("Head-relative positions ignore head pitch and roll but follow its heading") { xrt_pose head = XRT_POSE_IDENTITY; head.position = {0.3f, 1.6f, -0.2f}; // A hand 50 cm in front, 60 cm below and 20 cm left of the head. auto check = [&](xrt_vec3 expected) { xrt_vec3 forward = {0, 0, -1}, world_forward, right = {1, 0, 0}, world_right; math_quat_rotate_vec3(&head.orientation, &forward, &world_forward); math_quat_rotate_vec3(&head.orientation, &right, &world_right); // Build the hand position from the head's horizontal heading. world_forward.y = 0; world_right.y = 0; math_vec3_normalize(&world_forward); math_vec3_normalize(&world_right); xrt_vec3 hand = head.position; hand = m_vec3_add(hand, m_vec3_mul_scalar(world_forward, -expected.z)); hand = m_vec3_add(hand, m_vec3_mul_scalar(world_right, expected.x)); hand.y += expected.y; xrt_vec3 out; constellation_head_relative_position(&head, &hand, &out); CHECK(out.x == Catch::Approx(expected.x).margin(1e-4)); CHECK(out.y == Catch::Approx(expected.y).margin(1e-4)); CHECK(out.z == Catch::Approx(expected.z).margin(1e-4)); }; check({-0.2f, -0.6f, -0.5f}); // Looking down 60 degrees, or turned 120 degrees and rolled: the same body-relative numbers. head.orientation = rotation(-60 * M_PI / 180, {1, 0, 0}); check({-0.2f, -0.6f, -0.5f}); head.orientation = multiply(rotation(120 * M_PI / 180, {0, 1, 0}), rotation(25 * M_PI / 180, {0, 0, 1})); check({-0.2f, -0.6f, -0.5f}); xrt_vec3 hand = {0.3f, 1.0f, -0.2f}, out; constellation_head_relative_position(&head, &hand, &out); CHECK(out.y == Catch::Approx(-0.6).margin(1e-4)); CHECK(std::hypot(out.x, out.z) < 1e-4); } TEST_CASE("Confirmed heading excludes the r12 gravity-hypothesis identity jumps before selection") { Scene s; s.observe(0, {0, 1}); s.observe(1, {2}); constellation_constrained_result result{}; REQUIRE(constellation_constrained_search(&s.model, s.views.data(), 2, &s.truth, true, true, &s.config, &result)); for (double degrees : {79., 179.}) { auto prior = s.truth; prior.orientation = multiply(rotation(degrees * M_PI / 180, {0, 1, 0}), prior.orientation); CHECK_FALSE(constellation_constrained_search(&s.model, s.views.data(), 2, &prior, true, true, &s.config, &result)); CHECK(result.prior_rotation_rejections > 0); // Acquisition after loss still allows every heading, independently of this guard. REQUIRE(constellation_constrained_search(&s.model, s.views.data(), 2, &prior, false, false, &s.config, &result)); CHECK(angle(result.P_world_model.orientation, s.truth.orientation) < .001); } } TEST_CASE("Shared blob logging selects the strongest conflict in any device order") { constellation_constrained_result mine{}, weak{}, strong{}; for (auto *r : {&mine, &weak, &strong}) { r->num_assignments = 1; r->assignments[0] = {0, 7, 0, 0, 0}; r->inliers = 5; r->distinct_leds = 4; } weak.rejection = CONSTELLATION_CONSTRAINED_AMBIGUOUS; for (bool reversed : {false, true}) { const constellation_constrained_result *claims[] = {&mine, reversed ? &strong : &weak, reversed ? &weak : &strong}; bool published[] = {false, reversed, !reversed}, candidates[] = {true, false, false}; constellation_constrained_claim blocked[3]; unsigned blockers[3]; constellation_constrained_arbitrate_with_owners(claims, published, candidates, blocked, blockers, 3, 2); CHECK_FALSE(candidates[0]); CHECK(blocked[0] == CONSTELLATION_CLAIM_PUBLISHED); CHECK(claims[blockers[0]] == &strong); published[1] = published[2] = false; candidates[0] = true; candidates[reversed ? 1 : 2] = true; constellation_constrained_arbitrate_with_owners(claims, published, candidates, blocked, blockers, 3, 2); CHECK(blocked[0] == CONSTELLATION_CLAIM_CANDIDATE); CHECK(claims[blockers[0]] == &strong); } } TEST_CASE("Repeated near-depth candidates cannot erase a recent optical history") { constellation_track track; constellation_track_init(&track); xrt_quat imu = XRT_QUAT_IDENTITY; auto update = [&](int frame, float depth) { auto next = track_point({0, 0, depth}, imu, imu, frame * 33000000LL); return constellation_track_update(&track, &next, true, &imu, nullptr, nullptr); }; CHECK(update(1, 0.9f) == CONSTELLATION_TRACK_HOLD_CONFIRMING); REQUIRE(update(2, 0.9f) == CONSTELLATION_TRACK_PUBLISH); CHECK(update(3, 0.2f) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(update(4, 0.2f) == CONSTELLATION_TRACK_HOLD_JUMP); CHECK(update(5, 0.2f) == CONSTELLATION_TRACK_HOLD_RESTART); CHECK(update(6, 0.2f) == CONSTELLATION_TRACK_HOLD_UNCONFIRMED); CHECK(update(7, 0.2f) == CONSTELLATION_TRACK_HOLD_UNCONFIRMED); CHECK(track.published.P_world_model.position.z == Catch::Approx(0.9f)); } TEST_CASE("P3P acquisition forwards three-camera-view roots to joint verification") { Scene s; s.observe(0, {0, 1, 2}); s.observe(1, {0}); for (auto &o : s.observations) for (int i = 0; i < o.num_blobs; ++i) { o.blobs[i].led_id = LED_INVALID_ID; o.blobs[i].width = o.blobs[i].height = 2; } s.model.min_acquisition_leds = 5; struct Context { Scene *scene; unsigned checked = 0; bool recovered = false; } context{&s}; auto *model = t_constellation_search_model_new(&s.model); REQUIRE(model); auto *search = correspondence_search_new(&s.camera); REQUIRE(search); correspondence_search_set_blobs(search, s.observations[0].blobs, 3); search->pose_candidate_userdata = &context; search->pose_candidate_cb = [](void *userdata, const xrt_pose *pose, const pose_metrics *) { auto &ctx = *static_cast(userdata); ++ctx.checked; constellation_constrained_result result{}; if (constellation_constrained_verify_pose(&ctx.scene->model, ctx.scene->views.data(), 2, pose, CONSTELLATION_CONSTRAINED_P3P, &ctx.scene->config, &result)) { ctx.recovered |= distance(pose->position, ctx.scene->truth.position) < 0.001 && angle(pose->orientation, ctx.scene->truth.orientation) < 0.01; } }; xrt_pose seed = XRT_POSE_IDENTITY; seed.position = {42, 42, 42}; xrt_vec3 gravity{0, 1, 0}, pos_error{0.001f, 0.001f, 0.001f}, rot_error{0.1f, 0.1f, 0.1f}; pose_metrics metrics{}; auto flags = static_cast(CS_FLAG_SHALLOW_SEARCH | CS_FLAG_DEEP_SEARCH | CS_FLAG_MATCH_GRAVITY | CS_FLAG_MATCH_ALL_BLOBS | CS_FLAG_JOINT_P3P); correspondence_search_find_one_pose(search, model, flags, &seed, &pos_error, &rot_error, &gravity, 0.1f, &metrics); CHECK(context.checked > 0); CHECK(context.recovered); SECTION("Three observations alone cannot acquire") { s.observe(1, {}); context.recovered = false; seed = XRT_POSE_IDENTITY; correspondence_search_find_one_pose(search, model, flags, &seed, &pos_error, &rot_error, &gravity, 0.1f, &metrics); CHECK_FALSE(context.recovered); } SECTION("Published blob ownership is excluded before generating P3P roots") { context.checked = 0; s.views[0].excluded_blobs[1] = true; search->excluded_blobs = s.views[0].excluded_blobs; seed = XRT_POSE_IDENTITY; correspondence_search_find_one_pose(search, model, flags, &seed, &pos_error, &rot_error, &gravity, 0.1f, &metrics); CHECK(context.checked == 0); } correspondence_search_free(search); t_constellation_search_model_free(model); } TEST_CASE("Cold right acquisition uses unclaimed observations after the left publishes") { // Two geometrically identical objects are the hardest ownership case. Raw cold search // cannot choose between their rings; arbitration alone never exposes the second solution. Scene s; const xrt_pose right = s.truth; s.config.time_budget_us = 25000; xrt_pose left = right; left.position.x = -0.18f; s.truth = left; s.observe(0, {0, 1, 2}); s.observe(1, {0, 1, 2}); for (unsigned k = 0; k < 2; k++) { for (unsigned l = 0; l < s.leds.size(); l++) { xrt_vec3 world, local; math_pose_transform_point(&right, &s.leds[l].pos, &world); math_pose_transform_point(&s.views[k].P_cam_world, &world, &local); auto &o = s.observations[k]; auto &b = o.blobs[o.num_blobs++]; REQUIRE(t_camera_models_project(&s.camera.calib, local.x, local.y, local.z, &b.x, &b.y)); b.led_id = LED_MAKE_ID(99, 42); // Stale labels cannot become ownership. } } constellation_tracking_sample sample{}; sample.n_devices = 2; sample.devices[0].found_device_pose = true; REQUIRE(constellation_constrained_verify_pose(&s.model, s.views.data(), 2, &left, CONSTELLATION_CONSTRAINED_P3P, &s.config, &sample.devices[0].joint_result)); constellation_track track; constellation_track_init(&track); xrt_pose prior = XRT_POSE_IDENTITY; unsigned raw_published = 0, recovered_at = 0; for (unsigned frame = 1; frame <= 60; frame++) { s.observations[1].num_blobs = frame % 3 ? 6 : 5; // Vary visibility without changing blob indices. auto &result = sample.devices[1].joint_result; bool candidates[2] = {false, constellation_constrained_search(&s.model, s.views.data(), 2, &prior, false, false, &s.config, &result)}; const bool published[2] = {true, false}; const constellation_constrained_result *claims[2] = {&sample.devices[0].joint_result, &result}; constellation_constrained_claim blocked[2]; constellation_constrained_arbitrate(claims, published, candidates, blocked, 2, 2); raw_published += candidates[1]; auto remaining = s.views; const unsigned excluded = constellation_tracking_sample_exclude_published(&sample, 1, remaining.data(), remaining.size()); CHECK(excluded == 6); if (!constellation_constrained_search(&s.model, remaining.data(), 2, &prior, false, false, &s.config, &result)) continue; for (unsigned a = 0; a < result.num_assignments; a++) CHECK(result.assignments[a].blob_index >= 3); CHECK(distance(result.P_world_model.position, right.position) < 0.001); candidates[1] = true; constellation_constrained_arbitrate(claims, published, candidates, blocked, 2, 2); REQUIRE(candidates[1]); constellation_track_point next{result.P_world_model, right.orientation, int64_t(frame) * 33333333}; auto verdict = constellation_track_update(&track, &next, result.vision_determines_yaw, &right.orientation, nullptr, nullptr); if (verdict == CONSTELLATION_TRACK_PUBLISH && !recovered_at) recovered_at = frame; } CHECK(raw_published == 0); CHECK(recovered_at > 0); CHECK(recovered_at <= 60); // Removing the current publication re-enables all blobs, regardless of old LED labels. sample.devices[0].found_device_pose = false; auto remaining = s.views; CHECK(constellation_tracking_sample_exclude_published(&sample, 1, remaining.data(), 2) == 0); // The ownership retry does not bypass the optical/IMU continuity gate. constellation_track_point jump{right, right.orientation, int64_t(61) * 33333333}; jump.P_world_model.orientation = rotation(2, {0, 1, 0}); CHECK(constellation_track_update(&track, &jump, true, &right.orientation, nullptr, nullptr) == CONSTELLATION_TRACK_HOLD_JUMP); std::printf("sequential ownership: raw publications=%u recovered at frame=%u / 60\n", raw_published, recovered_at); } TEST_CASE("Cold P3P uses the capture-time tilt without trusting the position") { Scene s; s.truth.orientation = multiply(rotation(0.4, {0, 1, 0}), rotation(0.65, {1, 0, 0})); s.observe(0, {0, 1, 2}); s.observe(1, {0}); for (auto &o : s.observations) for (int i = 0; i < o.num_blobs; i++) { o.blobs[i].led_id = LED_INVALID_ID; o.blobs[i].width = o.blobs[i].height = 2; } struct Context { Scene *scene; unsigned recovered = 0; } context{&s}; auto *model = t_constellation_search_model_new(&s.model); auto *search = correspondence_search_new(&s.camera); REQUIRE(model); REQUIRE(search); search->pose_candidate_userdata = &context; search->pose_candidate_cb = [](void *userdata, const xrt_pose *pose, const pose_metrics *) { auto &ctx = *static_cast(userdata); constellation_constrained_result result{}; if (constellation_constrained_verify_pose(&ctx.scene->model, ctx.scene->views.data(), 2, pose, CONSTELLATION_CONSTRAINED_P3P, &ctx.scene->config, &result) && distance(result.P_world_model.position, ctx.scene->truth.position) < 0.001 && angle(result.P_world_model.orientation, ctx.scene->truth.orientation) < 0.01) ctx.recovered++; }; correspondence_search_set_blobs(search, s.observations[0].blobs, 3); const xrt_vec3 gravity = {0, 1, 0}; xrt_vec3 camera_gravity = gravity; auto flags = static_cast(CS_FLAG_SHALLOW_SEARCH | CS_FLAG_DEEP_SEARCH | CS_FLAG_MATCH_GRAVITY | CS_FLAG_MATCH_ALL_BLOBS | CS_FLAG_JOINT_P3P); // Only tilt is known. Arbitrary position and yaw must not gate cold acquisition. xrt_pose seed = {multiply(rotation(1.7, gravity), s.truth.orientation), {42, -42, 42}}; pose_metrics metrics{}; correspondence_search_find_one_pose(search, model, flags, &seed, nullptr, nullptr, &camera_gravity, 0.1f, &metrics); CHECK(context.recovered > 0); SECTION("An incompatible tilt still rejects the same geometry") { context.recovered = 0; seed = XRT_POSE_IDENTITY; correspondence_search_find_one_pose(search, model, flags, &seed, nullptr, nullptr, &camera_gravity, 0.1f, &metrics); CHECK(context.recovered == 0); } correspondence_search_free(search); t_constellation_search_model_free(model); } TEST_CASE("Tracked ring with many LEDs keeps publishing when the prior search is truncated") { /* r18 worn log: a tracked ring puts thousands of pairs on the prior edge list, beyond the * 1000-trial cap, and the fast path rejected 1267 of 1429 tracked frames as uncertain. A * truncated search must still verify the continuing identity, and never a wrong one. */ Scenario sc; sc.trusted = true; sc.heading = true; sc.min_keep = 8; sc.max_keep = 11; constellation_constrained_config config; constellation_constrained_default_config(&config); MonteCarlo capped = run_scenario(sc, 200, 0x5219, &config); print("trusted-heading-default-cap", capped); CHECK(capped.wrong == 0); CHECK(capped.reasons[CONSTELLATION_CONSTRAINED_UNCERTAIN] == 0); CHECK(capped.accepted >= 190); // Only the first few predicted pairs: the continuing identity is among them. config.max_prior_pair_trials = 20; MonteCarlo tight = run_scenario(sc, 200, 0x5219 + 1, &config); print("trusted-heading-20-pairs", tight); CHECK(tight.wrong == 0); CHECK(tight.accepted >= 190); RingScene s(2); s.choose_pose({-0.02f, 0.04f, 0.4f}, {4, 2}, 3); REQUIRE(s.observe(0, 4) + s.observe(1, 2) >= 5); s.config.max_prior_pair_trials = 1; constellation_constrained_result result; REQUIRE(constellation_constrained_search(&s.model, s.rig.views.data(), 2, &s.truth, true, true, &s.config, &result)); CHECK(result.budget_exhausted); CHECK(s.identity(result)); }