The open source OpenXR runtime
at prediction-2 1638 lines 55 kB view raw
1// Copyright 2021-2024, Collabora, Ltd. 2// SPDX-License-Identifier: BSL-1.0 3/*! 4 * @file 5 * @brief SLAM tracking code. 6 * @author Mateo de Mayo <mateo.demayo@collabora.com> 7 * @ingroup aux_tracking 8 */ 9 10#include "xrt/xrt_config_have.h" 11#include "xrt/xrt_defines.h" 12#include "xrt/xrt_tracking.h" 13#include "xrt/xrt_frameserver.h" 14#include "util/u_debug.h" 15#include "util/u_logging.h" 16#include "util/u_misc.h" 17#include "util/u_sink.h" 18#include "util/u_var.h" 19#include "util/u_trace_marker.h" 20#include "os/os_threading.h" 21#include "math/m_api.h" 22#include "math/m_filter_fifo.h" 23#include "math/m_filter_one_euro.h" 24#include "math/m_predict.h" 25#include "math/m_relation_history.h" 26#include "math/m_space.h" 27#include "math/m_vec3.h" 28#include "tracking/t_euroc_recorder.h" 29#include "tracking/t_openvr_tracker.h" 30#include "tracking/t_tracking.h" 31#include "tracking/t_vit_loader.h" 32 33#include "vit/vit_interface.h" 34 35#include <opencv2/core/mat.hpp> 36#include <opencv2/core/version.hpp> 37 38#include <deque> 39#include <filesystem> 40#include <fstream> 41#include <iomanip> 42#include <map> 43#include <mutex> 44#include <string> 45#include <vector> 46 47//! @todo Get preferred system from systems found at build time 48#define PREFERRED_VIT_SYSTEM_LIBRARY "libbasalt.so" 49 50#define SLAM_TRACE(...) U_LOG_IFL_T(t.log_level, __VA_ARGS__) 51#define SLAM_DEBUG(...) U_LOG_IFL_D(t.log_level, __VA_ARGS__) 52#define SLAM_INFO(...) U_LOG_IFL_I(t.log_level, __VA_ARGS__) 53#define SLAM_WARN(...) U_LOG_IFL_W(t.log_level, __VA_ARGS__) 54#define SLAM_ERROR(...) U_LOG_IFL_E(t.log_level, __VA_ARGS__) 55#define SLAM_ASSERT(predicate, ...) \ 56 do { \ 57 bool p = predicate; \ 58 if (!p) { \ 59 U_LOG(U_LOGGING_ERROR, __VA_ARGS__); \ 60 assert(false && "SLAM_ASSERT failed: " #predicate); \ 61 exit(EXIT_FAILURE); \ 62 } \ 63 } while (false); 64#define SLAM_ASSERT_(predicate) SLAM_ASSERT(predicate, "Assertion failed " #predicate) 65 66// Debug assertions, not vital but useful for finding errors 67#ifdef NDEBUG 68#define SLAM_DASSERT(predicate, ...) (void)(predicate) 69#define SLAM_DASSERT_(predicate) (void)(predicate) 70#else 71#define SLAM_DASSERT(predicate, ...) SLAM_ASSERT(predicate, __VA_ARGS__) 72#define SLAM_DASSERT_(predicate) SLAM_ASSERT_(predicate) 73#endif 74 75//! @see t_slam_tracker_config 76DEBUG_GET_ONCE_LOG_OPTION(slam_log, "SLAM_LOG", U_LOGGING_INFO) 77DEBUG_GET_ONCE_OPTION(vit_system_library_path, "VIT_SYSTEM_LIBRARY_PATH", PREFERRED_VIT_SYSTEM_LIBRARY) 78DEBUG_GET_ONCE_OPTION(slam_config, "SLAM_CONFIG", nullptr) 79DEBUG_GET_ONCE_BOOL_OPTION(slam_ui, "SLAM_UI", false) 80DEBUG_GET_ONCE_BOOL_OPTION(slam_submit_from_start, "SLAM_SUBMIT_FROM_START", false) 81DEBUG_GET_ONCE_NUM_OPTION(slam_openvr_groundtruth_device, "SLAM_OPENVR_GROUNDTRUTH_DEVICE", 0) 82DEBUG_GET_ONCE_NUM_OPTION(slam_prediction_type, "SLAM_PREDICTION_TYPE", long(SLAM_PRED_IP_IO_IA_IL)) 83DEBUG_GET_ONCE_BOOL_OPTION(slam_write_csvs, "SLAM_WRITE_CSVS", false) 84DEBUG_GET_ONCE_OPTION(slam_csv_path, "SLAM_CSV_PATH", "evaluation/") 85DEBUG_GET_ONCE_BOOL_OPTION(slam_timing_stat, "SLAM_TIMING_STAT", true) 86DEBUG_GET_ONCE_BOOL_OPTION(slam_features_stat, "SLAM_FEATURES_STAT", true) 87DEBUG_GET_ONCE_NUM_OPTION(slam_cam_count, "SLAM_CAM_COUNT", 2) 88 89//! Namespace for the interface to the external SLAM tracking system 90namespace xrt::auxiliary::tracking::slam { 91constexpr int UI_TIMING_POSE_COUNT = 192; 92constexpr int UI_FEATURES_POSE_COUNT = 192; 93constexpr int UI_GTDIFF_POSE_COUNT = 192; 94 95using os::Mutex; 96using std::deque; 97using std::ifstream; 98using std::make_shared; 99using std::map; 100using std::ofstream; 101using std::ostream; 102using std::pair; 103using std::shared_ptr; 104using std::string; 105using std::to_string; 106using std::unique_lock; 107using std::vector; 108using std::filesystem::create_directories; 109using Trajectory = map<timepoint_ns, xrt_pose>; 110using timing_sample = vector<timepoint_ns>; 111 112using xrt::auxiliary::math::RelationHistory; 113 114 115/* 116 * 117 * CSV Writers 118 * 119 */ 120 121ostream & 122operator<<(ostream &os, const xrt_pose_sample &s) 123{ 124 timepoint_ns ts = s.timestamp_ns; 125 xrt_vec3 p = s.pose.position; 126 xrt_quat r = s.pose.orientation; 127 os << ts << ","; 128 os << p.x << "," << p.y << "," << p.z << ","; 129 os << r.w << "," << r.x << "," << r.y << "," << r.z << CSV_EOL; 130 return os; 131} 132 133ostream & 134operator<<(ostream &os, const timing_sample &timestamps) 135{ 136 for (const timepoint_ns &ts : timestamps) { 137 string delimiter = &ts != &timestamps.back() ? "," : CSV_EOL; 138 os << ts << delimiter; 139 } 140 return os; 141} 142 143struct feature_count_sample 144{ 145 timepoint_ns ts; 146 vector<int> counts; 147}; 148 149ostream & 150operator<<(ostream &os, const feature_count_sample &s) 151{ 152 os << s.ts; 153 for (int count : s.counts) { 154 os << "," << count; 155 } 156 os << CSV_EOL; 157 return os; 158} 159 160//! Writes a CSV file for a particular row type 161template <typename RowType> class CSVWriter 162{ 163public: 164 bool enabled; // Modified through UI 165 166protected: 167 vector<string> column_names; 168 169private: 170 string directory; 171 string filename; 172 ofstream file; 173 bool created = false; 174 Mutex mutex; 175 176 void 177 create() 178 { 179 create_directories(directory); 180 file = ofstream{directory + "/" + filename}; 181 file << "#"; 182 for (const string &col : column_names) { 183 string delimiter = &col != &column_names.back() ? "," : CSV_EOL; 184 file << col << delimiter; 185 } 186 file << std::fixed << std::setprecision(CSV_PRECISION); 187 } 188 189public: 190 CSVWriter(const string &dir, const string &fn, bool e, const vector<string> &cn = {}) 191 : enabled(e), column_names(cn), directory(dir), filename(fn) 192 {} 193 194 virtual ~CSVWriter() {} 195 196 void 197 push(RowType row) 198 { 199 unique_lock lock(mutex); 200 201 if (!enabled) { 202 return; 203 } 204 205 if (!created) { 206 created = true; 207 create(); 208 } 209 210 file << row; 211 } 212}; 213 214//! Writes poses and their timestamps to a CSV file 215struct TrajectoryWriter : public CSVWriter<xrt_pose_sample> 216{ 217 TrajectoryWriter(const string &dir, const string &fn, bool e) : CSVWriter<xrt_pose_sample>(dir, fn, e) 218 { 219 column_names = {"timestamp [ns]", "p_RS_R_x [m]", "p_RS_R_y [m]", "p_RS_R_z [m]", 220 "q_RS_w []", "q_RS_x []", "q_RS_y []", "q_RS_z []"}; 221 } 222}; 223 224//! Writes timestamps measured when estimating a new pose by the SLAM system 225struct TimingWriter : public CSVWriter<timing_sample> 226{ 227 TimingWriter(const string &dir, const string &fn, bool e, const vector<string> &cn) 228 : CSVWriter<timing_sample>(dir, fn, e, cn) 229 {} 230}; 231 232//! Writes feature information specific to a particular estimated pose 233struct FeaturesWriter : public CSVWriter<feature_count_sample> 234{ 235 FeaturesWriter(const string &dir, const string &fn, bool e, size_t cam_count) 236 : CSVWriter<feature_count_sample>(dir, fn, e) 237 { 238 column_names.push_back("timestamp"); 239 for (size_t i = 0; i < cam_count; i++) { 240 column_names.push_back("cam" + to_string(i) + " feature count"); 241 } 242 } 243}; 244 245/*! 246 * Main implementation of @ref xrt_tracked_slam. This is an adapter class for 247 * SLAM tracking that wraps an external SLAM implementation. 248 * 249 * @implements xrt_tracked_slam 250 * @implements xrt_frame_node 251 * @implements xrt_frame_sink 252 * @implements xrt_imu_sink 253 * @implements xrt_pose_sink 254 */ 255struct TrackerSlam 256{ 257 struct xrt_tracked_slam base = {}; 258 struct xrt_frame_node node = {}; //!< Will be called on destruction 259 struct t_vit_bundle vit; //!< VIT system function pointers 260 struct vit_tracker_extension_set exts; //!< VIT tracker supported extensions 261 struct vit_tracker *tracker; //!< Pointer to the tracker created by the loaded VIT system; 262 263 struct xrt_slam_sinks sinks = {}; //!< Pointers to the sinks below 264 struct xrt_frame_sink cam_sinks[XRT_TRACKING_MAX_SLAM_CAMS]; //!< Sends camera frames to the SLAM system 265 struct xrt_imu_sink imu_sink = {}; //!< Sends imu samples to the SLAM system 266 struct xrt_pose_sink gt_sink = {}; //!< Register groundtruth trajectory for stats 267 struct xrt_hand_masks_sink hand_masks_sink = {}; //!< Register latest masks to ignore 268 269 bool submit; //!< Whether to submit data pushed to sinks to the SLAM tracker 270 uint32_t cam_count; //!< Number of cameras used for tracking 271 272 struct u_var_button reset_state_btn; //!< Reset tracker state button 273 274 enum u_logging_level log_level; //!< Logging level for the SLAM tracker, set by SLAM_LOG var 275 276 struct xrt_slam_sinks *euroc_recorder; //!< EuRoC dataset recording sinks 277 struct openvr_tracker *ovr_tracker; //!< OpenVR lighthouse tracker 278 279 // Used mainly for checking that the timestamps come in order 280 timepoint_ns last_imu_ts; //!< Last received IMU sample timestamp 281 vector<timepoint_ns> last_cam_ts; //!< Last received image timestamp per cam 282 struct xrt_hand_masks_sample last_hand_masks; //!< Last received hand masks info 283 Mutex last_hand_masks_mutex; //!< Mutex for @ref last_hand_masks 284 285 // Prediction 286 287 //! Type of prediction to use 288 t_slam_prediction_type pred_type; 289 u_var_combo pred_combo; //!< UI combo box to select @ref pred_type 290 RelationHistory slam_rels{}; //!< A history of relations produced purely from external SLAM tracker data 291 int dbg_pred_every = 1; //!< Skip X SLAM poses so that you get tracked mostly by the prediction algo 292 int dbg_pred_counter = 0; //!< SLAM pose counter for prediction debugging 293 struct os_mutex lock_ff; //!< Lock for gyro_ff and accel_ff. 294 struct m_ff_vec3_f32 *gyro_ff; //!< Last gyroscope samples 295 struct m_ff_vec3_f32 *accel_ff; //!< Last accelerometer samples 296 vector<u_sink_debug> ui_sink; //!< Sink to display frames in UI of each camera 297 298 //! Used to correct accelerometer measurements when integrating into the prediction. 299 //! @todo Should be automatically computed instead of required to be filled manually through the UI. 300 xrt_vec3 gravity_correction{0, 0, -MATH_GRAVITY_M_S2}; 301 302 struct xrt_space_relation last_rel = XRT_SPACE_RELATION_ZERO; //!< Last reported/tracked pose 303 timepoint_ns last_ts; //!< Last reported/tracked pose timestamp 304 305 //! Filters are used to smooth out the resulting trajectory 306 struct 307 { 308 // Moving average filter 309 bool use_moving_average_filter = false; 310 //! Time window in ms take the average on. 311 //! Increasing it smooths out the tracking at the cost of adding delay. 312 double window = 66; 313 struct m_ff_vec3_f32 *pos_ff; //! Predicted positions fifo 314 struct m_ff_vec3_f32 *rot_ff; //! Predicted rotations fifo (only xyz components, w is inferred) 315 316 // Exponential smoothing filter 317 bool use_exponential_smoothing_filter = false; 318 float alpha = 0.1; //!< How much should we lerp towards the @ref target value on each update 319 struct xrt_space_relation last = XRT_SPACE_RELATION_ZERO; //!< Last filtered relation 320 struct xrt_space_relation target = XRT_SPACE_RELATION_ZERO; //!< Target relation 321 322 // One euro filter 323 bool use_one_euro_filter = false; 324 m_filter_euro_vec3 pos_oe; //!< One euro position filter 325 m_filter_euro_quat rot_oe; //!< One euro rotation filter 326 const float min_cutoff = M_PI; //!< Default minimum cutoff frequency 327 const float min_dcutoff = 1; //!< Default minimum cutoff frequency for the derivative 328 const float beta = 0.16; //!< Default speed coefficient 329 330 } filter; 331 332 // Stats and metrics 333 334 // CSV writers for offline analysis (using pointers because of container_of) 335 TimingWriter *slam_times_writer; //!< Timestamps of the pipeline for performance analysis 336 FeaturesWriter *slam_features_writer; //!< Feature tracking information for analysis 337 TrajectoryWriter *slam_traj_writer; //!< Estimated poses from the SLAM system 338 TrajectoryWriter *pred_traj_writer; //!< Predicted poses 339 TrajectoryWriter *filt_traj_writer; //!< Predicted and filtered poses 340 341 //! Tracker timing info for performance evaluation 342 struct 343 { 344 bool enabled = false; //!< Whether the timing extension is enabled 345 float dur_ms[UI_TIMING_POSE_COUNT]; //!< Timing durations in ms 346 int idx = 0; //!< Index of latest entry in @ref dur_ms 347 u_var_combo start_ts; //!< UI combo box to select initial timing measurement 348 u_var_combo end_ts; //!< UI combo box to select final timing measurement 349 int start_ts_idx; //!< Selected initial timing measurement in @ref start_ts 350 int end_ts_idx; //!< Selected final timing measurement in @ref end_ts 351 struct u_var_timing ui; //!< Realtime UI for tracker durations 352 vector<string> columns; //!< Column names of the measured timestamps 353 string joined_columns; //!< Column names as a null separated string 354 struct u_var_button enable_btn; //!< Toggle tracker timing reports 355 } timing; 356 357 //! Tracker feature tracking info 358 struct Features 359 { 360 struct FeatureCounter 361 { 362 //! Feature count for each frame timestamp for this camera. 363 //! @note Harmless race condition over this as the UI might read this while it's being written 364 deque<pair<timepoint_ns, int>> entries{}; 365 366 //! Persistently stored camera name for display in the UI 367 string cam_name; 368 369 void 370 addFeatureCount(timepoint_ns ts, int count) 371 { 372 entries.emplace_back(ts, count); 373 if (entries.size() > UI_FEATURES_POSE_COUNT) { 374 entries.pop_front(); 375 } 376 } 377 }; 378 379 vector<FeatureCounter> fcs; //!< Store feature count info for each camera 380 u_var_curves fcs_ui; //!< Display of `fcs` in UI 381 382 bool enabled = false; //!< Whether the features extension is enabled 383 struct u_var_button enable_btn; //!< Toggle extension 384 } features; 385 386 //! Ground truth related fields 387 struct 388 { 389 Trajectory *trajectory; //!< Empty if we've not received groundtruth 390 xrt_pose origin; //!< First ground truth pose 391 float diffs_mm[UI_GTDIFF_POSE_COUNT]; //!< Positional error wrt ground truth 392 int diff_idx = 0; //!< Index of last error in @ref diffs_mm 393 struct u_var_timing diff_ui; //!< Realtime UI for positional error 394 bool override_tracking = false; //!< Force the tracker to report gt poses instead 395 } gt; 396}; 397 398 399/* 400 * 401 * Timing functionality 402 * 403 */ 404 405static void 406timing_ui_setup(TrackerSlam &t) 407{ 408 t.timing.enabled = false; 409 410 u_var_add_ro_raw_text(&t, "\nTracker timing", "Tracker timing"); 411 412 // Setup toggle button 413 static const char *msg[2] = {"[OFF] Enable timing", "[ON] Disable timing"}; 414 u_var_button_cb cb = [](void *t_ptr) { 415 TrackerSlam *t = (TrackerSlam *)t_ptr; 416 u_var_button &btn = t->timing.enable_btn; 417 bool e = !t->timing.enabled; 418 snprintf(btn.label, sizeof(btn.label), "%s", msg[e]); 419 vit_result_t vres = t->vit.tracker_enable_extension(t->tracker, VIT_TRACKER_EXTENSION_POSE_TIMING, e); 420 if (vres != VIT_SUCCESS) { 421 U_LOG_IFL_E(t->log_level, "Failed to set tracker timing extension"); 422 return; 423 } 424 t->timing.enabled = e; 425 }; 426 t.timing.enable_btn.cb = cb; 427 t.timing.enable_btn.disabled = !t.exts.has_pose_timing; 428 t.timing.enable_btn.ptr = &t; 429 u_var_add_button(&t, &t.timing.enable_btn, msg[t.timing.enabled]); 430 431 // We provide two timing columns by default, even if there is no extension support 432 t.timing.columns = {"sampled", "received_by_monado"}; 433 434 // Only fill the timing columns if the tracker supports pose timing 435 if (t.exts.has_pose_timing) { 436 vit_tracker_timing_titles titles = {}; 437 vit_result_t vres = t.vit.tracker_get_timing_titles(t.tracker, &titles); 438 if (vres != VIT_SUCCESS) { 439 SLAM_ERROR("Failed to get timing titles from tracker"); 440 return; 441 } 442 443 // Copies the titles locally. 444 std::vector<std::string> cols(titles.titles, titles.titles + titles.count); 445 446 t.timing.columns.insert(t.timing.columns.begin() + 1, cols.begin(), cols.end()); 447 } 448 449 // Construct null-separated array of options for the combo box 450 using namespace std::string_literals; 451 t.timing.joined_columns = ""; 452 for (const string &name : t.timing.columns) { 453 t.timing.joined_columns += name + "\0"s; 454 } 455 t.timing.joined_columns += "\0"s; 456 457 t.timing.start_ts.count = t.timing.columns.size(); 458 t.timing.start_ts.options = t.timing.joined_columns.c_str(); 459 t.timing.start_ts.value = &t.timing.start_ts_idx; 460 t.timing.start_ts_idx = 0; 461 u_var_add_combo(&t, &t.timing.start_ts, "Start timestamp"); 462 463 t.timing.end_ts.count = t.timing.columns.size(); 464 t.timing.end_ts.options = t.timing.joined_columns.c_str(); 465 t.timing.end_ts.value = &t.timing.end_ts_idx; 466 t.timing.end_ts_idx = t.timing.columns.size() - 1; 467 u_var_add_combo(&t, &t.timing.end_ts, "End timestamp"); 468 469 t.timing.ui.values.data = t.timing.dur_ms; 470 t.timing.ui.values.length = UI_TIMING_POSE_COUNT; 471 t.timing.ui.values.index_ptr = &t.timing.idx; 472 t.timing.ui.reference_timing = 16.6; 473 t.timing.ui.center_reference_timing = true; 474 t.timing.ui.range = t.timing.ui.reference_timing; 475 t.timing.ui.dynamic_rescale = true; 476 t.timing.ui.unit = "ms"; 477 u_var_add_f32_timing(&t, &t.timing.ui, "External tracker times"); 478} 479 480//! Updates timing UI with info from a computed pose and returns that info 481static vector<timepoint_ns> 482timing_ui_push(TrackerSlam &t, const vit_pose_t *pose, int64_t ts) 483{ 484 timepoint_ns now = os_monotonic_get_ns(); 485 vector<timepoint_ns> tss = {ts, now}; 486 487 // Add extra timestamps if the SLAM tracker provides them 488 if (t.timing.enabled) { 489 vit_pose_timing timing; 490 vit_result_t vres = t.vit.pose_get_timing(pose, &timing); 491 if (vres != VIT_SUCCESS) { 492 // Even if the timing is enabled, some of the poses already in the queue won't have it enabled. 493 if (vres != VIT_ERROR_NOT_ENABLED) { 494 SLAM_ERROR("Failed to get pose timing"); 495 } 496 497 return {}; 498 } 499 500 std::vector<int64_t> data(timing.timestamps, timing.timestamps + timing.count); 501 tss.insert(tss.begin() + 1, data.begin(), data.end()); 502 503 // The two timestamps to compare in the graph 504 timepoint_ns start = tss.at(t.timing.start_ts_idx); 505 timepoint_ns end = tss.at(t.timing.end_ts_idx); 506 507 // Push to the UI graph 508 float tss_ms = (end - start) / U_TIME_1MS_IN_NS; 509 t.timing.idx = (t.timing.idx + 1) % UI_TIMING_POSE_COUNT; 510 t.timing.dur_ms[t.timing.idx] = tss_ms; 511 constexpr float a = 1.0f / UI_TIMING_POSE_COUNT; // Exponential moving average 512 t.timing.ui.reference_timing = (1 - a) * t.timing.ui.reference_timing + a * tss_ms; 513 } 514 515 return tss; 516} 517 518 519/* 520 * 521 * Feature information functionality 522 * 523 */ 524 525static void 526features_ui_setup(TrackerSlam &t) 527{ 528 t.features.enabled = false; 529 530 u_var_add_ro_raw_text(&t, "\nTracker features", "Tracker features"); 531 532 // Setup toggle button 533 static const char *msg[2] = {"[OFF] Enable features info", "[ON] Disable features info"}; 534 u_var_button_cb cb = [](void *t_ptr) { 535 TrackerSlam *t = (TrackerSlam *)t_ptr; 536 u_var_button &btn = t->features.enable_btn; 537 bool e = !t->features.enabled; 538 snprintf(btn.label, sizeof(btn.label), "%s", msg[e]); 539 vit_result_t vres = t->vit.tracker_enable_extension(t->tracker, VIT_TRACKER_EXTENSION_POSE_FEATURES, e); 540 if (vres != VIT_SUCCESS) { 541 U_LOG_IFL_E(t->log_level, "Failed to set tracker features extension"); 542 return; 543 } 544 t->features.enabled = e; 545 }; 546 t.features.enable_btn.cb = cb; 547 t.features.enable_btn.disabled = !t.exts.has_pose_features; 548 t.features.enable_btn.ptr = &t; 549 u_var_add_button(&t, &t.features.enable_btn, msg[t.features.enabled]); 550 551 // Setup graph 552 u_var_curve_getter getter = [](void *fs_ptr, int i) -> u_var_curve_point { 553 auto *fs = (TrackerSlam::Features::FeatureCounter *)fs_ptr; 554 timepoint_ns now = os_monotonic_get_ns(); 555 556 size_t size = fs->entries.size(); 557 if (size == 0) { 558 return {0, 0}; 559 } 560 561 int last_idx = size - 1; 562 if (i > last_idx) { 563 i = last_idx; 564 } 565 566 auto [ts, count] = fs->entries.at(last_idx - i); 567 return {time_ns_to_s(now - ts), double(count)}; 568 }; 569 570 t.features.fcs_ui.curve_count = t.cam_count; 571 t.features.fcs_ui.xlabel = "Last seconds"; 572 t.features.fcs_ui.ylabel = "Number of features"; 573 574 t.features.fcs.resize(t.cam_count); 575 for (uint32_t i = 0; i < t.cam_count; ++i) { 576 auto &fc = t.features.fcs[i]; 577 fc.cam_name = "Cam" + to_string(i); 578 579 auto &fc_ui = t.features.fcs_ui.curves[i]; 580 fc_ui.count = UI_FEATURES_POSE_COUNT; 581 fc_ui.data = &fc; 582 fc_ui.getter = getter; 583 fc_ui.label = fc.cam_name.c_str(); 584 } 585 586 u_var_add_curves(&t, &t.features.fcs_ui, "Feature count"); 587} 588 589static vector<int> 590features_ui_push(TrackerSlam &t, const vit_pose_t *pose, int64_t ts) 591{ 592 if (!t.features.enabled) { 593 return {}; 594 } 595 596 // Push to the UI graph 597 vector<int> fcs{}; 598 for (uint32_t i = 0; i < t.cam_count; ++i) { 599 vit_pose_features features = {}; 600 vit_result_t vres = t.vit.pose_get_features(pose, i, &features); 601 if (vres != VIT_SUCCESS) { 602 // Even if the features are enabled, some of the poses already in the queue won't have it 603 // enabled. 604 if (vres != VIT_ERROR_NOT_ENABLED) { 605 SLAM_ERROR("Failed to get pose features for camera %u", i); 606 } 607 608 return {}; 609 } 610 611 t.features.fcs.at(i).addFeatureCount(ts, features.count); 612 fcs.push_back(features.count); 613 } 614 615 return fcs; 616} 617 618/* 619 * 620 * Ground truth functionality 621 * 622 */ 623 624//! Gets an interpolated groundtruth pose (if available) at a specified timestamp 625static xrt_pose 626get_gt_pose_at(const Trajectory &gt, timepoint_ns ts) 627{ 628 if (gt.empty()) { 629 return XRT_POSE_IDENTITY; 630 } 631 632 Trajectory::const_iterator rit = gt.upper_bound(ts); 633 634 if (rit == gt.begin()) { // Too far in the past, return first gt pose 635 return gt.begin()->second; 636 } 637 638 if (rit == gt.end()) { // Too far in the future, return last gt pose 639 return std::prev(gt.end())->second; 640 } 641 642 Trajectory::const_iterator lit = std::prev(rit); 643 644 const auto &[lts, lpose] = *lit; 645 const auto &[rts, rpose] = *rit; 646 647 float t = double(ts - lts) / double(rts - lts); 648 SLAM_DASSERT_(0 <= t && t <= 1); 649 650 xrt_pose res{}; 651 math_quat_slerp(&lpose.orientation, &rpose.orientation, t, &res.orientation); 652 res.position = m_vec3_lerp(lpose.position, rpose.position, t); 653 return res; 654} 655 656//! Converts a pose from the tracker to ground truth 657static struct xrt_pose 658xr2gt_pose(const xrt_pose &gt_origin, const xrt_pose &xr_pose) 659{ 660 //! @todo Right now this is hardcoded for Basalt and the EuRoC vicon datasets 661 //! groundtruth and ignores orientation. Applies a fixed transformation so 662 //! that the tracked and groundtruth trajectories origins and general motion 663 //! match. The usual way of evaluating trajectory errors in SLAM requires to 664 //! first align the trajectories through a non-linear optimization (e.g. gauss 665 //! newton) so that they are as similar as possible. For this you need the 666 //! entire tracked trajectory to be known beforehand, which makes it not 667 //! suitable for reporting an error metric in realtime. See this 2-page paper 668 //! for more info on trajectory alignment: 669 //! https://ylatif.github.io/movingsensors/cameraReady/paper07.pdf 670 671 xrt_vec3 pos = xr_pose.position; 672 xrt_quat z180{0, 0, 1, 0}; 673 math_quat_rotate_vec3(&z180, &pos, &pos); 674 math_quat_rotate_vec3(&gt_origin.orientation, &pos, &pos); 675 pos += gt_origin.position; 676 677 return {XRT_QUAT_IDENTITY, pos}; 678} 679 680//! The inverse of @ref xr2gt_pose. 681static struct xrt_pose 682gt2xr_pose(const xrt_pose &gt_origin, const xrt_pose &gt_pose) 683{ 684 xrt_vec3 pos = gt_pose.position; 685 pos -= gt_origin.position; 686 xrt_quat gt_origin_orientation_inv = gt_origin.orientation; 687 math_quat_invert(&gt_origin_orientation_inv, &gt_origin_orientation_inv); 688 math_quat_rotate_vec3(&gt_origin_orientation_inv, &pos, &pos); 689 xrt_quat zn180{0, 0, -1, 0}; 690 math_quat_rotate_vec3(&zn180, &pos, &pos); 691 692 return {XRT_QUAT_IDENTITY, pos}; 693} 694 695static void 696gt_ui_setup(TrackerSlam &t) 697{ 698 u_var_add_ro_raw_text(&t, "\nTracker groundtruth", "Tracker groundtruth"); 699 t.gt.diff_ui.values.data = t.gt.diffs_mm; 700 t.gt.diff_ui.values.length = UI_GTDIFF_POSE_COUNT; 701 t.gt.diff_ui.values.index_ptr = &t.gt.diff_idx; 702 t.gt.diff_ui.reference_timing = 0; 703 t.gt.diff_ui.center_reference_timing = true; 704 t.gt.diff_ui.range = 100; // 10cm 705 t.gt.diff_ui.dynamic_rescale = true; 706 t.gt.diff_ui.unit = "mm"; 707 u_var_add_f32_timing(&t, &t.gt.diff_ui, "Tracking absolute error"); 708} 709 710static void 711gt_ui_push(TrackerSlam &t, timepoint_ns ts, xrt_pose tracked_pose) 712{ 713 if (t.gt.trajectory->empty()) { 714 return; 715 } 716 717 xrt_pose gt_pose = get_gt_pose_at(*t.gt.trajectory, ts); 718 xrt_pose xr_pose = xr2gt_pose(t.gt.origin, tracked_pose); 719 720 float len_mm = m_vec3_len(xr_pose.position - gt_pose.position) * 1000; 721 t.gt.diff_idx = (t.gt.diff_idx + 1) % UI_GTDIFF_POSE_COUNT; 722 t.gt.diffs_mm[t.gt.diff_idx] = len_mm; 723 constexpr float a = 1.0f / UI_GTDIFF_POSE_COUNT; // Exponential moving average 724 t.gt.diff_ui.reference_timing = (1 - a) * t.gt.diff_ui.reference_timing + a * len_mm; 725} 726 727/* 728 * 729 * Tracker functionality 730 * 731 */ 732 733//! Dequeue all tracked poses from the SLAM system and update prediction data with them. 734static bool 735flush_poses(TrackerSlam &t) 736{ 737 738 vit_pose_t *pose = NULL; 739 vit_result_t vres = t.vit.tracker_pop_pose(t.tracker, &pose); 740 if (vres != VIT_SUCCESS) { 741 SLAM_ERROR("Failed to get pose from VIT tracker"); 742 } 743 744 if (pose == NULL) { 745 SLAM_TRACE("No poses to flush"); 746 return false; 747 } 748 749 do { 750 // New pose 751 vit_pose_data_t data; 752 vres = t.vit.pose_get_data(pose, &data); 753 if (vres != VIT_SUCCESS) { 754 SLAM_ERROR("Failed to get pose data from VIT tracker"); 755 return false; 756 } 757 758 int64_t nts = data.timestamp; 759 760 xrt_vec3 npos{data.px, data.py, data.pz}; 761 xrt_quat nrot{data.ox, data.oy, data.oz, data.ow}; 762 763 // Last relation 764 xrt_space_relation lr = XRT_SPACE_RELATION_ZERO; 765 int64_t lts; 766 t.slam_rels.get_latest(&lts, &lr); 767 xrt_vec3 lpos = lr.pose.position; 768 xrt_quat lrot = lr.pose.orientation; 769 770 double dt = time_ns_to_s(nts - lts); 771 772 SLAM_TRACE("Dequeued SLAM pose ts=%ld p=[%f,%f,%f] r=[%f,%f,%f,%f]", // 773 nts, data.px, data.py, data.pz, data.ox, data.oy, data.oz, data.ow); 774 775 // TODO linear velocity from the VIT system 776 // Compute new relation based on new pose and velocities since last pose 777 xrt_space_relation rel{}; 778 rel.relation_flags = XRT_SPACE_RELATION_BITMASK_ALL; 779 rel.pose = {nrot, npos}; 780 rel.linear_velocity = (npos - lpos) / dt; 781 math_quat_finite_difference(&lrot, &nrot, dt, &rel.angular_velocity); 782 783 // Push to relationship history unless we are debugging prediction 784 if (t.dbg_pred_counter % t.dbg_pred_every == 0) { 785 t.slam_rels.push(rel, nts); 786 } 787 t.dbg_pred_counter = (t.dbg_pred_counter + 1) % t.dbg_pred_every; 788 789 gt_ui_push(t, nts, rel.pose); 790 t.slam_traj_writer->push({nts, rel.pose}); 791 xrt_pose_sample pose_sample = {nts, rel.pose}; 792 xrt_sink_push_pose(t.euroc_recorder->gt, &pose_sample); 793 794 auto tss = timing_ui_push(t, pose, nts); 795 t.slam_times_writer->push(tss); 796 797 if (t.features.enabled) { 798 vector feat_count = features_ui_push(t, pose, nts); 799 t.slam_features_writer->push({nts, feat_count}); 800 } 801 802 t.vit.pose_destroy(pose); 803 } while (t.vit.tracker_pop_pose(t.tracker, &pose) == VIT_SUCCESS && pose); 804 805 return true; 806} 807 808//! Integrates IMU samples on top of a base pose and predicts from that 809static void 810predict_pose_from_imu(TrackerSlam &t, 811 timepoint_ns when_ns, 812 xrt_space_relation base_rel, // Pose to integrate IMUs on top of 813 timepoint_ns base_rel_ts, 814 struct xrt_space_relation *out_relation) 815{ 816 os_mutex_lock(&t.lock_ff); 817 818 // Find oldest imu index i that is newer than latest SLAM pose (or -1) 819 int i = 0; 820 uint64_t imu_ts = UINT64_MAX; 821 xrt_vec3 _; 822 while (m_ff_vec3_f32_get(t.gyro_ff, i, &_, &imu_ts)) { 823 if ((int64_t)imu_ts < base_rel_ts) { 824 i--; // Back to the oldest newer-than-SLAM IMU index (or -1) 825 break; 826 } 827 i++; 828 } 829 830 if (i == -1) { 831 SLAM_WARN("No IMU samples received after latest SLAM pose (and frame)"); 832 } 833 834 xrt_space_relation integ_rel = base_rel; 835 timepoint_ns integ_rel_ts = base_rel_ts; 836 xrt_quat &o = integ_rel.pose.orientation; 837 xrt_vec3 &p = integ_rel.pose.position; 838 xrt_vec3 &w = integ_rel.angular_velocity; 839 xrt_vec3 &v = integ_rel.linear_velocity; 840 bool clamped = false; // If when_ns is older than the latest IMU ts 841 842 while (i >= 0) { // Decreasing i increases timestamp 843 // Get samples 844 xrt_vec3 g{}; 845 xrt_vec3 a{}; 846 uint64_t g_ts{}; 847 uint64_t a_ts{}; 848 bool got = true; 849 got &= m_ff_vec3_f32_get(t.gyro_ff, i, &g, &g_ts); 850 got &= m_ff_vec3_f32_get(t.accel_ff, i, &a, &a_ts); 851 timepoint_ns ts = g_ts; 852 853 854 // Checks 855 if (ts > when_ns) { 856 clamped = true; 857 //! @todo Instead of using same a and g values, do an interpolated sample like this: 858 // a = prev_a + ((when_ns - prev_ts) / (ts - prev_ts)) * (a - prev_a); 859 // g = prev_g + ((when_ns - prev_ts) / (ts - prev_ts)) * (g - prev_g); 860 ts = when_ns; // clamp ts to when_ns 861 } 862 SLAM_DASSERT(got && g_ts == a_ts, "Failure getting synced gyro and accel samples"); 863 SLAM_DASSERT(ts >= base_rel_ts, "Accessing imu sample that is older than latest SLAM pose"); 864 865 // Update time 866 float dt = (float)time_ns_to_s(ts - integ_rel_ts); 867 integ_rel_ts = ts; 868 869 // Integrate gyroscope 870 xrt_quat angvel_delta{}; 871 xrt_vec3 scaled_half_g = g * dt * 0.5f; 872 math_quat_exp(&scaled_half_g, &angvel_delta); // Same as using math_quat_from_angle_vector(g/dt) 873 math_quat_rotate(&o, &angvel_delta, &o); // Orientation 874 math_quat_rotate_derivative(&o, &g, &w); // Angular velocity 875 876 // Integrate accelerometer 877 xrt_vec3 world_accel{}; 878 math_quat_rotate_vec3(&o, &a, &world_accel); 879 world_accel += t.gravity_correction; 880 v += world_accel * dt; // Linear velocity 881 p += v * dt + world_accel * (dt * dt * 0.5f); // Position 882 883 if (clamped) { 884 break; 885 } 886 i--; 887 } 888 889 os_mutex_unlock(&t.lock_ff); 890 891 // Do the prediction based on the updated relation 892 double last_imu_to_now_dt = time_ns_to_s(when_ns - integ_rel_ts); 893 xrt_space_relation predicted_relation{}; 894 m_predict_relation(&integ_rel, last_imu_to_now_dt, &predicted_relation); 895 896 *out_relation = predicted_relation; 897} 898 899//! Return our best guess of the relation at time @p when_ns using all the data the tracker has. 900static void 901predict_pose(TrackerSlam &t, timepoint_ns when_ns, struct xrt_space_relation *out_relation) 902{ 903 XRT_TRACE_MARKER(); 904 905 bool valid_pred_type = t.pred_type >= SLAM_PRED_NONE && t.pred_type < SLAM_PRED_COUNT; 906 SLAM_DASSERT(valid_pred_type, "Invalid prediction type (%d)", t.pred_type); 907 908 // Get last relation computed purely from SLAM data 909 xrt_space_relation rel{}; 910 int64_t rel_ts; 911 bool empty = !t.slam_rels.get_latest(&rel_ts, &rel); 912 913 // Stop if there is no previous relation to use for prediction 914 if (empty) { 915 out_relation->relation_flags = XRT_SPACE_RELATION_BITMASK_NONE; 916 return; 917 } 918 919 // Use only last SLAM pose without prediction if PREDICTION_NONE 920 if (t.pred_type == SLAM_PRED_NONE) { 921 *out_relation = rel; 922 return; 923 } 924 925 // Use only SLAM data if asking for an old point in time or PREDICTION_SP_SO_SA_SL 926 SLAM_DASSERT_(rel_ts < INT64_MAX); 927 if (t.pred_type == SLAM_PRED_SP_SO_SA_SL || when_ns <= (int64_t)rel_ts) { 928 t.slam_rels.get(when_ns, out_relation); 929 return; 930 } 931 932 933 if (t.pred_type == SLAM_PRED_IP_IO_IA_IL) { 934 predict_pose_from_imu(t, when_ns, rel, (int64_t)rel_ts, out_relation); 935 return; 936 } 937 938 os_mutex_lock(&t.lock_ff); 939 940 // Update angular velocity with gyro data 941 if (t.pred_type >= SLAM_PRED_SP_SO_IA_SL) { 942 xrt_vec3 avg_gyro{}; 943 m_ff_vec3_f32_filter(t.gyro_ff, rel_ts, when_ns, &avg_gyro); 944 math_quat_rotate_derivative(&rel.pose.orientation, &avg_gyro, &rel.angular_velocity); 945 } 946 947 // Update linear velocity with accel data 948 if (t.pred_type >= SLAM_PRED_SP_SO_IA_IL) { 949 xrt_vec3 avg_accel{}; 950 m_ff_vec3_f32_filter(t.accel_ff, rel_ts, when_ns, &avg_accel); 951 xrt_vec3 world_accel{}; 952 math_quat_rotate_vec3(&rel.pose.orientation, &avg_accel, &world_accel); 953 world_accel += t.gravity_correction; 954 double slam_to_imu_dt = time_ns_to_s(t.last_imu_ts - rel_ts); 955 rel.linear_velocity += world_accel * slam_to_imu_dt; 956 } 957 958 os_mutex_unlock(&t.lock_ff); 959 960 // Do the prediction based on the updated relation 961 double slam_to_now_dt = time_ns_to_s(when_ns - rel_ts); 962 xrt_space_relation predicted_relation{}; 963 m_predict_relation(&rel, slam_to_now_dt, &predicted_relation); 964 965 *out_relation = predicted_relation; 966} 967 968//! Various filters to remove noise from the predicted trajectory. 969static void 970filter_pose(TrackerSlam &t, timepoint_ns when_ns, struct xrt_space_relation *out_relation) 971{ 972 XRT_TRACE_MARKER(); 973 974 if (t.filter.use_moving_average_filter) { 975 if (out_relation->relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) { 976 xrt_vec3 pos = out_relation->pose.position; 977 m_ff_vec3_f32_push(t.filter.pos_ff, &pos, when_ns); 978 } 979 980 if (out_relation->relation_flags & XRT_SPACE_RELATION_ORIENTATION_VALID_BIT) { 981 // Don't save w component as we can retrieve it knowing these are (almost) unit quaternions 982 xrt_vec3 rot = {out_relation->pose.orientation.x, out_relation->pose.orientation.y, 983 out_relation->pose.orientation.z}; 984 m_ff_vec3_f32_push(t.filter.rot_ff, &rot, when_ns); 985 } 986 987 // Get averages in time window 988 timepoint_ns window = t.filter.window * U_TIME_1MS_IN_NS; 989 xrt_vec3 avg_pos; 990 m_ff_vec3_f32_filter(t.filter.pos_ff, when_ns - window, when_ns, &avg_pos); 991 xrt_vec3 avg_rot; // Naive but good enough rotation average 992 m_ff_vec3_f32_filter(t.filter.rot_ff, when_ns - window, when_ns, &avg_rot); 993 994 // Considering the naive averaging this W is a bit wrong, but it feels reasonably well 995 float avg_rot_w = sqrtf(1 - (avg_rot.x * avg_rot.x + avg_rot.y * avg_rot.y + avg_rot.z * avg_rot.z)); 996 out_relation->pose.orientation = xrt_quat{avg_rot.x, avg_rot.y, avg_rot.z, avg_rot_w}; 997 out_relation->pose.position = avg_pos; 998 999 //! @todo Implement the quaternion averaging with a m_ff_vec4_f32 and 1000 //! normalization. Although it would be best to have a way of generalizing 1001 //! types before so as to not have redundant copies of ff logic. 1002 } 1003 1004 if (t.filter.use_exponential_smoothing_filter) { 1005 xrt_space_relation &last = t.filter.last; 1006 xrt_space_relation &target = t.filter.target; 1007 target = *out_relation; 1008 m_space_relation_interpolate(&last, &target, t.filter.alpha, target.relation_flags, &last); 1009 *out_relation = last; 1010 } 1011 1012 if (t.filter.use_one_euro_filter) { 1013 xrt_pose &p = out_relation->pose; 1014 if (out_relation->relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) { 1015 m_filter_euro_vec3_run(&t.filter.pos_oe, when_ns, &p.position, &p.position); 1016 } 1017 if (out_relation->relation_flags & XRT_SPACE_RELATION_ORIENTATION_VALID_BIT) { 1018 m_filter_euro_quat_run(&t.filter.rot_oe, when_ns, &p.orientation, &p.orientation); 1019 } 1020 } 1021} 1022 1023static void 1024setup_ui(TrackerSlam &t) 1025{ 1026 t.pred_combo.count = SLAM_PRED_COUNT; 1027 t.pred_combo.options = "None\0Interpolate SLAM poses\0Also gyro\0Also accel\0Latest IMU\0"; 1028 t.pred_combo.value = (int *)&t.pred_type; 1029 t.ui_sink = vector<u_sink_debug>(t.cam_count); 1030 for (size_t i = 0; i < t.ui_sink.size(); i++) { 1031 u_sink_debug_init(&t.ui_sink[i]); 1032 } 1033 os_mutex_init(&t.lock_ff); 1034 m_ff_vec3_f32_alloc(&t.gyro_ff, 1000); 1035 m_ff_vec3_f32_alloc(&t.accel_ff, 1000); 1036 m_ff_vec3_f32_alloc(&t.filter.pos_ff, 1000); 1037 m_ff_vec3_f32_alloc(&t.filter.rot_ff, 1000); 1038 1039 u_var_add_root(&t, "SLAM Tracker", true); 1040 u_var_add_log_level(&t, &t.log_level, "Log Level"); 1041 u_var_add_bool(&t, &t.submit, "Submit data to SLAM"); 1042 1043 u_var_button_cb reset_state_cb = [](void *t_ptr) { 1044 TrackerSlam &t = *(TrackerSlam *)t_ptr; 1045 1046 vit_result_t vres = t.vit.tracker_reset(t.tracker); 1047 if (vres != VIT_SUCCESS) { 1048 SLAM_WARN("Failed to reset VIT tracker"); 1049 } 1050 }; 1051 t.reset_state_btn.cb = reset_state_cb; 1052 t.reset_state_btn.ptr = &t; 1053 u_var_add_button(&t, &t.reset_state_btn, "Reset tracker state"); 1054 1055 u_var_add_bool(&t, &t.gt.override_tracking, "Track with ground truth (if available)"); 1056 euroc_recorder_add_ui(t.euroc_recorder, &t, ""); 1057 1058 u_var_add_gui_header(&t, NULL, "Trajectory Filter"); 1059 u_var_add_bool(&t, &t.filter.use_moving_average_filter, "Enable moving average filter"); 1060 u_var_add_f64(&t, &t.filter.window, "Window size (ms)"); 1061 u_var_add_bool(&t, &t.filter.use_exponential_smoothing_filter, "Enable exponential smoothing filter"); 1062 u_var_add_f32(&t, &t.filter.alpha, "Smoothing factor"); 1063 u_var_add_bool(&t, &t.filter.use_one_euro_filter, "Enable one euro filter"); 1064 u_var_add_f32(&t, &t.filter.pos_oe.base.fc_min, "Position minimum cutoff"); 1065 u_var_add_f32(&t, &t.filter.pos_oe.base.beta, "Position beta speed"); 1066 u_var_add_f32(&t, &t.filter.pos_oe.base.fc_min_d, "Position minimum delta cutoff"); 1067 u_var_add_f32(&t, &t.filter.rot_oe.base.fc_min, "Orientation minimum cutoff"); 1068 u_var_add_f32(&t, &t.filter.rot_oe.base.beta, "Orientation beta speed"); 1069 u_var_add_f32(&t, &t.filter.rot_oe.base.fc_min_d, "Orientation minimum delta cutoff"); 1070 1071 u_var_add_gui_header(&t, NULL, "Prediction"); 1072 u_var_add_combo(&t, &t.pred_combo, "Prediction Type"); 1073 u_var_add_i32(&t, &t.dbg_pred_every, "Debug prediction skips (try 30)"); 1074 u_var_add_ro_ff_vec3_f32(&t, t.gyro_ff, "Gyroscope"); 1075 u_var_add_ro_ff_vec3_f32(&t, t.accel_ff, "Accelerometer"); 1076 u_var_add_f32(&t, &t.gravity_correction.z, "Gravity Correction"); 1077 for (size_t i = 0; i < t.ui_sink.size(); i++) { 1078 char label[64] = {0}; 1079 snprintf(label, sizeof(label), "Camera %zu", i); 1080 u_var_add_sink_debug(&t, &t.ui_sink[i], label); 1081 } 1082 1083 u_var_add_gui_header(&t, NULL, "Stats"); 1084 u_var_add_ro_raw_text(&t, "\nRecord to CSV files", "Record to CSV files"); 1085 u_var_add_bool(&t, &t.slam_traj_writer->enabled, "Record tracked trajectory"); 1086 u_var_add_bool(&t, &t.pred_traj_writer->enabled, "Record predicted trajectory"); 1087 u_var_add_bool(&t, &t.filt_traj_writer->enabled, "Record filtered trajectory"); 1088 u_var_add_bool(&t, &t.slam_times_writer->enabled, "Record tracker times"); 1089 u_var_add_bool(&t, &t.slam_features_writer->enabled, "Record feature count"); 1090 timing_ui_setup(t); 1091 features_ui_setup(t); 1092 // Later, gt_ui_setup will setup the tracking error UI if ground truth becomes available 1093} 1094 1095static void 1096add_camera_calibration(const TrackerSlam &t, const t_slam_camera_calibration *calib, uint32_t cam_index) 1097{ 1098 const t_camera_calibration &view = calib->base; 1099 1100 vit_camera_calibration params = {}; 1101 params.camera_index = cam_index; 1102 params.width = view.image_size_pixels.w; 1103 params.height = view.image_size_pixels.h; 1104 params.frequency = calib->frequency; 1105 1106 params.fx = view.intrinsics[0][0]; 1107 params.fy = view.intrinsics[1][1]; 1108 params.cx = view.intrinsics[0][2]; 1109 params.cy = view.intrinsics[1][2]; 1110 1111 switch (view.distortion_model) { 1112 case T_DISTORTION_OPENCV_RADTAN_8: { 1113 params.model = VIT_CAMERA_DISTORTION_RT8; 1114 const size_t size = sizeof(struct t_camera_calibration_rt8_params) + sizeof(double); 1115 params.distortion_count = size / sizeof(double); 1116 SLAM_ASSERT_(params.distortion_count == 9); 1117 1118 memcpy(params.distortion, &view.rt8, size); 1119 1120 // -1 metric radius tells Basalt to estimate the metric radius on its own. 1121 params.distortion[8] = -1.f; 1122 break; 1123 } 1124 case T_DISTORTION_WMR: { 1125 params.model = VIT_CAMERA_DISTORTION_RT8; 1126 const size_t size = sizeof(struct t_camera_calibration_rt8_params) + sizeof(double); 1127 params.distortion_count = size / sizeof(double); 1128 SLAM_ASSERT_(params.distortion_count == 9); 1129 1130 memcpy(params.distortion, &view.wmr, size); 1131 1132 params.distortion[8] = view.wmr.rpmax; 1133 1134 break; 1135 } 1136 case T_DISTORTION_FISHEYE_KB4: { 1137 params.model = VIT_CAMERA_DISTORTION_KB4; 1138 const size_t size = sizeof(struct t_camera_calibration_kb4_params); 1139 params.distortion_count = size / sizeof(double); 1140 SLAM_ASSERT_(params.distortion_count == 4); 1141 1142 memcpy(params.distortion, &view.kb4, size); 1143 break; 1144 } 1145 default: 1146 SLAM_ASSERT(false, "SLAM doesn't support distortion type %s", 1147 t_stringify_camera_distortion_model(view.distortion_model)); 1148 break; 1149 } 1150 1151 xrt_matrix_4x4 T; // Row major T_imu_cam 1152 math_matrix_4x4_transpose(&calib->T_imu_cam, &T); 1153 1154 // Converts the xrt_matrix_4x4 from float to double 1155 for (size_t i = 0; i < ARRAY_SIZE(params.transform); ++i) 1156 params.transform[i] = T.v[i]; 1157 1158 vit_result_t vres = t.vit.tracker_add_camera_calibration(t.tracker, &params); 1159 if (vres != VIT_SUCCESS) { 1160 SLAM_ERROR("Failed to add camera calibration for camera %u", cam_index); 1161 } 1162} 1163 1164static void 1165add_imu_calibration(const TrackerSlam &t, const t_slam_imu_calibration *imu_calib) 1166{ 1167 vit_imu_calibration_t params = {}; 1168 params.imu_index = 0; 1169 params.frequency = imu_calib->frequency; 1170 1171 // TODO improve memcpy size calculation 1172 1173 const t_inertial_calibration &accel = imu_calib->base.accel; 1174 memcpy(params.accel.transform, accel.transform, sizeof(double) * 9); 1175 memcpy(params.accel.offset, accel.offset, sizeof(double) * 3); 1176 memcpy(params.accel.bias_std, accel.bias_std, sizeof(double) * 3); 1177 memcpy(params.accel.noise_std, accel.noise_std, sizeof(double) * 3); 1178 1179 const t_inertial_calibration &gyro = imu_calib->base.gyro; 1180 memcpy(params.gyro.transform, gyro.transform, sizeof(double) * 9); 1181 memcpy(params.gyro.offset, gyro.offset, sizeof(double) * 3); 1182 memcpy(params.gyro.bias_std, gyro.bias_std, sizeof(double) * 3); 1183 memcpy(params.gyro.noise_std, gyro.noise_std, sizeof(double) * 3); 1184 1185 vit_result_t vres = t.vit.tracker_add_imu_calibration(t.tracker, &params); 1186 if (vres != VIT_SUCCESS) { 1187 SLAM_ERROR("Failed to add imu calibration"); 1188 } 1189} 1190 1191static void 1192send_calibration(const TrackerSlam &t, const t_slam_calibration &c) 1193{ 1194 // Try to send camera calibration data to the SLAM system 1195 if (t.exts.has_add_camera_calibration) { 1196 for (int i = 0; i < c.cam_count; i++) { 1197 SLAM_INFO("Sending Camera %d calibration from Monado", i); 1198 add_camera_calibration(t, &c.cams[i], i); 1199 } 1200 } else { 1201 SLAM_WARN("Tracker doesn't support camera calibration"); 1202 } 1203 1204 // Try to send IMU calibration data to the SLAM system 1205 if (t.exts.has_add_imu_calibration) { 1206 SLAM_INFO("Sending IMU calibration from Monado"); 1207 add_imu_calibration(t, &c.imu); 1208 } else { 1209 SLAM_WARN("Tracker doesn't support IMU calibration"); 1210 } 1211} 1212 1213} // namespace xrt::auxiliary::tracking::slam 1214 1215using namespace xrt::auxiliary::tracking::slam; 1216 1217/* 1218 * 1219 * External functions 1220 * 1221 */ 1222 1223//! Get a filtered prediction from the SLAM tracked poses. 1224extern "C" void 1225t_slam_get_tracked_pose(struct xrt_tracked_slam *xts, timepoint_ns when_ns, struct xrt_space_relation *out_relation) 1226{ 1227 XRT_TRACE_MARKER(); 1228 1229 auto &t = *container_of(xts, TrackerSlam, base); 1230 1231 //! @todo This should not be cached, the same timestamp can be requested at a 1232 //! later time on the frame for a better prediction. 1233 if (when_ns == t.last_ts) { 1234 *out_relation = t.last_rel; 1235 return; 1236 } 1237 1238 flush_poses(t); 1239 1240 predict_pose(t, when_ns, out_relation); 1241 t.pred_traj_writer->push({when_ns, out_relation->pose}); 1242 1243 filter_pose(t, when_ns, out_relation); 1244 t.filt_traj_writer->push({when_ns, out_relation->pose}); 1245 1246 t.last_rel = *out_relation; 1247 t.last_ts = when_ns; 1248 1249 if (t.gt.override_tracking) { 1250 out_relation->pose = gt2xr_pose(t.gt.origin, get_gt_pose_at(*t.gt.trajectory, when_ns)); 1251 } 1252} 1253 1254//! Receive and register ground truth to use for trajectory error metrics. 1255extern "C" void 1256t_slam_gt_sink_push(struct xrt_pose_sink *sink, xrt_pose_sample *sample) 1257{ 1258 XRT_TRACE_MARKER(); 1259 1260 auto &t = *container_of(sink, TrackerSlam, gt_sink); 1261 1262 if (t.gt.trajectory->empty()) { 1263 t.gt.origin = sample->pose; 1264 gt_ui_setup(t); 1265 } 1266 1267 t.gt.trajectory->insert_or_assign(sample->timestamp_ns, sample->pose); 1268 xrt_sink_push_pose(t.euroc_recorder->gt, sample); 1269} 1270 1271//! Receive and register masks to use in the next image 1272extern "C" void 1273t_slam_hand_mask_sink_push(struct xrt_hand_masks_sink *sink, struct xrt_hand_masks_sample *hand_masks) 1274{ 1275 XRT_TRACE_MARKER(); 1276 1277 auto &t = *container_of(sink, TrackerSlam, hand_masks_sink); 1278 unique_lock lock(t.last_hand_masks_mutex); 1279 t.last_hand_masks = *hand_masks; 1280} 1281 1282//! Receive and send IMU samples to the external SLAM system 1283extern "C" void 1284t_slam_receive_imu(struct xrt_imu_sink *sink, struct xrt_imu_sample *s) 1285{ 1286 XRT_TRACE_MARKER(); 1287 1288 auto &t = *container_of(sink, TrackerSlam, imu_sink); 1289 1290 timepoint_ns ts = s->timestamp_ns; 1291 xrt_vec3_f64 a = s->accel_m_s2; 1292 xrt_vec3_f64 w = s->gyro_rad_secs; 1293 1294 timepoint_ns now = (timepoint_ns)os_monotonic_get_ns(); 1295 SLAM_TRACE("[%ld] imu t=%ld a=[%f,%f,%f] w=[%f,%f,%f]", now, ts, a.x, a.y, a.z, w.x, w.y, w.z); 1296 // Check monotonically increasing timestamps 1297 if (ts <= t.last_imu_ts) { 1298 SLAM_WARN("Sample (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts, t.last_imu_ts, 1299 t.last_imu_ts - ts); 1300 return; 1301 } 1302 t.last_imu_ts = ts; 1303 1304 //! @todo There are many conversions like these between xrt and 1305 //! slam_tracker.hpp types. Implement a casting mechanism to avoid copies. 1306 vit_imu_sample_t sample = {}; 1307 sample.timestamp = ts; 1308 sample.ax = a.x; 1309 sample.ay = a.y; 1310 sample.az = a.z; 1311 sample.wx = w.x; 1312 sample.wy = w.y; 1313 sample.wz = w.z; 1314 1315 if (t.submit) { 1316 t.vit.tracker_push_imu_sample(t.tracker, &sample); 1317 } 1318 1319 xrt_sink_push_imu(t.euroc_recorder->imu, s); 1320 1321 struct xrt_vec3 gyro = {(float)w.x, (float)w.y, (float)w.z}; 1322 struct xrt_vec3 accel = {(float)a.x, (float)a.y, (float)a.z}; 1323 os_mutex_lock(&t.lock_ff); 1324 m_ff_vec3_f32_push(t.gyro_ff, &gyro, ts); 1325 m_ff_vec3_f32_push(t.accel_ff, &accel, ts); 1326 os_mutex_unlock(&t.lock_ff); 1327} 1328 1329//! Push the frame to the external SLAM system 1330static void 1331receive_frame(TrackerSlam &t, struct xrt_frame *frame, uint32_t cam_index) 1332{ 1333 XRT_TRACE_MARKER(); 1334 1335 SLAM_DASSERT_(frame->timestamp < INT64_MAX); 1336 1337 // Return early if we don't submit 1338 if (!t.submit) { 1339 return; 1340 } 1341 1342 if (cam_index == t.cam_count - 1) { 1343 flush_poses(t); // Useful to flush SLAM poses when no openxr app is open 1344 } 1345 1346 SLAM_DASSERT(t.last_cam_ts[0] != INT64_MIN || cam_index == 0, "First frame was not a cam0 frame"); 1347 1348 // Check monotonically increasing timestamps 1349 timepoint_ns &last_ts = t.last_cam_ts[cam_index]; 1350 timepoint_ns ts = (int64_t)frame->timestamp; 1351 SLAM_TRACE("[%" PRId64 "] cam%d frame t=%" PRId64, os_monotonic_get_ns(), cam_index, ts); 1352 if (last_ts >= ts) { 1353 SLAM_WARN("Frame (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts, last_ts, 1354 last_ts - ts); 1355 } 1356 last_ts = ts; 1357 1358 // Construct and send the image sample 1359 vit_img_sample sample = {}; 1360 sample.cam_index = cam_index; 1361 sample.timestamp = ts; 1362 1363 sample.data = frame->data; 1364 sample.width = frame->width; 1365 sample.height = frame->height; 1366 sample.stride = frame->stride; 1367 sample.size = frame->size; 1368 1369 // TODO check format before 1370 switch (frame->format) { 1371 case XRT_FORMAT_L8: sample.format = VIT_IMAGE_FORMAT_L8; break; 1372 case XRT_FORMAT_R8G8B8: sample.format = VIT_IMAGE_FORMAT_R8G8B8; break; 1373 default: SLAM_ERROR("Unknown image format"); return; 1374 } 1375 1376 xrt_hand_masks_sample hand_masks{}; 1377 { 1378 unique_lock lock(t.last_hand_masks_mutex); 1379 hand_masks = t.last_hand_masks; 1380 } 1381 1382 auto &view = hand_masks.views[cam_index]; 1383 std::vector<vit_mask_t> masks; 1384 if (view.enabled) { 1385 for (auto &hand : view.hands) { 1386 if (!hand.enabled) { 1387 continue; 1388 } 1389 vit_mask_t mask{}; 1390 mask.x = hand.rect.x; 1391 mask.y = hand.rect.y; 1392 mask.w = hand.rect.w; 1393 mask.h = hand.rect.h; 1394 masks.push_back(mask); 1395 } 1396 1397 sample.mask_count = masks.size(); 1398 sample.masks = masks.empty() ? nullptr : masks.data(); 1399 } 1400 1401 { 1402 XRT_TRACE_IDENT(slam_push); 1403 t.vit.tracker_push_img_sample(t.tracker, &sample); 1404 } 1405} 1406 1407#define DEFINE_RECEIVE_CAM(cam_id) \ 1408 extern "C" void t_slam_receive_cam##cam_id(struct xrt_frame_sink *sink, struct xrt_frame *frame) \ 1409 { \ 1410 auto &t = *container_of(sink, TrackerSlam, cam_sinks[cam_id]); \ 1411 receive_frame(t, frame, cam_id); \ 1412 u_sink_debug_push_frame(&t.ui_sink[cam_id], frame); \ 1413 xrt_sink_push_frame(t.euroc_recorder->cams[cam_id], frame); \ 1414 } 1415 1416DEFINE_RECEIVE_CAM(0) 1417DEFINE_RECEIVE_CAM(1) 1418DEFINE_RECEIVE_CAM(2) 1419DEFINE_RECEIVE_CAM(3) 1420DEFINE_RECEIVE_CAM(4) 1421 1422//! Define a function for each XRT_TRACKING_MAX_SLAM_CAMS and reference it in this array 1423void (*t_slam_receive_cam[XRT_TRACKING_MAX_SLAM_CAMS])(xrt_frame_sink *, xrt_frame *) = { 1424 t_slam_receive_cam0, // 1425 t_slam_receive_cam1, // 1426 t_slam_receive_cam2, // 1427 t_slam_receive_cam3, // 1428 t_slam_receive_cam4, // 1429}; 1430 1431 1432extern "C" void 1433t_slam_node_break_apart(struct xrt_frame_node *node) 1434{ 1435 auto &t = *container_of(node, TrackerSlam, node); 1436 if (t.ovr_tracker != NULL) { 1437 t_openvr_tracker_stop(t.ovr_tracker); 1438 } 1439 1440 vit_result_t vres = t.vit.tracker_stop(t.tracker); 1441 if (vres != VIT_SUCCESS) { 1442 SLAM_ERROR("Failed to stop VIT tracker"); 1443 return; 1444 } 1445 1446 SLAM_DEBUG("SLAM tracker dismantled"); 1447} 1448 1449extern "C" void 1450t_slam_node_destroy(struct xrt_frame_node *node) 1451{ 1452 auto t_ptr = container_of(node, TrackerSlam, node); 1453 auto &t = *t_ptr; // Needed by SLAM_DEBUG 1454 SLAM_DEBUG("Destroying SLAM tracker"); 1455 if (t.ovr_tracker != NULL) { 1456 t_openvr_tracker_destroy(t.ovr_tracker); 1457 } 1458 delete t.gt.trajectory; 1459 delete t.slam_times_writer; 1460 delete t.slam_features_writer; 1461 delete t.slam_traj_writer; 1462 delete t.pred_traj_writer; 1463 delete t.filt_traj_writer; 1464 u_var_remove_root(t_ptr); 1465 for (size_t i = 0; i < t.ui_sink.size(); i++) { 1466 u_sink_debug_destroy(&t.ui_sink[i]); 1467 } 1468 m_ff_vec3_f32_free(&t.gyro_ff); 1469 m_ff_vec3_f32_free(&t.accel_ff); 1470 os_mutex_destroy(&t.lock_ff); 1471 m_ff_vec3_f32_free(&t.filter.pos_ff); 1472 m_ff_vec3_f32_free(&t.filter.rot_ff); 1473 1474 t_ptr->vit.tracker_destroy(t_ptr->tracker); 1475 t_vit_bundle_unload(&t_ptr->vit); 1476 1477 delete t_ptr; 1478} 1479 1480extern "C" int 1481t_slam_start(struct xrt_tracked_slam *xts) 1482{ 1483 auto &t = *container_of(xts, TrackerSlam, base); 1484 vit_result_t vres = t.vit.tracker_start(t.tracker); 1485 if (vres != VIT_SUCCESS) { 1486 SLAM_ERROR("Failed to start VIT tracker"); 1487 return -1; 1488 } 1489 1490 SLAM_DEBUG("SLAM tracker started"); 1491 return 0; 1492} 1493 1494extern "C" void 1495t_slam_fill_default_config(struct t_slam_tracker_config *config) 1496{ 1497 config->log_level = debug_get_log_option_slam_log(); 1498 config->vit_system_library_path = debug_get_option_vit_system_library_path(); 1499 config->slam_config = debug_get_option_slam_config(); 1500 config->slam_ui = debug_get_bool_option_slam_ui(); 1501 config->submit_from_start = debug_get_bool_option_slam_submit_from_start(); 1502 config->openvr_groundtruth_device = int(debug_get_num_option_slam_openvr_groundtruth_device()); 1503 config->prediction = t_slam_prediction_type(debug_get_num_option_slam_prediction_type()); 1504 config->write_csvs = debug_get_bool_option_slam_write_csvs(); 1505 config->csv_path = debug_get_option_slam_csv_path(); 1506 config->timing_stat = debug_get_bool_option_slam_timing_stat(); 1507 config->features_stat = debug_get_bool_option_slam_features_stat(); 1508 config->cam_count = int(debug_get_num_option_slam_cam_count()); 1509 config->slam_calib = NULL; 1510} 1511 1512extern "C" int 1513t_slam_create(struct xrt_frame_context *xfctx, 1514 struct t_slam_tracker_config *config, 1515 struct xrt_tracked_slam **out_xts, 1516 struct xrt_slam_sinks **out_sink) 1517{ 1518 struct t_slam_tracker_config default_config = {}; 1519 if (config == nullptr) { 1520 t_slam_fill_default_config(&default_config); 1521 config = &default_config; 1522 } 1523 1524 enum u_logging_level log_level = config->log_level; 1525 1526 std::unique_ptr<TrackerSlam> t_ptr = std::make_unique<TrackerSlam>(); 1527 TrackerSlam &t = *t_ptr; 1528 1529 t.log_level = log_level; 1530 1531 SLAM_INFO("Loading VIT system library from VIT_SYSTEM_LIBRARY_PATH='%s'", config->vit_system_library_path); 1532 1533 if (!t_vit_bundle_load(&t.vit, config->vit_system_library_path)) { 1534 SLAM_ERROR("Failed to load VIT system library from '%s'", config->vit_system_library_path); 1535 return -1; 1536 } 1537 1538 // Check the user has provided a SLAM_CONFIG file 1539 const char *config_file = config->slam_config; 1540 bool some_calib = config->slam_calib != nullptr; 1541 if (!config_file && !some_calib) { 1542 SLAM_WARN("Unable to determine sensor calibration, did you forget to set SLAM_CONFIG?"); 1543 return -1; 1544 } 1545 1546 struct vit_config system_config = {}; 1547 system_config.file = config_file; 1548 system_config.cam_count = config->cam_count; 1549 system_config.show_ui = config->slam_ui; 1550 1551 vit_result_t vres = t.vit.tracker_create(&system_config, &t.tracker); 1552 if (vres != VIT_SUCCESS) { 1553 SLAM_ERROR("Failed to create VIT tracker (%d)", vres); 1554 return -1; 1555 } 1556 1557 vres = t.vit.tracker_get_supported_extensions(t.tracker, &t.exts); 1558 if (vres != VIT_SUCCESS) { 1559 SLAM_ERROR("Failed to get VIT tracker supported extensions (%d)", vres); 1560 return -1; 1561 } 1562 1563 t.base.get_tracked_pose = t_slam_get_tracked_pose; 1564 1565 if (!config_file) { 1566 SLAM_INFO("Using calibration from driver and default pipeline settings"); 1567 send_calibration(t, *config->slam_calib); // Not null because of `some_calib` 1568 } else { 1569 SLAM_INFO("Using sensor calibration provided by the SLAM_CONFIG file"); 1570 } 1571 1572 SLAM_ASSERT(t_slam_receive_cam[ARRAY_SIZE(t_slam_receive_cam) - 1] != nullptr, "See `cam_sink_push` docs"); 1573 t.sinks.cam_count = config->cam_count; 1574 for (int i = 0; i < XRT_TRACKING_MAX_SLAM_CAMS; i++) { 1575 t.cam_sinks[i].push_frame = t_slam_receive_cam[i]; 1576 t.sinks.cams[i] = &t.cam_sinks[i]; 1577 } 1578 1579 t.imu_sink.push_imu = t_slam_receive_imu; 1580 t.sinks.imu = &t.imu_sink; 1581 1582 t.gt_sink.push_pose = t_slam_gt_sink_push; 1583 t.sinks.gt = &t.gt_sink; 1584 1585 t.hand_masks_sink.push_hand_masks = t_slam_hand_mask_sink_push; 1586 t.sinks.hand_masks = &t.hand_masks_sink; 1587 1588 t.submit = config->submit_from_start; 1589 t.cam_count = config->cam_count; 1590 1591 t.node.break_apart = t_slam_node_break_apart; 1592 t.node.destroy = t_slam_node_destroy; 1593 1594 xrt_frame_context_add(xfctx, &t.node); 1595 1596 t.euroc_recorder = euroc_recorder_create(xfctx, NULL, t.cam_count, false); 1597 1598 t.last_imu_ts = INT64_MIN; 1599 t.last_cam_ts = vector<timepoint_ns>(t.cam_count, INT64_MIN); 1600 t.last_hand_masks = xrt_hand_masks_sample{}; 1601 1602 t.pred_type = config->prediction; 1603 1604 m_filter_euro_vec3_init(&t.filter.pos_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta); 1605 m_filter_euro_quat_init(&t.filter.rot_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta); 1606 1607 t.gt.trajectory = new Trajectory{}; 1608 1609 // Setup CSV files 1610 bool write_csvs = config->write_csvs; 1611 string dir = config->csv_path; 1612 t.slam_times_writer = new TimingWriter(dir, "timing.csv", write_csvs, t.timing.columns); 1613 t.slam_features_writer = new FeaturesWriter(dir, "features.csv", write_csvs, t.cam_count); 1614 t.slam_traj_writer = new TrajectoryWriter(dir, "tracking.csv", write_csvs); 1615 t.pred_traj_writer = new TrajectoryWriter(dir, "prediction.csv", write_csvs); 1616 t.filt_traj_writer = new TrajectoryWriter(dir, "filtering.csv", write_csvs); 1617 1618 setup_ui(t); 1619 1620 // Setup OpenVR groundtruth tracker 1621 if (config->openvr_groundtruth_device > 0) { 1622 enum openvr_device dev_class = openvr_device(config->openvr_groundtruth_device); 1623 const double freq = 1000.0f; 1624 t.ovr_tracker = t_openvr_tracker_create(freq, &dev_class, &t.sinks.gt, 1); 1625 if (t.ovr_tracker != NULL) { 1626 t_openvr_tracker_start(t.ovr_tracker); 1627 } 1628 } 1629 1630 // Get ownership 1631 TrackerSlam *tracker = t_ptr.release(); 1632 1633 *out_xts = &tracker->base; 1634 *out_sink = &tracker->sinks; 1635 1636 SLAM_DEBUG("SLAM tracker created"); 1637 return 0; 1638}