A Modular Framework for Next-Generation Visual SLAM Systems
# Install system dependencies
sudo apt-get update
sudo apt-get install wget git cmake vim clang libclang-dev pkg-config libc6-dbg gdb valgrind libgtk2.0-dev libboost-all-dev libmetis-dev
# Install Rust
wget https://raw.githubusercontent.com/rust-lang/rustup/master/rustup-init.sh
chmod +x rustup-init.sh
./rustup-init.sh -y
# Create directories for bauhaus project, clone repo
mkdir ~/bauhaus-home && cd ~/bauhaus-home
git clone git@github.com:droneslab/bauhaus.git
mkdir ~/bauhaus-home/depends
# Upgrade version of cmake (needed for C++ bindings)
# Only do this for ubuntu 20, ignore for ubuntu 22! Didn't test on earlier versions than 20.
cd ~/bauhaus-home/depends
sudo apt remove --purge cmake && hash -r
sudo apt install build-essential libssl-dev
sudo wget https://github.com/Kitware/CMake/releases/download/v3.20.2/cmake-3.20.2.tar.gz
tar -zxvf cmake-3.20.2.tar.gz
cd cmake-3.20.2 && ./bootstrap && make && make install
# Install eigen
# This installs 3.3: sudo apt-get install libeigen3-dev
cd ~/bauhaus-home/depends/
wget https://gitlab.com/libeigen/eigen/-/archive/3.2.10/eigen-3.2.10.tar.gz
tar –xvzf eigen-3.2.10.tar.gz
cd eigen-3.2.10 && mkdir build && cd build
cmake ..
make
sudo make install
export EIGEN3_INCLUDE_DIR=/usr/local/include
# Install OpenCV
cd ~/bauhaus-home/depends
git clone https://github.com/opencv/opencv.git
cd opencv && git checkout tags/4.5.4 && cd ..
git clone https://github.com/opencv/opencv_contrib.git
cd opencv_contrib && git checkout tags/4.5.4 && cd ..
cd opencv && mkdir build && cd build
cmake -DOPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules -DBUILD_opencv_xfeatures2d=ON -DOPENCV_ENABLE_NONFREE=ON -DWITH_GTK=ON -DWITH_EIGEN=OFF ..
make -j4 install
# Install Pangolin
cd ~/bauhaus-home/depends
git clone --recursive https://github.com/stevenlovegrove/Pangolin.git
cd Pangolin
./scripts/install_prerequisites.sh recommended
cmake -B build
cmake --build build
# Install gtsam
# cd ~/bauhaus-home/depends
# git clone git@github.com:ssemenova/gtsam-rs.git
cd ~/bauhaus-home/bauhaus/gtsam-rs/sys/3rd/
git clone https://github.com/borglab/gtsam.git --branch 4.2
cd ../../
cargo build --release
# Build bauhaus
cd ~/bauhaus-home/bauhaus/bauhaus/
cargo build --releaseCommon Build Problems
- Segfaults inside orbslam-bindings
- If you have two versions of OpenCV (for example, one from apt that gets downloaded when you install ROS and the one you installed in the instructions above) then you can get segfaults inside the C++ OpenCV code in
orbslam-bindings. This occurs because C++ finds one OpenCV version (usually the ROS version) and Rust finds a different one (usually the system-installed one). The easiest workaround is just to uninstall ROS and the apt version of OpenCV. It should be possible to inform the rust OpenCV package to look for the right version with environment variables like this but I have not tested it.
- If you have two versions of OpenCV (for example, one from apt that gets downloaded when you install ROS and the one you installed in the instructions above) then you can get segfaults inside the C++ OpenCV code in
If you want to run bauhaus inside a docker container instead:
- Install docker
- (Macs only) Download Xquartz
- Create bauhaus container (linux):
Create bauhaus container (mac):
cd bauhaus ./docker_run.sh [pathToDatasetDirectory]cd bauhaus ./docker_run_mac.sh [pathToDatasetDirectory] - Using rust-analyzer in VSCode typically gives errors about not finding opencv. To resolve these errors, you need to open VSCode inside the container:
- Download the Remote - Containers extension on VSCode
- Follow instructions for quick start to open VSCode inside container. This opens a new VSCode window
- For compile-time code checking, in the new VSCode window, install rust-analyzer extension
If you want to set up ORB_SLAM3 to compare its performance against bauhaus You can also just follow the instructions in the ORB_SLAM3 repo, but if bauhaus is already set up then you only need to do these steps. Tested on Ubuntu 22.
sudo apt-get install libssl-dev
git clone git@github.com:droneslab/bauhaus-orbslam3.git
cd ORB_SLAM3
# If using bauhaus-orbslam3, don't have to do this.
# sed -i 's/++11/++14/g' CMakeLists.txt
./build.shRELEASE mode (slow building, fast execution). This is probably what you want to use
cargo build --release
cargo run --release [PATH TO DATASET SEQUENCE] [PATH TO SYSTEM CONFIG FILE] [PATH TO DATASET CONFIG FILE] [DATASET NAME]
For example: cargo run --release ~/euroc/MH_01_easy/ system_config.yaml config_datasets/EUROC.yaml eurocDEBUG mode (fast building, slow execution)
cargo build
cargo run [PATH TO DATASET] [PATH TO SYSTEM CONFIG FILE] [PATH TO DATASET CONFIG FILE] [DATASET NAME]Current supported datasets are tum, euroc, and kitti. We have dataset configuration files for these datasets in the bauhaus/config_datasets folder.
Using the Visualizer
- Download the foxglove application. This can be on any device (does not need to be the test device).
- Open foxglove, click on
layoutin the top right corner, thenimport from file. Load the file infoxglove/foxglovelayout.json. - Make sure the
visualizeractor inconfig.yamlis not commented out. In the actor settings, change the setting `stream" to either true or false (true will stream to a websocket, false will save to an mcap file for later replay). - Run the system. If you are streaming, the foxglove application should update with the visualization in a second or two. If you are not streaming, the system will write the file
bauhaus/results/out.mcapafter execution is over. To open this file in foxglove, clickfile > open local file.
For longer datasets, the mcap file size can get unreasonably large because it saves all the images. You can turn this off by setting the image_draw_type setting in the visualizer actor to none. The other valid options are plain (unmodified images), features (detected features), and featuresandmatches (current and previous image with feature matches highlighted).
cargo build # Either debug or release build works
rust-gdb --args target/debug/binbauhaus [DATASET] config.yamlYou can also use regular gdb instead of rust-gdb, but it doesn't work in all cases.
Run with address sanitizer:
RUSTFLAGS="-Z sanitizer=address" cargo run --target x86_64-unknown-linux-gnu [DATASET] config.yamlRun with valgrind and save output to log.txt:
cargo build # Either debug or release build works
valgrind target/debug/binbauhaus [DATASET] config.yaml > log.txt 2>&1The RwLock will deadlock if you take two locks in one scope, but this just shows up as an infinite loop on the command line. To find deadlocks, look inside main.rs for the word deadlock, uncomment the lines of code that spawn a thread. This runs a separate thread that periodically wakes up, checks for a deadlock, and prints out the thread info if there is one. Additionally (or alternatively) you can change all RwLocks to Mutexes, which should give an error if you try to lock twice. This should be pretty straightforward by changing pub type MapLock to Arc<Mutex<Map>> and then changing occurences of read() and write() to lock().
Use this for very basic timing information. It is not very accurate, so if you need to dig deep into performance you should follow the steps below to use the Tracy profiler instead.
- Print time of certain functions
- Add
#[time()]before the function name like this:#[time()] fn function() {}
- You can also customize the name of the timer like this:
#[time("MyFunction")]. This is useful if you have similarly-named functions.
- Add
- To print time of arbitrary sections of code
- Add
let timer = timer!()when you want to start timing. - It will print the time when
timergoes out of scope. Alternatively, you can set the end point by callingfinish!(timer)
- Add
- Compile/run the normal way, but make sure that
log_levelinconfig.yamlis set todebug.
This requires some setup, see here
cargo flamegraph -o flamegraph.svg --root --release --ignore-status -- ~/datasets/kitti_00_0/ config.yamlSet up
- Set up C++
Tracy C++ version has to be compatible with tracy-client version in Rust. See table here.
apt install libdbus-glib-1-dev libcapstone-dev cd ~/bauhaus-home git clone https://github.com/wolfpld/tracy.git cd tracy git checkout 897aec5 # Set to version 0.9.1 cd profiler/build/unix && make && cd ../../../ cd capture/build/unix && make && cd ../../../ cd csvexport/build/unix && make && cd ../../../
- Try to run
./tracy/profiler/build/unix/Tracy-release. If you get this error, you need to make this change to the tracy source code. - In bauhaus
Cargo.toml, set tracy-client features to "enable", like this:tracy-client = {version = "0.16.0", features = ["enable"] }
- Uncomment this line of code in
main.rs:let _client = tracy_client::Client::start();
Mark up code
- To time a region, add this line of code. Then the timing will be displayed when the variable goes out of scope.
You can either let the variable go out of scope, or purposefully drop it like this:
let _span = tracy_client::span!("search_for_triangulation");
drop(span);
- To add messages (shows up as a marker on the tracy log at the time the message was added):
The 2nd argument to the message function is the callstack depth, see the tracy-client documentation here
tracy_client::Client::running() .expect("message! without a running Client") .message("message text", 2);
- To plot custom information:
tracy_client::plot!("LBA: MPs to Optimize", mps_to_optimize as f64);
Run
- Two ways to run: showing the GUI while the program is running, or logging the output and viewing the results in the GUI afterward. Instructions below are for logging and viewing afterward, but if you want it in real-time you should be able to just run the
Tracy-releasecommand in step 3 instead of thecapture-releasecommand in step 1.
- Start the tracy client (this is from bauhaus-home directory)
./tracy/capture/build/unix/capture-release -o output.tracy
- Run bauhaus normally
- After bauhaus ends, tracy capture should have created
output.tracy. To view this, open the GUI application:./tracy/profiler/build/unix/Tracy-release
- Select
open saved tracein the GUI
- If you're on a mac and sshing into the bauhaus computer or running it in docker,
brew install tracywill actually work to show the tracy GUI. I could not get wayland forwarding to work.
- To export tracy statistics to a csv, run:
You can then further process this with the
./tracy/csvexport/build/unix/csvexport-release output.tracy
tracy.pyscript in the scripts directory.
Helpful links
- blog post on using tracy with rust
- tracy documentation(download documentation pdf here)
- Rust tracy-client crate
To compile C++ bindings with clang instead of g++, set these environment variables:
export CXX=/usr/bin/clang++
export CC=/usr/bin/clangAll the steps below reference the module Visualizer as an example.
- Add module in config
- In the
config.yamlfile, add the settings for your new module undermodules - Visualizer example:
- name: VISUALIZER file: vis actor_message: VisMsg actor_function: vis address: localhost port: !!str 7779 multithreaded: false threads: 1 possible_paths: - from: FeatureDetection to: KFDecision input: feature vectors output: frame, feature points
- In the
- Create a file for the new module
- Create a new file in
src/modules/to holds your module code. The filename should match that which you declared in the config file. Unless otherwise specific, you will be using this file for all the following steps. - Visualizer example:
src/modules/vis.rs.
- Create a new file in
- Create the actor
- Implement the constructor for the actor and the trait
Function. This trait tells axiom which function to run when the actor processes an incoming message. - Visualizer example (from
vis.rs):/// Constructor impl bauhausVis { pub fn new(id: String) -> bauhausVis { bauhausVis { traj_img: Mat::new_rows_cols_with_default(376, 500, core::CV_8UC3, core::Scalar::all(0.0)).unwrap(), cam_img: Mat::default(), traj_pos: DVVector3::zeros(), traj_rot: DVMatrix3::zeros(), id: id } } } /// Function trait impl Function for bauhausVis { fn handle(&mut self, _context: axiom::prelude::Context, message: Message) -> ActorResult<()> { self.visualize(_context, message).unwrap(); Ok(Status::done(())) } }
- Implement the constructor for the actor and the trait
- Create the message that the actor processes
- Create the struct and constructor for the message. Do this in
src/modules/yourmodulename.rsorsrc/modules/messages/newmessagename.rs. - Visualizer example:
/// Struct #[derive(Debug, Serialize, Deserialize)] pub struct VisMsg { new_pose: Pose, } /// Constructor impl VisMsg { pub fn new(pose: Pose, ids: std::collections::HashMap<String, axiom::actors::Aid>) -> Self { Self { new_pose: pose, } } }
- Create the struct and constructor for the message. Do this in
- Implement the actual actor functionality
- Finally, you can implement the functionality of the actor. This is the code that the actor runs on a new message.
- For the visualizer, this function is:
visualize(&mut self, context: Context, message: Message) -> ActorResult<()>
- Register the module in the system
- Tip: All the code sections in the following steps can be found by searching for the string
REGISTER MODULE - In
src/registered_modules.rs, add a string to refer to your module. Visualizer example:
pub static VISUALIZER: &str = "VISUALIZER";
- In
src/registered_modules.rs, undergetmethod, add a reference to your object. Visualizer example:
"vis" => FunctionProxy {function: Box::new(crate::modules::vis::bauhausVis::new(id.clone()))}
- In
src/modules/mod.rs, add your module. Visualizer example:
pub mod vis;
- Tip: All the code sections in the following steps can be found by searching for the string
You can add custom settings to the config.yaml file. These can be anything, but an example that is already implemented are the settings for camera calibration and ORB feature extraction.
To add a new setting, under system in the config file, add a new key-value pair. Supported types are String, bool, f64, and i32. You do not have to declare the type in the config file.
To load the new config file to memory when initializing the system, modify the function load_config() in src/main.rs to parse for your setting and insert it into the global params. This function already has examples so just copy one of them.
To use the setting in your code, reference it like:
let max_features: i32 = SETTINGS.get(SYSTEM, "max_features");Where max_features is the key you added in the config file.
Strings are formatted like: TODO (string)
- Feature implementation TO DOs
- MVP
- Sensor types
- IMU
- Stereo
- RGBD
- Modules
- relocalization
- loop closing
- Features
- multimaps
- reset
- timestamps -- playing frames based on timestamps in timestamps file, instead of a set fps
- visualizer
- Design TO DOs
- design
- fine-grained locking -- locking on singular keyframes
- concurrency -- concurrency control between local mapping and loop closing
- variable locations -- variables stored somewhere that isn't ideal ... e.g. global variables that could be messages, member variables that could be local to a function
- code organization -- messy code that could be cleaner
- rust issues -- things rust won't let us do. mostly for references to the map, like getting simultaneous mutable and immutable references to different parts of the map
- map connections -- places where mappoints or keyframes have connections to other parts of the map. it would be nice if we could guarantee that the connections are updated/correct rather than duplicating all these connections across all the objects and hoping we remember to update them correctly after a map modification
- timing -- things that take too long. mostly clones, sometimes LTO for ffi bindings
- design
- Notes
- note
- paper note
- testing
design timestamps
Detailed Information
To get link time optimization performed on the binary created by rustc (the Rust compiler), it is necessary to use the same versions of LLVM/clang that the current Rust toolchain (in the rust toolchain file in bauhaus) uses to compile the C++ code. If the Rust toolchain is updated to the latest stable/nightly offered by the Rust language maintainers, chances are that most Linux distributions would not have the correct versions of LLVM/clang. This is because the Rust toolchain is updated quite frequently. If the versions do not match, then lld (the linker) will not be able to link the code successfully at the final step.
Now this poses another problem, namely having multiple versions of clang. One that is installed from the repositories of the distribution and the one that we just compiled. The trick here is to simply build the compiler and not install it. The install operation
normally represented by make install does not have to be run. If it were to be installed, there is a good chance that the operating system might break. All the CMakeLists.txt files for compiling the C++ code need to be modified to accomodate the use of a specific compiler.
This is done using set(CMAKE_CXX_COMPILER "path/to/compiler").
The linker lld, will also have to be compiled along with clang and llvm. The path to the linker must also be mentioned in the CMake file. This is done using add_link_options(-fuse-ld=/path/to/linker/ld.lld)
Along with this, set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) must also be specified in the CMakeLists.txt files to enable link time optimization. The version of CMake has to be 3.29 or above for this flag to work. Here we again arrive at the problem of packages in a distribution's repository lagging behind the package development. This will most likely mean that cmake would have to be compiled from source or the binary with the right version would have to be used. This has to be added to the path by modifying $PATH to accomodate the location where the cmake binary lives (using export $PATH). Also one must pass the following RUSTFLAGS to cargo for enabling cross language link time optimization - RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" cargo build --release. Note - Passing the -flto flag in the build.rs file does not work.
If running the binary on a standalone computer which is different from the computer that compiled the binary, the standalone computer will require a glibc that matches the version required by clang. If there is no match, the binary will not run.
To get the binary running, the right version of glibc's shared library (libc.so) has to be compiled from scratch and must be loaded using export LD_LIBRARY_PATH=/path/to/newly/built/libc.so.
The dynamic linker as well (ie something that looks like ld-linux-x86-64.so.2) in built the glibc directory has to used on the binary. This is accomplished with patchelf, a tool that facilitates the use of a different dynamic linker apart from the one in the system's /usr.
The patchelf tool can be installed from the distribution's repos. For a Debian based system, this would be sudo apt-get install patchelf. The executable for bauhaus would lie within /target/release, it should be called binbauhaus.
Specific Steps
Building bauhaus with LTO
- Build the corresponding llvm tools/clang from source. Detailed instructions are given at the following link - https://llvm.org/docs/CMake.html
- The source files for the above are found here - https://github.com/llvm/llvm-project. Sometimes, volunteers may upload the binaries which may be used, however if no binaries exist, the above mentioned will have to be built from source.
- Specify the correct compiler and linker path (path specification mentioned above) in all the
CMakeLists.txt- These would be the ones corresponding to g2o,orb_slam3 and DBoW2. - Build a version of cmake above 3.29 or get a copy of the binary from the following link - https://github.com/Kitware/CMake/releases
- Add the location of the cmake executable to the $PATH variable using
export $PATH. (To do this, useecho $PATHand add the location at the beginning using the syntax used in the$PATHvariable). - For compiling the project, invoke cargo as follows:
RUSTFLAGS="-Clinker-plugin-lto -Clinker=/path/to/clang -Clink-arg=-fuse-ld=lld" cargo build --release
Executing the bauhaus binary on a non-developmental device (if running it normally fails)
- Get a copy of the glibc required by device from the following website - https://ftp.gnu.org/gnu/glibc/. This information (version) will be seen in the terminal if binbauhaus is unable to execute.
- Compile glibc from source, the instructions are available at the following website - https://sourceware.org/glibc/wiki/Testing/Builds.
- Get the binbauhaus executable to use the new dynamic linker that we just built using
patchelf. - Install patchelf using
sudo apt-get install patchelffor Debian based distros orsudo pacman -S patchelffor Arch based distros. - Use the following command in the terminal -
patchelf --set-interpreter /path/where/newly/compiled/ld-linux-x86-64.so/lives /path/of/bauhaus_executable(binbauhaus). - Add the directory containing libc.so to
export LD_LIBRARY_PATH=/path/of/library/with/libc, this would be where the glibc that we built from source exists.