English | 简体中文
Visual-inertial odometry (VIO) in C++26. The backend uses GTSAM's
IncrementalFixedLagSmoother to solve IMU preintegration factors and visual reprojection factors
in a single factor graph, marginalizing states that leave the time window.
- Language standard: C++26 (enforced by CMake)
- Dependencies: GTSAM 4.3, mylog, OpenCV 4.x and MASt3R (all git submodules)
- Optional: LuaJIT 2.1 (configuration); a system OpenCV 4 install as an alternative to the bundled submodule
- Version: single source is
project(VERSION)inCMakeLists.txt; read at runtime viamyslam::kVersion
- Architecture
- Data flow
- Module responsibilities
- Execution flow
- Repository layout
- Requirements
- Building
- Running
- Configuration
- Dataset replay
- Scripts
- Tests
- Engineering conventions
- Current limitations
The system has five parts, listed in call order:
- Initializer
VioInitializer— state machineUNINITIALIZED → STATIONARY_COLLECTING → GRAVITY_ALIGNED → TRACKING_OK, withLOST_DEGRADEDon anomaly (which rebuilds the pipeline). Stationarity is decided from both accelerometer and gyro standard deviation with thresholds scaled by the configured noise density and sample rate; gyro bias calibration runs after gravity alignment, producing the initial pose, velocity, and bias. Ifsystem.initTimeoutelapses without meeting the stationarity test, alignment relaxes to the non-stationary path, where the mean angular rate contains real motion and is therefore not used as a gyro bias prior (it is zeroed and left to the backend's online estimation). - Keyframe decision
KeyframeSelector— decides whether the current camera frame becomes a keyframe, using average parallax, feature retention, and a time-based fallback. The fallback additionally requires a minimum triangulation parallax. - Backend
VioBackend— combinesCombinedImuFactor(IMU preintegration) withProjectionFactor(visual reprojection) and solves throughIncrementalFixedLagSmootherover theslidingWindowLagwindow, marginalizing states that leave it. Visual residuals use a Huber kernel. - Propagator
HighRatePropagator— propagates state between optimizations at the IMU rate and resets its base to the optimized state after each optimization. - Health monitor
HealthMonitor— checks bias magnitude, speed magnitude, and active observation count, returning alert text when a threshold is exceeded.
The simulation and recorded-dataset paths reuse all of the above; only the sensor source changes.
The two columns are the two sample rates. Each column is split into an initialization stage and a
tracking stage; within a column the stages are selected by the current SystemState and are
mutually exclusive. Blue boxes are the same VioBackend instance invoked on both rate paths.
The main loop is driven by VioSystem::run(): time advances by the IMU step, each IMU sample
updates preintegration or propagation, camera frames trigger initialization or keyframe decisions,
and a keyframe triggers backend optimization followed by a propagator reset.
| Module | Responsibility |
|---|---|
vio/configHolder.hpp |
Sensor structs (ImuMeasurement / CameraFrame / FeatureObservation) and VioConfig |
vio/initializer.hpp |
SystemState state machine, stationarity detection, gravity alignment, bias calibration |
vio/keyframe_selector.hpp |
Keyframe decision returning DecisionResult (flag, reason, parallax, retention) |
vio/backend.hpp |
Factor graph construction, triangulation, layered outlier gating, marginalization, latestNavState() |
vio/propagator.hpp |
IMU-rate state propagation and base reset |
vio/health_monitor.hpp |
Bias, speed, and observation-count checks returning HealthReport |
vio/simulator.hpp |
3D trajectory and IMU / camera observation generation with analytic ground truth |
vio/system.hpp |
Dual-rate main loop orchestration, SystemConfig, accuracy evaluation |
vio/vio.hpp |
Umbrella header including all of the above |
lua_config.hpp |
Lua configuration loading producing ProjectConfig |
log_config.hpp |
Thin wrapper over the mylog C++ API (level + file output) |
dataset/configHolder.hpp |
Replay configuration and statistics structs; standard library only, no OpenCV |
dataset/intrinsics_file.hpp |
Parser for intrinsics estimated by external tools such as MASt3R (standard library only) |
dataset/subt_dataset.hpp |
SubT sequence reader: IMU CSV, camera timestamps, image index |
dataset/mei_tracker.hpp |
MEI model distortion correction and GFTT + LK optical-flow tracking |
dataset/dataset_runner.hpp |
Recorded-sequence replay orchestration reusing the VIO components |
dataset/visualizer.hpp |
OpenCV window and video output |
dataset/ply_export.hpp |
Export landmarks and trajectory as PLY |
vio/* and dataset/configHolder.hpp include no OpenCV headers, so the project still builds and parses
its configuration when the dataset tools are disabled.
VioInitializer::feedImu() accumulates stationary samples and tracks accelerometer and gyro
standard deviation. Once the samples qualify, tryGravityAlignment() derives roll/pitch from the
mean specific force, estimates bias from the mean gyro reading, and returns a Rot3 plus
ConstantBias. On success VioSystem calls VioBackend::initializeSystem() to install the first
prior and sets the state to TRACKING_OK.
When a keyframe is accepted, VioBackend::processKeyframe() performs:
- Triangulates new landmarks from feature correspondences between the previous and current
keyframe, skipping points below
minTriangulationParallaxPx, and rejects triangulations whose depth falls outside[minLandmarkDepth, maxLandmarkDepth]; - Computes reprojection residuals against the preintegrated predicted pose, temporarily rejecting
observations above
maxReprojectionErrorPreopt. The depth interval is enforced here for every landmark, unconditionally — it is a geometric-validity test, not an outlier test, so it is not exempted by thelandmarkGateWarmupObsgrace period. A single landmark behind the camera has a projection Jacobian that diverges as 1/z and is enough to make the Hessian singular; - If fewer than
minVisualFactorsForHealthvisual factors survive and the recent IMU window indicates the device is stationary, injects a zero-velocity prior (ZUPT,zuptWindowSeconds,zuptVelocitySigma). Visual loss leaves only the IMU chain, which constrains how velocity changes but not velocity itself, so integration error accumulates and position grows quadratically; - Inserts
ProjectionFactors and runs the smoother update, withextraSmootherIterationsadditional iterations. If the solve throwsIndeterminateSystemException, the backend rebuilds the smoother around the last successful keyframe instead of propagating the failure: the coordinate frame is preserved and only that keyframe is skipped; - Recomputes residuals against the optimized pose: observations above
maxReprojectionErrorare marked as outliers after a multi-frame streak, and those abovemaxReprojectionErrorHardare rejected on a single frame; - Runs
HealthMonitorand records any alert. AVISUALdegradation (observation starvation, e.g. the image going dark) does not rebuild the pipeline — the next frame would be just as dark, and rebuilding only resets the coordinate frame repeatedly. Inertial degradations do rebuild.
Keyframe data also maintains sliding-window size statistics and the fixed-lag duration
slidingWindowLag.
VioSystem::evaluate() computes translation error per keyframe against simulated ground truth and
reports RMSE and maximum error. RMSE below rmseThreshold is reported as passing.
myslam/
├── CMakeLists.txt # top-level build, single version source, format targets
├── config/config.lua # default runtime configuration (copied into build/config/)
├── src/
│ ├── CMakeLists.txt # INTERFACE targets myslam / myslam_lua / myslam_sanitized
│ ├── myslam.hpp.in # version template expanded by configure_file
│ ├── lua_config.hpp # Lua configuration loading
│ ├── log_config.hpp # logging configuration
│ ├── vio/ # VIO core, no OpenCV dependency
│ └── dataset/ # dataset replay, OpenCV confined to this layer
├── examples/
│ ├── 1_visual_inertial_odometry.cpp # synthetic run example
│ └── 2_subt_dataset.cpp # recorded dataset replay
├── tests/
│ ├── test_vio.cpp # VIO smoke test with accuracy assertions
│ └── test_lua_config.cpp # parsing, clamping, cross-validation, failure paths
├── docs/
│ ├── architecture.svg # architecture diagram (English)
│ └── architecture_zh.svg # architecture diagram (Chinese)
├── scripts/
│ ├── run_subt.sh # dataset replay script
│ ├── plot_tum.py # TUM trajectory plotting
│ ├── gen_architecture_svg.py # generates the diagrams under docs/
│ └── setup-hooks.sh # install the pre-commit formatting hook
└── thirdparty/ # gtsam / mylog / opencv / mast3r (git submodules)
| Item | Requirement |
|---|---|
| CMake | ≥ 3.20 |
| Compiler | Accepts -std=c++26 (GCC 14+) |
| Build tool | Ninja or Make |
| System libraries | TBB, Boost, Eigen3, Threads |
| LuaJIT | 2.1+, discoverable via pkg-config; when missing, Lua support is turned off automatically |
| MASt3R | Only for --init mast3r: a PyTorch environment plus the checkpoint; otherwise unused |
| OpenCV | 4.x, only when MYSLAM_BUILD_DATASET_TOOLS=ON; taken from the system by default, or built from thirdparty/opencv with MYSLAM_USE_BUNDLED_OPENCV=ON |
| clang-format | 23.1.3 (commit hook and CI both check against this version) |
Install on Ubuntu:
sudo apt-get install -y build-essential cmake ninja-build pkgconf \
libtbb-dev libboost-all-dev libeigen3-dev libluajit-5.1-dev
# dataset tools only (system OpenCV backend; skip it for MYSLAM_USE_BUNDLED_OPENCV)
sudo apt-get install -y libopencv-dev
# formatter
pipx install "clang-format==23.1.3"git clone --recursive <repo-url> myslam
cd myslam
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failureThe first build compiles GTSAM. Core-only builds can skip the ~225 MB of
thirdparty/opencv + thirdparty/mast3r with
git submodule update --init thirdparty/gtsam thirdparty/mylog.
| Option | Default | Description |
|---|---|---|
MYSLAM_BUILD_EXAMPLES |
ON when top-level |
Build example programs |
MYSLAM_BUILD_TESTS |
ON when top-level |
Build tests |
MYSLAM_BUILD_DATASET_TOOLS |
OFF |
Build the OpenCV dataset replay tools |
MYSLAM_USE_BUNDLED_OPENCV |
OFF |
Build OpenCV from thirdparty/opencv instead of using system packages |
MYSLAM_SANITIZE |
ON when top-level |
Enable ASan/UBSan in Debug |
MYSLAM_ENABLE_LUA |
ON |
Lua configuration support; falls back to OFF with a warning when LuaJIT is missing |
When included via add_subdirectory, these options default to OFF.
Under Debug + ASan, keyframe optimization is slow, so dataset replay uses a separate Release tree:
cmake -B build-release -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DMYSLAM_SANITIZE=OFF \
-DMYSLAM_BUILD_TESTS=OFF \
-DMYSLAM_BUILD_DATASET_TOOLS=ON
cmake --build build-release --target 2_subt_dataset -j"$(nproc)"scripts/run_subt.sh defaults to build-release.
thirdparty/opencv is a tracked submodule pinned to the 4.x branch, so the dataset tools can build
against a version that evolves with this repository instead of the host distribution:
cmake -B build-release -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DMYSLAM_SANITIZE=OFF \
-DMYSLAM_BUILD_TESTS=OFF \
-DMYSLAM_BUILD_DATASET_TOOLS=ON \
-DMYSLAM_USE_BUNDLED_OPENCV=ON
cmake --build build-release --target 2_subt_dataset -j"$(nproc)"Only the modules the replay tools need are configured (core, imgproc, imgcodecs,
features2d, video, calib3d, highgui, videoio), with tests, samples, Python/Java bindings
and the optional codecs (JPEG2000, TIFF, OpenEXR, GDAL, gPhoto2) disabled. The first configure
builds those modules from source, so expect a longer compile than the system-package backend.
Both backends expose the same myslam::opencv interface target; consumers do not care which one is
active. Follow upstream with git submodule update --remote thirdparty/opencv.
thirdparty/mast3r pins naver/mast3r, the reconstruction stack
behind the SubT sequences. It is a Python/PyTorch project, so it takes no part in the C++ build — but
it is an optional runtime dependency: 2_subt_dataset --init mast3r calls
scripts/mast3r_intrinsics.py to estimate intrinsics from the images (see
Where the intrinsics come from). Without that flag it is never
touched.
Update it with git submodule update --remote thirdparty/mast3r. Estimation also needs the nested
dust3r checkout: git submodule update --init --recursive thirdparty/mast3r.
The version string appears once, in project(VERSION ...); configure_file writes it to
<build>/src/generated/myslam.hpp. Rebuild after changing it.
cmake --build build --target format # reformat in place
cmake --build build --target format-check # check onlyscripts/setup-hooks.sh installs a pre-commit hook that rejects unformatted code.
cd build
./bin/1_visual_inertial_odometryRuns five seconds of simulation with default configuration (IMU 200 Hz / camera 20 Hz) and prints:
评估关键帧样本数 : 24
整体平移 RMSE 误差: 0.0365 米
最大轨迹偏差 : 0.0778 米
Exit code 0 means RMSE is below system.rmseThreshold (default 0.20 m).
Logs are written to a .log file in the current working directory (managed by mylog's
FileManager); the terminal only prints [info] 已加载配置: .... Open the log file for results.
./bin/1_visual_inertial_odometry # reads config/config.lua, else built-in defaults
./bin/1_visual_inertial_odometry my_config.lua # explicit; exit code 1 on load or validation failureRuntime parameters come from a Lua script, by default config/config.lua, copied into
build/config/ at configure time.
| Section | Consumer | Contents |
|---|---|---|
vio |
VioConfig |
camera intrinsics, extrinsics, IMU noise, Huber kernel threshold, keyframe policy, sliding window |
system |
SystemConfig |
camera step, run duration, RMSE threshold |
dataset |
MeiIntrinsics / DatasetRunConfig |
dataset intrinsics, replay range, visualization switches |
logger / fileManager |
mylog | log level, buffers, rotation and retention |
Every field may be omitted; omission keeps the built-in default.
- The script runs in a bare
lua_Statewith no standard libraries registered, soio/os/package/debugare unavailable. An instruction budget (kInstructionBudget, 50 million) is set as a hook. - Values are parsed into a temporary copy and written back to
ProjectConfigonly after all validation passes; any failure leaves the caller's configuration unchanged. - Out-of-range values are handled two ways: tuning fields are clamped into a safe interval with a warning; hard prerequisites such as focal length, pinhole scale, and image dimensions are rejected instead of corrected.
- Unregistered keys inside a section are reported individually.
- Cross-field constraints are checked: keyframe interval bounds, relative ordering of reprojection thresholds, and the camera/IMU rate relationship.
The IMU sampling step is not a configuration field; it derives from vio.imuRateHz:
const double imuDt = vio::SystemConfig::imuDtFrom(config_.imuRateHz); // 1 / imuRateHzA leftover system.imuDt in an older config produces a migration message.
#include "lua_config.hpp"
myslam::config::ProjectConfig project;
const auto result = myslam::config::load("config/config.lua", project, /*applyLogging=*/true);
if (!result) { /* keep built-in defaults; result.error().message explains why */ }
else if (!result->logging) { /* fall back to the mylog C++ API */ }Without linking myslam::myslam_lua, the header is a stub whose load() returns failure.
2_subt_dataset uses the same VIO components as the simulator; only the sensor source changes.
Expected layout:
Handheld1_Folder/
├── cam_0/
│ ├── 0.png, 1.png, ...
│ └── timestamps.txt # one nanosecond timestamp per line
└── imu/
└── imu_data.csv # timestamp, q_x, q_y, q_z, q_w,
# ang_vel_{x,y,z}, lin_acc_{x,y,z}
MEI camera model: the backend reprojects through gtsam::Cal3_S2, while the sequence uses the
MEI model. MeiFeatureTracker first inverts the radtan distortion, then back-projects to a ray
under the MEI model, and finally projects through virtual pinhole intrinsics scaled by
pinholeScale. The virtual intrinsics are mirrored into VioConfig.
IMU timestamp gaps: consecutive samples separated by more than
maxImuGapFactor / imuRateHz are treated as a gap (default 10 sample periods, i.e. 50 ms at
200 Hz; adjustable via dataset.replay.maxImuGapFactor). Two rules apply:
- Zero-order hold never spans a gap: there is no IMU data inside it, so integration stops one sample period after the last valid sample instead of extrapolating across the missing time;
- The pipeline is rebuilt and re-initialized. Segment count is reported in
DatasetRunReport::segments, resets inresets.
Each segment re-initializes from a local origin, so their coordinate frames differ and
trajectories and point clouds must not be concatenated across segments. The TUM output marks
segment boundaries with # comment lines (plot_tum.py skips them), and the report warns when
there is more than one segment.
The preintegration boundary is aligned to the camera timestamp: a camera time usually falls
between two IMU samples, so integration is truncated proportionally at cameraTime, and the
straddling sample is only partially consumed with its remainder left for the next frame. The
interval covered by CombinedImuFactor therefore ends exactly at the keyframe timestamp.
build-release/bin/2_subt_dataset \
--data /path/Handheld1_Folder/cam_0 \
--imu /path/Handheld1_Folder/imu/imu_data.csv \
--stamps /path/Handheld1_Folder/cam_0/timestamps.txt \
[--output traj.tum] [--start-frame N] [--max-frames N] [--stride N] \
[--max-features N] [--max-duration S] [--ply cloud.ply] \
[--viz] [--video out.mp4] [--viz-stride N] [--viz-rate R] \
[--config config/config.lua] \
[--init manual|mast3r] [--intrinsics-file FILE]--config supplies baseline defaults; command-line arguments take precedence. When omitted, it
reads config/config.lua in the current directory, else uses built-in defaults.
--fx/--fy/--cx/--cy/--xi/--k1/--k2/--p1/--p2/--width/--height/--pinhole-scale override MEI
intrinsics individually.
Sequences that ship no calibration file can skip the camera parameters entirely and let MASt3R estimate them from the images:
scripts/run_subt.sh --init mast3r # or 2_subt_dataset ... --init mast3rHow it works:
- Six image pairs by default (
--mast3r-pairs) are fed to the MASt3R model inthirdparty/mast3r; a focal length is recovered from the predicted point maps withestimate_focal_knowing_depth, the principal point is taken as the image centre; - the result is written to
<cam_0>/../mast3r_intrinsics.txtand cached, so later replays skip the inference;--force-estimaterecomputes it and--mast3r-cacherelocates it; - the parser uses it for the virtual pinhole focal only:
pinholeScale = focal / fxso the backend'spinholeFx()equals the estimate.xiandk1/k2/p1/p2are inherited from the configured calibration, so the fisheye unprojection still runs.
The estimate is an effective pinhole focal length fitted under a pinhole assumption, while the MEI
fx is a projection parameter: Handheld1 has fx = 758.3 with xi = 1.583, whose paraxial
pinhole focal is only fx / (1 + xi) ≈ 294. An earlier revision stored focal = 408 into fx
and zeroed xi, which is equivalent to treating the raw fisheye frame as a pinhole image:
mx/mz = x degenerates the pixel mapping to identity and bypasses distortion inversion, moving
the valid-field mask from 54.7% to 99.8%. The circular mechanical rim of a fisheye lens is a
high-contrast corner magnet, so GFTT piled features onto the rim, optical flow spent its tracking
budget on edges that cannot be triangulated, and the trajectory scale came out more than 2× too
large. Keeping xi lets meiToPinhole's mz <= 1e-6 and bounds checks reject the rim again.
Inference needs a PyTorch environment: point --python, MYSLAM_PYTHON or the script's
MAST3R_PYTHON at it (the conda mast3r env works), and pass the checkpoint with
--mast3r-weights or MAST3R_WEIGHTS; by default it is looked up under
thirdparty/mast3r/checkpoints/.
Two limits worth knowing:
- MASt3R predicts a pinhole model, so on a >180° MEI fisheye it only yields an approximate
focal (Handheld1 lands anywhere in 400–500 px). It cannot recover
xior the distortion terms, so a new sequence's lens characteristics still belong indataset.intrinsics, and accuracy evaluation still needs the yaml calibration; - estimation covers the focal length only. The camera-IMU extrinsic
Ticlikewise still comes from the built-in value or a calibration file.
Machines that already have an estimate can skip inference with --intrinsics-file result.txt
(format documented in src/dataset/intrinsics_file.hpp):
model = pinhole
source = mast3r
focal = 495.90
cx = 320.0
cy = 240.0
width = 640
height = 480
focal is the backend's virtual pinhole focal length, not the MEI fx. Any xi/k1/k2/p1/p2
missing from the file is inherited from the configuration; writing fx/fy is rejected outright,
which also invalidates caches produced by the earlier revision so they get recomputed.
--viz: OpenCV window (needsDISPLAY, ESC/q to quit)--video FILE: encode to video (falls back to.avi/MJPG when mp4 is unavailable)--viz-rate R:0full speed,1real time,2double speed
Layout: the top-left panel is the raw fisheye image with features in raw pixel coordinates and a cyan outline of the usable MEI field of view; the top-right panel is the same frame warped into the virtual pinhole image the tracker works in, with features in pinhole coordinates. The bottom status bar holds a trajectory minimap on the left and status text on the right. Green = tracked from the previous frame, orange = newly detected.
The undistorted view shares the virtual pinhole intrinsics used for reprojection (pinholeScale
times the focal length); its black border is the part of the pinhole canvas the fisheye frame does
not cover. It is a reference view: tracked features already live in that canvas, so drawing them on
the raw fisheye image would shift them systematically — each panel uses its own pixel coordinates.
Set dataset.viz.showUndistorted = false to drop the right panel (and the per-frame remap).
scripts/plot_tum.py traj.tum --save traj.png # 3D / XY top-down / position-velocity panels
scripts/plot_tum.py traj.tum --ply cloud.ply --showPLY clouds open in MeshLab / CloudCompare / Open3D.
scripts/run_subt.sh # default sequence, starting at frame 2300
scripts/run_subt.sh --max-frames 0 # replay everything remaining
scripts/run_subt.sh --root /path/Handheld2_Folder
scripts/run_subt.sh --viz # live window
scripts/run_subt.sh --video out.avi --ply cloud.ply
scripts/run_subt.sh --plot # plot the trajectory afterwards
scripts/run_subt.sh --no-build # skip the build
scripts/run_subt.sh --init mast3r # estimate intrinsics with MASt3RThe default sequence path is ../MASt3R-Fusion/dataset/Handheld1_Folder, overridable with
--root or SUBT_ROOT. It starts at frame 2300 (that sequence has exposure jumps and an IMU gap
early on). When --max-frames is not given, the script uses 500000.
--init mast3r forwards to the replay tool and sets MYSLAM_PYTHON from MAST3R_PYTHON, so
the MASt3R inference runs in the interpreter you name.
Requires numpy and matplotlib. Produces three panels: 3D trajectory, XY top-down, and
position/velocity over time.
Installs .githooks/pre-commit (git config core.hooksPath .githooks), which checks staged files
with clang-format --dry-run -Werror.
ctest --test-dir build --output-on-failure| Test | Coverage |
|---|---|
myslam_test |
Public API usable, keyframes produced, RMSE within threshold, derived step valid |
myslam_config_test |
Parsing, clamping, cross-validation, unknown keys, quaternion normalization, failure paths, logging section effect, default-config self-consistency |
myslam_intrinsics_test |
Intrinsics file: focal lands on the virtual pinhole, distortion inherited from the baseline, legacy fx fields rejected, principal-point fallback and malformed input rejected |
CI (.github/workflows/ci.yml) runs format checking, a build, and the suite on Ubuntu with
g++-14, pinning clang-format 23.1.3.
Conventions come from the mylog submodule; the full statement is in the project's
instructions.md.
- C++26, 4-space indent, Allman braces, 120 columns
- Types
CamelCase; functions and localscamelBack; members suffixed_ - Constants
kprefix +CamelCase; enums and macrosUPPER_CASE; namespaceslower_case - Build switches prefixed
MYSLAM_*; test targetmyslam_test - Doxygen comments; clang-format required before committing
- No loop closure or global relocalization; error accumulates over time.
- No map persistence; landmarks are reclaimed as the marginalization window advances.
- Monocular camera plus IMU only; no multi-camera, depth, or GNSS fusion.
- No
install()orexport()rules.myslamis an INTERFACE target,INSTALL_INTERFACEis a placeholder, and the generated header is not installed. - Configuration loads once at startup; no runtime hot reload.
- Logs are written to the current working directory rather than a project-managed location.
- If a prefix that shadows the system
libfontconfigis active (e.g. conda), the dataset tools fail at runtime withundefined symbol: FcConfigSetDefaultSubstitute; work around it withLD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu.