A Rust library for reading, writing, and manipulating TRX brain tractography files, plus format-conversion helpers for common streamline formats.
TRX is a binary file format for storing streamline tractography data using memory-mapped arrays. It supports multiple coordinate precisions (f16, f32, f64), per-vertex data (DPV), per-streamline data (DPS), named groups, and data-per-group (DPG) arrays — all backed by zero-copy memory mapping for efficient access to large datasets.
- Zero-copy memory mapping via
memmap2for efficient access to multi-gigabyte tractography files - Multiple precision support — f16, f32, and f64 positions with runtime dtype detection
- Full read/write support for both directory-based and ZIP archive (
.trx) formats - Format conversion helpers for
.tck,.tck.gz,.vtk, and.tt.gz -> .trx - Streaming construction — build TRX files incrementally with
TrxStream - Ancillary data access — DPS (per-streamline), DPV (per-vertex), group membership arrays, and DPG metadata
- Set operations — intersection, union, and difference on streamline sets
- Connectivity matrices — compute group-to-group connectivity (count or weighted)
- Subset and merge — extract streamlines by index or concatenate multiple files
Add to your Cargo.toml:
[dependencies]
trx-rs = { git = "https://github.com/tee-ar-ex/trx-rs" }use trx_rs::TrxFile;
let trx = TrxFile::<f32>::load("tractogram.trx")?;
println!("{} streamlines, {} vertices", trx.nb_streamlines(), trx.nb_vertices());
for (i, streamline) in trx.streamlines().enumerate() {
println!("Streamline {i}: {} points", streamline.len());
}When the position dtype isn't known at compile time:
use trx_rs::AnyTrxFile;
let any = AnyTrxFile::load("tractogram.trx")?;
any.with_typed(
|trx_f16| { /* work with TrxFile<f16> */ },
|trx_f32| { /* work with TrxFile<f32> */ },
|trx_f64| { /* work with TrxFile<f64> */ },
);// Per-streamline scalar (e.g., SIFT2 weights)
let weights = trx.dps::<f32>("weights")?;
for row in weights.rows() {
println!("weight: {}", row[0]);
}
// Per-vertex scalar (e.g., FA along streamline)
let fa = trx.dpv::<f32>("fa")?;
// Group membership (streamline indices)
let members: &[u32] = trx.group("corticospinal_tract")?;
// Data-per-group metadata (for example, RGB color stored on a group)
let color = trx.dpg::<u8>("corticospinal_tract", "color")?;
assert_eq!(color.shape(), (1, 3));use trx_rs::TrxStream;
let affine = [[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0]];
let dimensions = [128, 128, 80];
let mut stream = TrxStream::<f32>::new(affine, dimensions);
stream.push_streamline(&points);
let trx = stream.finalize();
trx.save("output.trx")?; // ZIP archive
trx.save_to_directory("output/")?; // directory formatuse trx_rs::ops::merge::merge_trx_shards;
use trx_rs::ops::subset::subset_streamlines;
use trx_rs::ops::streamline_ops::{difference, intersection};
// Extract a subset of streamlines by index
let sub = subset_streamlines(&trx, &[0, 5, 10, 42])?;
// Merge multiple files (must share affine and dimensions)
let merged = merge_trx_shards(&[&shard1, &shard2])?;
// Set operations on streamlines
let common = intersection(&trx_a, &trx_b)?;
let only_a = difference(&trx_a, &trx_b)?;trx-rs currently exposes conversion at the library level:
use trx_rs::{convert, read_tractogram, write_tractogram, ConversionOptions, DType};
// Convert between file formats.
convert("input.tck.gz".as_ref(), "output.trx".as_ref(), &ConversionOptions::default())?;
// Read a non-TRX tractogram into the neutral in-memory representation.
let tractogram = read_tractogram("bundles.tt.gz".as_ref(), &ConversionOptions::default())?;
// Write TRX with explicit positions dtype.
let options = ConversionOptions {
trx_positions_dtype: DType::Float16,
..Default::default()
};
write_tractogram("bundles_f16.trx".as_ref(), &tractogram, &options)?;Supported conversion formats:
| Format | Read | Write | Notes |
|---|---|---|---|
TRX |
Yes | Yes | Directory and .trx zip archives |
TCK / TCK.GZ |
Yes | Yes | Gzipped TCK is supported |
VTK |
Yes | Yes | Legacy PolyData POINTS + LINES subset |
TT.GZ |
Yes | No | Import only; TT clusters become TRX groups and TT colors become dpg/<group>/color.3.uint8 |
TT import behavior:
- TT streamline points are decoded in voxel space and mapped to TRX positions with
trans_to_mnidirectly - TT
clustervalues become TRX groups file.tt.gz.txtsidecar labels are used for group names when present- TT colors become
dpg/<group>/color.3.uint8 - TT
reportandparameter_idare stored inheader.extra
Build the library and tests:
cargo build
cargo test --workspaceBuild an optimized release:
cargo build --releaseAs a library dependency:
[dependencies]
trx-rs = { git = "https://github.com/tee-ar-ex/trx-rs" }For local development:
git clone https://github.com/tee-ar-ex/trx-rs
cd trx-rs
cargo build --releaseThe repository now ships a trxrs binary under src/bin/trxrs.rs.
Build it locally:
cargo build --release
./target/release/trxrs --helpInstall it into Cargo's bin directory:
cargo install --path .
trxrs --helpAvailable subcommands:
trxrs info <input>trxrs convert <input> <output> [--positions-dtype f16|f32|f64] [--reference <nifti|trx>]trxrs concatenate <input-a.trx> <input-b.trx> ... --output <output.trx> [--positions-dtype f16|f32|f64]trxrs manipulate-dtype <input.trx> <output.trx> [--positions-dtype f16|f32|f64]trxrs transform <input.trx> <output.trx> --transform <h5-or-txt>— apply an ANTs/ITK spatial transform to streamline coordinates (see Applying ANTs transforms)
Examples:
# Inspect a TRX or supported foreign-format tractogram
trxrs info bundles.trx
# Convert gzipped TCK to TRX with float16 positions
trxrs convert bundles.tck.gz bundles.trx --positions-dtype f16
# Export to TrackVis (.trk); a reference supplies the affine when the input has none
trxrs convert bundles.tck bundles.trk --reference dwi.nii.gz
# Rewrite an existing TRX with float32 positions
trxrs manipulate-dtype input.trx output.trx --positions-dtype f32
# Concatenate TRX files and write float16 output
trxrs concatenate shard1.trx shard2.trx --output merged.trx --positions-dtype f16Notes:
trx convertuses the library conversion layer and supportstrx,tck,tck.gz,vtk, andtt.gzinput- output may be
trx,tck,vtk, ortrk; TrackVis (.trk) export needs a spatial affine, taken from the input (TRX/TRK) or from--reference(TCK/VTK), and does not carry per-vertex/per-streamline scalars .tt.gzis import-only; writing Tiny Track is not implementedtrx concatenatecurrently supports TRX inputs and TRX output only- offsets are written as
uint32when every offset fits inu32(i.e., for any tractogram with ≤ 4 G vertices), otherwiseuint64— the TRX spec accepts both
trxrs transform is the Rust counterpart to antsApplyTransformsToTRX. It
warps streamline vertex coordinates through an ITK Composite .h5 (with
embedded warp + affines), an Insight Transform File V1.0 (.txt,
affine-only), or an ITK MATLAB v4 binary (.mat, affine-only — what
ANTs writes for *0GenericAffine.mat).
Tractograms warp in the opposite spatial direction from images.
Concretely, with paired BIDS h5 files for subject sub-01:
| You have | You want | Pass to --transform |
|---|---|---|
sub-01_space-ACPC_tracts.trx |
tracts in MNI152NLin2009cAsym |
sub-01_from-MNI152NLin2009cAsym_to-ACPC_xfm.h5 |
sub-01_space-MNI152NLin2009cAsym_tracts.trx |
tracts in ACPC |
sub-01_from-ACPC_to-MNI152NLin2009cAsym_xfm.h5 |
sub-01_space-T1w_tracts.trx |
tracts in MNI152NLin6Asym |
sub-01_from-MNI152NLin6Asym_to-T1w_xfm.h5 |
If you are coming from antsApplyTransforms for images: pass the same
h5 you would use to warp an image of the destination space into the
source space. (That's the same convention as antsApplyTransformsToPoints.)
Image warping with antsApplyTransforms is pull-based: the chain
inside from-X_to-Y_xfm.h5 (the file that warps an X-image onto a Y-grid,
per BIDS) internally maps target Y voxels back to source X coordinates.
Applied to a point, that same chain sends a Y-point to an X-point. So to
warp a streamline FROM space A TO space B, you need a chain that maps
A → B — which lives in the opposite-named file from-B_to-A_xfm.h5.
trxrs transform \
sub-01_space-ACPC_tracts.trx \
sub-01_space-MNI152NLin2009cAsym_tracts.trx \
--transform sub-01_from-MNI152NLin2009cAsym_to-ACPC_xfm.h5 \
--reference sub-01_space-MNI152NLin2009cAsym_T1w.nii.gz # optional, for headerThe --reference flag is optional — if given, the output TRX header's
voxel_to_rasmm and dimensions are taken from it. The streamline
coordinates themselves are warped regardless.
For an affine-only .txt or .mat, --invert numerically flips the chain
so you can use a single file in either direction. Warps cannot be
numerically inverted; for those, use the paired from-Y_to-X_xfm.h5 instead.
| Type | Positions | DPS/DPV |
|---|---|---|
f16 (half) |
Yes | Yes |
f32 |
Yes | Yes |
f64 |
Yes | Yes |
u8, u16, u32, u64 |
- | Yes |
i8, i16, i32, i64 |
- | Yes |
# Full test suite
cargo test --workspace
# Formatting check
cargo fmt --check
# Lint with warnings denied
cargo clippy --workspace --all-targets --all-features -- -D warnings
# Coverage report (requires cargo-llvm-cov)
cargo llvm-cov --workspace --htmlReal-data integration tests download the fixture archives on first run and cache
them in target/test_data/. Set TRX_TEST_DATA_DIR to reuse an existing copy
of the fixture data instead of downloading it again.
Benchmarks use a real 10M-streamline HCP dataset (~7.7 GB, auto-downloaded from figshare):
# Quick smoke test with 100k streamlines
TRX_BENCH_MAX_STREAMLINES=100000 cargo bench
# Full benchmark
cargo benchSet TRX_BENCH_DATA_DIR to point to a cached copy of the benchmark data.
BSD 2-Clause