Classic vs TRX: A Complete Tractography Walkthrough

HCP Data · Glasser 360 + Schaefer 4S456 · NODDI scalars · 500k streamlines

NoteRendering notes

This document executes during render (eval: true). All MRtrix binaries come from the trx-mrtrix build at build/bin/, not the system installation. The demo uses 1M streamlines for a long render; change N_TRACKS in the R setup chunk to make it faster/slower.

Timing and resource measurement

Every major command is wrapped with mtime — a thin shell function around /usr/bin/time -l (macOS) that emits a one-line summary after each step:

┌─ tckgen (TRX)              ─────────────────────────────────────────────┐
│  wall:    52.3s    user:   812.4s    sys:    6.1s    peak mem: 2.31 GB  │
└─────────────────────────────────────────────────────────────────────────┘
Metric What it measures
wall Elapsed clock time — the number you actually wait
user Total CPU time across all threads; user >> wall for multithreaded commands
sys Kernel time (I/O, memory mapping)
peak mem Physical memory footprint (phys_footprint via TASK_VM_INFO) — excludes clean read-only mmap pages, so TRX files are not double-counted

maximum resident set size (RSS) counts clean file-backed mmap pages — TRX files are opened read-only via mmap, so RSS is inflated for TRX inputs even though those pages impose no memory pressure. peak memory footprint counts only dirty/anonymous pages and matches what Activity Monitor shows.


Setup

DERIV=/Users/mcieslak/data/qsirecon/derivatives/qsirecon-MRtrix3_act-HSVS/sub-100307/dwi
PARC=/Users/mcieslak/data/qsirecon/sub-100307/dwi

source ../timing_utils.sh

ln -sf "$DERIV/sub-100307_space-T1w_model-msmtcsd_param-fod_label-WM_dwimap.mif.gz" wm_fod.mif.gz
ln -sf "$DERIV/sub-100307_space-T1w_model-mtnorm_param-inliermask_dwimap.nii.gz"     mask.nii.gz
ln -sf "$PARC/sub-100307_space-T1w_model-noddi_param-icvf_dwimap.nii.gz"             icvf.nii.gz
ln -sf "$PARC/sub-100307_space-T1w_model-noddi_param-isovf_dwimap.nii.gz"            isovf.nii.gz
ln -sf "$PARC/sub-100307_space-T1w_seg-Glasser_dseg.mif.gz"                          glasser.mif.gz
ln -sf "$PARC/sub-100307_space-T1w_seg-Glasser_dseg.txt"                             glasser.txt
ln -sf "$PARC/sub-100307_space-T1w_seg-4S456Parcels_dseg.mif.gz"                     4S456.mif.gz
ln -sf "$PARC/sub-100307_space-T1w_seg-4S456Parcels_dseg.txt"                        4S456.txt

echo "N_TRACKS = $N_TRACKS"
echo "tckgen:   $(which tckgen)"
echo "trxlabel: $(which trxlabel)"
ls -lh *.mif.gz *.nii.gz *.txt
N_TRACKS = 1000000
tckgen:   /Users/mcieslak/projects/trx-mrtrix/build/bin/tckgen
trxlabel: /Users/mcieslak/projects/trx-mrtrix/build/bin/trxlabel
-rw-r--r--@ 1 mcieslak  staff   7.2K Mar 19 17:25 4S456_nodes.txt
lrwxr-xr-x@ 1 mcieslak  staff    94B Mar 19 19:57 4S456.mif.gz -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_seg-4S456Parcels_dseg.mif.gz
lrwxr-xr-x@ 1 mcieslak  staff    91B Mar 19 19:57 4S456.txt -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_seg-4S456Parcels_dseg.txt
-rw-r--r--@ 1 mcieslak  staff   289K Mar 18 23:03 assignments_glasser_classic.txt
-rw-r--r--@ 1 mcieslak  staff    16K Mar 19 17:25 combined_nodes.txt
-rw-r--r--@ 1 mcieslak  staff   3.4K Mar 19 17:25 glasser_nodes.txt
lrwxr-xr-x@ 1 mcieslak  staff    89B Mar 19 19:57 glasser.mif.gz -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_seg-Glasser_dseg.mif.gz
lrwxr-xr-x@ 1 mcieslak  staff    86B Mar 19 19:57 glasser.txt -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_seg-Glasser_dseg.txt
-rw-r--r--@ 1 mcieslak  staff    12M Mar 19 17:24 icvf_mean.txt
lrwxr-xr-x@ 1 mcieslak  staff   102B Mar 19 19:57 icvf.nii.gz -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_model-noddi_param-icvf_dwimap.nii.gz
-rw-r--r--@ 1 mcieslak  staff    13M Mar 19 17:24 isovf_mean.txt
lrwxr-xr-x@ 1 mcieslak  staff   103B Mar 19 19:57 isovf.nii.gz -> /Users/mcieslak/data/qsirecon/sub-100307/dwi/sub-100307_space-T1w_model-noddi_param-isovf_dwimap.nii.gz
lrwxr-xr-x@ 1 mcieslak  staff   147B Mar 19 19:57 mask.nii.gz -> /Users/mcieslak/data/qsirecon/derivatives/qsirecon-MRtrix3_act-HSVS/sub-100307/dwi/sub-100307_space-T1w_model-mtnorm_param-inliermask_dwimap.nii.gz
-rw-r--r--@ 1 mcieslak  staff   1.3M Mar 18 23:03 tdi_weighted_trx.mif.gz
-rw-r--r--@ 1 mcieslak  staff    16K Mar 19 16:47 tmp_combined_names.txt
-rw-r--r--@ 1 mcieslak  staff   3.4K Mar 19 16:30 tmp_diag_names.txt
lrwxr-xr-x@ 1 mcieslak  staff   150B Mar 19 19:57 wm_fod.mif.gz -> /Users/mcieslak/data/qsirecon/derivatives/qsirecon-MRtrix3_act-HSVS/sub-100307/dwi/sub-100307_space-T1w_model-msmtcsd_param-fod_label-WM_dwimap.mif.gz

Step 1 — Generate streamlines

Both pipelines start from a common tckgen run. MRtrix selects the writer from the output filename extension, so the only difference is .tck vs .trx.

CLASSIC

source ../timing_utils.sh
mtime "tckgen (classic)" \
tckgen wm_fod.mif.gz       \
    -algorithm iFOD2        \
    -seed_image mask.nii.gz \
    -mask       mask.nii.gz \
    -minlength  30          \
    -maxlength  250         \
    -select     $N_TRACKS   \
    -nthreads   8           \
    -force                  \
    tracks.tck
tckgen: [WARNING] existing output files will be overwritten
tckgen: uncompressing image "mask.nii.gz"... [==================================================]
tckgen: uncompressing image "mask.nii.gz"... [==================================================]
tckgen: uncompressing image "wm_fod.mif.gz"... [==================================================]
tckgen: [  0%]        1 seeds,        1 streamlines,        1 selected
tckgen: [  0%]        2 seeds,        2 streamlines,        1 selected
tckgen: [  0%]      401 seeds,      281 streamlines,      139 selected
tckgen: [  0%]     1161 seeds,      842 streamlines,      458 selected
tckgen: [  0%]     2741 seeds,     2041 streamlines,     1053 selected
tckgen: [  0%]     6071 seeds,     4515 streamlines,     2304 selected
tckgen: [  0%]    12251 seeds,     9180 streamlines,     4669 selected
tckgen: [  0%]    25351 seeds,    19020 streamlines,     9631 selected
tckgen: [  1%]    48361 seeds,    36396 streamlines,    18573 selected
tckgen: [  3%]    99121 seeds,    74606 streamlines,    38126 selected
tckgen: [  7%]   197601 seeds,   148550 streamlines,    75921 selected
tckgen: [ 15%]   390881 seeds,   294252 streamlines,   150501 selected
tckgen: [ 29%]   778871 seeds,   585537 streamlines,   299640 selected
tckgen: [ 59%]  1555871 seeds,  1169516 streamlines,   598398 selected
tckgen: [100%]  2600282 seeds,  1953917 streamlines,  1000000 selected

┌─ tckgen (classic)             ─────────────────────────────────────────────┐
│  wall:   685.28s    user:  5468.98s    sys:   7.53s    peak mem:  0.65 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo tracks.tck
du -sh  tracks.tck
***********************************
  Tracks file: "tracks.tck"
    command_history:      tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
    count:                1000000
    downsample_factor:    3
    fod_power:            0.25
    init_threshold:       0.100000001
    lmax:                 8
    max_angle:            45
    max_dist:             250
    max_num_seeds:        1000000000
    max_num_tracks:       1000000
    max_seed_attempts:    1000
    max_trials:           1000
    method:               iFOD2
    min_dist:             30
    mrtrix_version:       3.0.8-1694-g783d7f06-dirty
    rk4:                  0
    samples_per_step:     4
    sh_precomputed:       1
    source:               wm_fod.mif.gz
    step_size:            0.625
    stop_on_all_include:  0
    threshold:            0.100000001
    timestamp:            1773964637.623773098
    total_count:          1953917
    unidirectional:       0
    ROI:                  mask mask.nii.gz
    ROI:                  seed mask.nii.gz
1.5G    tracks.tck

TRX

source ../timing_utils.sh
mtime "tckgen (TRX float16)" \
tckgen wm_fod.mif.gz       \
    -algorithm iFOD2        \
    -seed_image mask.nii.gz \
    -mask       mask.nii.gz \
    -minlength  30          \
    -maxlength  250         \
    -select     $N_TRACKS   \
    -nthreads   8           \
    -trx_float16            \
    -force                  \
    tracks_f16.trx
tckgen: [WARNING] existing output files will be overwritten
tckgen: uncompressing image "mask.nii.gz"... [==================================================]
tckgen: uncompressing image "mask.nii.gz"... [==================================================]
tckgen: uncompressing image "wm_fod.mif.gz"... [==================================================]
tckgen: [  0%]        1 seeds,        0 streamlines,        0 selected
tckgen: [  0%]        6 seeds,        3 streamlines,        1 selected
tckgen: [  0%]        7 seeds,        4 streamlines,        1 selected
tckgen: [  0%]      701 seeds,      521 streamlines,      266 selected
tckgen: [  0%]     2371 seeds,     1769 streamlines,      896 selected
tckgen: [  0%]     5591 seeds,     4156 streamlines,     2135 selected
tckgen: [  0%]    11741 seeds,     8863 streamlines,     4541 selected
tckgen: [  0%]    24291 seeds,    18293 streamlines,     9362 selected
tckgen: [  1%]    48581 seeds,    36504 streamlines,    18794 selected
tckgen: [  3%]    97761 seeds,    73594 streamlines,    37640 selected
tckgen: [  7%]   195551 seeds,   147109 streamlines,    75105 selected
tckgen: [ 15%]   390950 seeds,   293792 streamlines,   150000 selected
tckgen: [ 29%]   781241 seeds,   587050 streamlines,   299938 selected
tckgen: [ 60%]  1559232 seeds,  1171682 streamlines,   600001 selected
tckgen: [100%]  2600157 seeds,  1954332 streamlines,  1000000 selected

┌─ tckgen (TRX float16)         ─────────────────────────────────────────────┐
│  wall:   685.35s    user:  5446.07s    sys:   9.53s    peak mem:  1.37 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo tracks_f16.trx
du -sh  tracks_f16.trx
***********************************
  Tracks file: "tracks_f16.trx"
    TRX streamlines:      1000000
    TRX vertices:         129735473
    Metadata:
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773965322.5397930145
      unidirectional: 0
750M    tracks_f16.trx

The float16 TRX is roughly half the size of the TCK at the same streamline count. For the remainder of this walkthrough we use tckconvert to produce a float32 TRX from the same TCK, so both pipelines operate on byte-for-byte identical streamlines and any output differences are purely due to format, not stochastic variation.

source ../timing_utils.sh
mtime "tckconvert → TRX" \
tckconvert tracks.tck tracks.trx -force
tckconvert: [WARNING] existing output files will be overwritten

┌─ tckconvert → TRX           ─────────────────────────────────────────────┐
│  wall:     5.58s    user:     2.38s    sys:   2.70s    peak mem:  1.45 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo tracks.trx
du -sh  tracks.trx
***********************************
  Tracks file: "tracks.trx"
    TRX streamlines:      1000000
    TRX vertices:         129654918
    VOXEL_TO_RASMM:       [1, 0, 0, 0]
                          [0, 1, 0, 0]
                          [0, 0, 1, 0]
                          [0, 0, 0, 1]
    DIMENSIONS:           [1, 1, 1]
    Metadata:
      command_history: tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
      count: 1000000
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      mrtrix_version: 3.0.8-1694-g783d7f06-dirty
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773964637.623773098
      total_count: 1953917
      unidirectional: 0
1.5G    tracks.trx

Step 2 — SIFT2 weighting

SIFT2 calibrates per-streamline weights to match the WM FOD amplitudes. In the classic pipeline the weights land in a separate CSV. In the TRX pipeline they are embedded as a data_per_streamline field named weights.

CLASSIC

source ../timing_utils.sh
mtime "tcksift2 (classic)" \
tcksift2                   \
    tracks.tck             \
    wm_fod.mif.gz          \
    weights_classic.csv    \
    -nthreads 8            \
    -force
tcksift2: [WARNING] existing output files will be overwritten
tcksift2: uncompressing image "wm_fod.mif.gz"... [==================================================]
tcksift2: Creating homogeneous processing mask... [==================================================]
tcksift2: segmenting FODs... [==================================================]
tcksift2: mapping tracks to image... [==================================================]
tcksift2:          Iteration     CF (data)       CF (reg)    Streamlines
..tcksift2: [  . ]         1        31.068%         0.281%        1000000
..tcksift2: [  . ]         2        24.118%         0.762%        1000000
..tcksift2: [   .]         3        22.534%         1.163%        1000000
....tcksift2: [ .  ]         5        21.240%         1.678%        1000000
........tcksift2: [  . ]         9        20.343%         2.147%        1000000
................tcksift2: [   .]        17        19.844%         2.448%        1000000
..............................tcksift2: [done]        32        19.616%         2.595%        1000000

┌─ tcksift2 (classic)           ─────────────────────────────────────────────┐
│  wall:    55.01s    user:   418.28s    sys:   3.58s    peak mem:  2.16 GB  │
└────────────────────────────────────────────────────────────────────────────┘
wc -l weights_classic.csv
du -sh tracks.tck weights_classic.csv
       2 weights_classic.csv
1.5G    tracks.tck
 12M    weights_classic.csv

TRX

source ../timing_utils.sh
mtime "tcksift2 (TRX)" \
tcksift2               \
    tracks.trx         \
    wm_fod.mif.gz      \
    weights            \
    -nthreads 8        \
    -force
tcksift2: [WARNING] existing output files will be overwritten
tcksift2: uncompressing image "wm_fod.mif.gz"... [==================================================]
tcksift2: Creating homogeneous processing mask... [==================================================]
tcksift2: segmenting FODs... [==================================================]
tcksift2:          Iteration     CF (data)       CF (reg)    Streamlines
..tcksift2: [  . ]         1        31.068%         0.281%        1000000
..tcksift2: [  . ]         2        24.118%         0.762%        1000000
..tcksift2: [   .]         3        22.534%         1.163%        1000000
....tcksift2: [ .  ]         5        21.240%         1.678%        1000000
........tcksift2: [  . ]         9        20.343%         2.147%        1000000
................tcksift2: [   .]        17        19.844%         2.448%        1000000
..............................tcksift2: [done]        32        19.616%         2.595%        1000000

┌─ tcksift2 (TRX)               ─────────────────────────────────────────────┐
│  wall:    56.08s    user:   416.62s    sys:   5.01s    peak mem:  2.14 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo tracks.trx
***********************************
  Tracks file: "tracks.trx"
    TRX streamlines:      1000000
    TRX vertices:         129654918
    VOXEL_TO_RASMM:       [1, 0, 0, 0]
                          [0, 1, 0, 0]
                          [0, 0, 1, 0]
                          [0, 0, 0, 1]
    DIMENSIONS:           [1, 1, 1]
    Metadata:
      command_history: tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
      count: 1000000
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      mrtrix_version: 3.0.8-1694-g783d7f06-dirty
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773964637.623773098
      total_count: 1953917
      unidirectional: 0
    Data per streamline (1):
      weights: 1000000 x 1

Step 3 — Sample NODDI scalar maps

NODDI provides two microstructure metrics:

  • ICVF (intracellular volume fraction) — neurite density proxy
  • ISOVF (isotropic volume fraction) — free water content

tcksample interpolates a volumetric image at every streamline vertex, yielding:

  • dpv (per-vertex) — full sampled profile; used for mrview coloring and along-tract statistics
  • dps mean (per-streamline) — one number per streamline; useful as a connectivity weight or covariate

CLASSIC

source ../timing_utils.sh
mtime "tcksample icvf dpv" \
tcksample tracks.tck icvf.nii.gz icvf.tsf -force

mtime "tcksample isovf dpv" \
tcksample tracks.tck isovf.nii.gz isovf.tsf -force

mtime "tcksample icvf mean" \
tcksample tracks.tck icvf.nii.gz icvf_mean.txt -stat_tck mean -force

mtime "tcksample isovf mean" \
tcksample tracks.tck isovf.nii.gz isovf_mean.txt -stat_tck mean -force

du -sh icvf.tsf isovf.tsf icvf_mean.txt isovf_mean.txt
tcksample: [WARNING] existing output files will be overwritten
tcksample: uncompressing image "icvf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample icvf dpv           ─────────────────────────────────────────────┐
│  wall:     1.26s    user:     3.66s    sys:   0.62s    peak mem:  0.08 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: [WARNING] existing output files will be overwritten
tcksample: uncompressing image "isovf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample isovf dpv          ─────────────────────────────────────────────┐
│  wall:     1.23s    user:     3.86s    sys:   0.62s    peak mem:  0.10 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: [WARNING] existing output files will be overwritten
tcksample: uncompressing image "icvf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample icvf mean          ─────────────────────────────────────────────┐
│  wall:     1.40s    user:     4.12s    sys:   0.24s    peak mem:  0.03 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: [WARNING] existing output files will be overwritten
tcksample: uncompressing image "isovf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample isovf mean         ─────────────────────────────────────────────┐
│  wall:     1.41s    user:     4.15s    sys:   0.24s    peak mem:  0.03 GB  │
└────────────────────────────────────────────────────────────────────────────┘

512M    icvf.tsf
512M    isovf.tsf
 12M    icvf_mean.txt
 13M    isovf_mean.txt

TRX

source ../timing_utils.sh
mtime "tcksample icvf dpv" \
tcksample tracks.trx icvf.nii.gz icvf

mtime "tcksample isovf dpv" \
tcksample tracks.trx isovf.nii.gz isovf

mtime "tcksample icvf mean" \
tcksample tracks.trx icvf.nii.gz icvf_mean -stat_tck mean

mtime "tcksample isovf mean" \
tcksample tracks.trx isovf.nii.gz isovf_mean -stat_tck mean

tckinfo -prefix_depth 2 tracks.trx
tcksample: uncompressing image "icvf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample icvf dpv           ─────────────────────────────────────────────┐
│  wall:     2.25s    user:     6.78s    sys:   1.61s    peak mem:  1.57 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: uncompressing image "isovf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample isovf dpv          ─────────────────────────────────────────────┐
│  wall:     2.71s    user:     5.02s    sys:   2.13s    peak mem:  1.56 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: uncompressing image "icvf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample icvf mean          ─────────────────────────────────────────────┐
│  wall:     2.91s    user:     7.15s    sys:   2.18s    peak mem:  0.15 GB  │
└────────────────────────────────────────────────────────────────────────────┘

tcksample: uncompressing image "isovf.nii.gz"... [==================================================]
tcksample: Sampling values underlying streamlines... [==================================================]

┌─ tcksample isovf mean         ─────────────────────────────────────────────┐
│  wall:     2.96s    user:     7.14s    sys:   2.20s    peak mem:  0.15 GB  │
└────────────────────────────────────────────────────────────────────────────┘

***********************************
  Tracks file: "tracks.trx"
    TRX streamlines:      1000000
    TRX vertices:         129654918
    VOXEL_TO_RASMM:       [1, 0, 0, 0]
                          [0, 1, 0, 0]
                          [0, 0, 1, 0]
                          [0, 0, 0, 1]
    DIMENSIONS:           [1, 1, 1]
    Metadata:
      command_history: tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
      count: 1000000
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      mrtrix_version: 3.0.8-1694-g783d7f06-dirty
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773964637.623773098
      total_count: 1953917
      unidirectional: 0
    Data per streamline (3):
      icvf_mean: 1000000 x 1
      isovf_mean: 1000000 x 1
      weights: 1000000 x 1
    Data per vertex (2):
      icvf: 129654918 x 1
      isovf: 129654918 x 1

Step 4 — Visualize results in mrview

mrview loads TRX files directly. The SIFT2 weights and NODDI scalars embedded in the previous two steps are immediately available as colormap sources — no sidecar files to locate or load separately.

Open tracks.trx in mrview → Tools → Tractography → Colour → Scalar file → TRX field… and select icvf, isovf, or weights for per-vertex or per-streamline coloring. If a field name exists in both dps and dpv, disambiguate using dps:<name> or dpv:<name>.

Command-line capture via -capture.folder, -capture.prefix, and -capture.grab works with TRX inputs (requires a live GUI session):

mrview mask.nii.gz \
  -mode 2 \
  -imagevisible 0 \
  -tractography.load tracks.trx \
  -tractography.geometry lines \
  -tractography.opacity 1.0 \
  -tractography.thickness 0.25 \
  -tractography.slab -1 \
  -tractography.trx_scalar dps:weights \
  -tractography.tsf_range 0.15,2.5 \
  -capture.folder . \
  -capture.prefix trx_sift_overlay \
  -capture.grab \
  -exit
source ../timing_utils.sh
rm -f trx_sift_overlay*.png mrview_trx_overlay.png mrview_trx_overlay_fallback.svg
rm -f ../mrview_trx_overlay.png ../mrview_trx_overlay_fallback.svg

set +e
mrview mask.nii.gz \
  -mode 2 \
  -imagevisible 0 \
  -tractography.load tracks.trx \
  -tractography.geometry lines \
  -tractography.opacity 1.0 \
  -tractography.thickness 0.25 \
  -tractography.slab -1 \
  -tractography.trx_scalar dps:weights \
  -tractography.tsf_range 0.15,2.5 \
  -capture.folder . \
  -capture.prefix trx_sift_overlay \
  -capture.grab \
  -exit
mrview_status=$?
set -e

capture_file=""
for f in trx_sift_overlay*.png; do
  if [ -f "$f" ]; then
    capture_file="$f"
    break
  fi
done

if [ -n "$capture_file" ]; then
  cp "$capture_file" mrview_trx_overlay.png
  cp "$capture_file" ../mrview_trx_overlay.png
  echo "Captured mrview image: \`$capture_file\`"
  echo ""
  echo "![TRX streamlines coloured by embedded SIFT2 weights](mrview_trx_overlay.png)"
fi

Captured mrview image: trx_sift_overlay0000.png

TRX streamlines coloured by embedded SIFT2 weights

Step 5 — Atlas labeling and connectome construction

tck2connectome assigns streamlines to node pairs on-the-fly — the assignment is ephemeral and must be recomputed for every new atlas or metric.

trxlabel embeds the assignment as TRX groups permanently. Multiple atlases can be labeled in a single pass over the tractogram by repeating -nodes, -lut, and -prefix — geometry is read exactly once regardless of how many atlases are requested.

4a — Classic: two separate tck2connectome runs

The classic pipeline must reread all geometry and redo the radial search for each atlas.

source ../timing_utils.sh
mtime "tck2connectome Glasser" \
tck2connectome                  \
    tracks.tck                  \
    glasser.mif.gz              \
    connectome_glasser_classic.csv \
    -tck_weights_in weights_classic.csv \
    -assignment_radial_search 2 \
    -symmetric                  \
    -zero_diagonal              \
    -force
tck2connectome: [WARNING] existing output files will be overwritten
tck2connectome: uncompressing image "glasser.mif.gz"... [==================================================]
tck2connectome: Constructing connectome... [==================================================]

┌─ tck2connectome Glasser       ─────────────────────────────────────────────┐
│  wall:     1.52s    user:     1.72s    sys:   0.26s    peak mem:  0.07 GB  │
└────────────────────────────────────────────────────────────────────────────┘
source ../timing_utils.sh
mtime "tck2connectome 4S456" \
tck2connectome                  \
    tracks.tck                  \
    4S456.mif.gz                \
    connectome_4S456_classic.csv \
    -tck_weights_in weights_classic.csv \
    -assignment_radial_search 2 \
    -symmetric                  \
    -zero_diagonal              \
    -force
tck2connectome: [WARNING] existing output files will be overwritten
tck2connectome: uncompressing image "4S456.mif.gz"... [==================================================]
tck2connectome: Constructing connectome... [==================================================]

┌─ tck2connectome 4S456         ─────────────────────────────────────────────┐
│  wall:     1.48s    user:     1.64s    sys:   0.23s    peak mem:  0.07 GB  │
└────────────────────────────────────────────────────────────────────────────┘

4b — TRX: both atlases in a single trxlabel pass

trxlabel accepts multiple -nodes/-lut/-prefix triplets and labels all atlases in one pass over the tractogram — no geometry is read twice.

source ../timing_utils.sh
mtime "trxlabel Glasser + 4S456" \
trxlabel                         \
    tracks.trx tracks.trx        \
    -nodes  glasser.mif.gz       \
    -nodes  4S456.mif.gz         \
    -lut    glasser.txt          \
    -lut    4S456.txt            \
    -prefix glasser              \
    -prefix 4S456                \
    -assignment_radial_search 2  \
    -force
trxlabel: [WARNING] existing output files will be overwritten
trxlabel: uncompressing image "glasser.mif.gz"... [==================================================]
trxlabel: Assigning streamlines to nodes (glasser)... [==================================================]
trxlabel: uncompressing image "4S456.mif.gz"... [==================================================]
trxlabel: Assigning streamlines to nodes (4S456)... [==================================================]

┌─ trxlabel Glasser + 4S456     ─────────────────────────────────────────────┐
│  wall:     3.34s    user:     1.28s    sys:   2.01s    peak mem:  0.06 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo -prefix_depth 2 tracks.trx
***********************************
  Tracks file: "tracks.trx"
    TRX streamlines:      1000000
    TRX vertices:         129654918
    VOXEL_TO_RASMM:       [1, 0, 0, 0]
                          [0, 1, 0, 0]
                          [0, 0, 1, 0]
                          [0, 0, 0, 1]
    DIMENSIONS:           [1, 1, 1]
    Metadata:
      command_history: tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
      count: 1000000
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      mrtrix_version: 3.0.8-1694-g783d7f06-dirty
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773964637.623773098
      total_count: 1953917
      unidirectional: 0
    Groups (816):
      4S456_Cerebellar_*: 273210 streamlines (10 groups)
      4S456_LH_*: 588758 streamlines (202 groups)
      4S456_LH-Anterior: 2561 streamlines
      4S456_LH-Ca: 7353 streamlines
      4S456_LH-Central_Lateral-Lateral_Posterior-Medial_Pulvinar: 5626 streamlines
      4S456_LH-EXA: 445 streamlines
      4S456_LH-GPe: 2269 streamlines
      4S456_LH-GPi: 1205 streamlines
      4S456_LH-HN: 105 streamlines
      4S456_LH-HTH: 4482 streamlines
      4S456_LH-MN: 207 streamlines
      4S456_LH-Medio_Dorsal: 6149 streamlines
      4S456_LH-NAC: 522 streamlines
      4S456_LH-Pu: 14048 streamlines
      4S456_LH-Pulvinar: 6437 streamlines
      4S456_LH-RN: 2375 streamlines
      4S456_LH-SNc_PBP_VTA: 682 streamlines
      4S456_LH-SNr: 1544 streamlines
      4S456_LH-STH: 1487 streamlines
      4S456_LH-VeP: 134 streamlines
      4S456_LH-Ventral_*: 8945 streamlines (3 groups)
      4S456_RH_*: 587158 streamlines (202 groups)
      4S456_RH-Anterior: 1323 streamlines
      4S456_RH-Ca: 9240 streamlines
      4S456_RH-Central_Lateral-Lateral_Posterior-Medial_Pulvinar: 7574 streamlines
      4S456_RH-EXA: 419 streamlines
      4S456_RH-GPe: 1955 streamlines
      4S456_RH-GPi: 1883 streamlines
      4S456_RH-HN: 327 streamlines
      4S456_RH-HTH: 5344 streamlines
      4S456_RH-MN: 226 streamlines
      4S456_RH-Medio_Dorsal: 5435 streamlines
      4S456_RH-NAC: 501 streamlines
      4S456_RH-Pu: 12911 streamlines
      4S456_RH-Pulvinar: 8401 streamlines
      4S456_RH-RN: 3009 streamlines
      4S456_RH-SNc_PBP_VTA: 734 streamlines
      4S456_RH-SNr: 1676 streamlines
      4S456_RH-STH: 1093 streamlines
      4S456_RH-VeP: 378 streamlines
      4S456_RH-Ventral_*: 9199 streamlines (3 groups)
      glasser_Left_*: 582226 streamlines (180 groups)
      glasser_Right_*: 579407 streamlines (180 groups)
    Data per streamline (3):
      icvf_mean: 1000000 x 1
      isovf_mean: 1000000 x 1
      weights: 1000000 x 1
    Data per vertex (2):
      icvf: 129654918 x 1
      isovf: 129654918 x 1
source ../timing_utils.sh
mtime "trx2connectome Glasser" \
trx2connectome                   \
    tracks.trx                   \
    connectome_glasser_trx.csv   \
    -tck_weights_in weights      \
    -out_node_names glasser_nodes.txt \
    -group_prefix glasser        \
    -lut glasser.txt             \
    -symmetric                   \
    -zero_diagonal               \
    -force
trx2connectome: [WARNING] existing output files will be overwritten
trx2connectome: Building connectome from TRX groups... [==================================================]

┌─ trx2connectome Glasser       ─────────────────────────────────────────────┐
│  wall:     5.26s    user:     0.76s    sys:   4.17s    peak mem:  0.10 GB  │
└────────────────────────────────────────────────────────────────────────────┘
source ../timing_utils.sh
mtime "trx2connectome 4S456" \
trx2connectome                 \
    tracks.trx                 \
    connectome_4S456_trx.csv   \
    -tck_weights_in weights    \
    -out_node_names 4S456_nodes.txt \
    -group_prefix 4S456        \
    -lut 4S456.txt             \
    -symmetric                 \
    -zero_diagonal             \
    -force
trx2connectome: [WARNING] existing output files will be overwritten
trx2connectome: Building connectome from TRX groups... [==================================================]

┌─ trx2connectome 4S456         ─────────────────────────────────────────────┐
│  wall:     4.95s    user:     0.79s    sys:   4.12s    peak mem:  0.11 GB  │
└────────────────────────────────────────────────────────────────────────────┘
tckinfo -prefix_depth 2 tracks.trx
***********************************
  Tracks file: "tracks.trx"
    TRX streamlines:      1000000
    TRX vertices:         129654918
    VOXEL_TO_RASMM:       [1, 0, 0, 0]
                          [0, 1, 0, 0]
                          [0, 0, 1, 0]
                          [0, 0, 0, 1]
    DIMENSIONS:           [1, 1, 1]
    Metadata:
      command_history: tckgen wm_fod.mif.gz -algorithm iFOD2 -seed_image mask.nii.gz -mask mask.nii.gz -minlength 30 -maxlength 250 -select 1000000 -nthreads 8 -force tracks.tck  (version=3.0.8-1694-g783d7f06-dirty)
      count: 1000000
      downsample_factor: 3
      fod_power: 0.25
      init_threshold: 0.100000001
      lmax: 8
      max_angle: 45
      max_dist: 250
      max_num_seeds: 1000000000
      max_num_tracks: 1000000
      max_seed_attempts: 1000
      max_trials: 1000
      method: iFOD2
      min_dist: 30
      mrtrix_version: 3.0.8-1694-g783d7f06-dirty
      rk4: 0
      samples_per_step: 4
      sh_precomputed: 1
      source: wm_fod.mif.gz
      step_size: 0.625
      stop_on_all_include: 0
      threshold: 0.100000001
      timestamp: 1773964637.623773098
      total_count: 1953917
      unidirectional: 0
    Groups (816):
      4S456_Cerebellar_*: 273210 streamlines (10 groups)
      4S456_LH_*: 588758 streamlines (202 groups)
      4S456_LH-Anterior: 2561 streamlines
      4S456_LH-Ca: 7353 streamlines
      4S456_LH-Central_Lateral-Lateral_Posterior-Medial_Pulvinar: 5626 streamlines
      4S456_LH-EXA: 445 streamlines
      4S456_LH-GPe: 2269 streamlines
      4S456_LH-GPi: 1205 streamlines
      4S456_LH-HN: 105 streamlines
      4S456_LH-HTH: 4482 streamlines
      4S456_LH-MN: 207 streamlines
      4S456_LH-Medio_Dorsal: 6149 streamlines
      4S456_LH-NAC: 522 streamlines
      4S456_LH-Pu: 14048 streamlines
      4S456_LH-Pulvinar: 6437 streamlines
      4S456_LH-RN: 2375 streamlines
      4S456_LH-SNc_PBP_VTA: 682 streamlines
      4S456_LH-SNr: 1544 streamlines
      4S456_LH-STH: 1487 streamlines
      4S456_LH-VeP: 134 streamlines
      4S456_LH-Ventral_*: 8945 streamlines (3 groups)
      4S456_RH_*: 587158 streamlines (202 groups)
      4S456_RH-Anterior: 1323 streamlines
      4S456_RH-Ca: 9240 streamlines
      4S456_RH-Central_Lateral-Lateral_Posterior-Medial_Pulvinar: 7574 streamlines
      4S456_RH-EXA: 419 streamlines
      4S456_RH-GPe: 1955 streamlines
      4S456_RH-GPi: 1883 streamlines
      4S456_RH-HN: 327 streamlines
      4S456_RH-HTH: 5344 streamlines
      4S456_RH-MN: 226 streamlines
      4S456_RH-Medio_Dorsal: 5435 streamlines
      4S456_RH-NAC: 501 streamlines
      4S456_RH-Pu: 12911 streamlines
      4S456_RH-Pulvinar: 8401 streamlines
      4S456_RH-RN: 3009 streamlines
      4S456_RH-SNc_PBP_VTA: 734 streamlines
      4S456_RH-SNr: 1676 streamlines
      4S456_RH-STH: 1093 streamlines
      4S456_RH-VeP: 378 streamlines
      4S456_RH-Ventral_*: 9199 streamlines (3 groups)
      glasser_Left_*: 582226 streamlines (180 groups)
      glasser_Right_*: 579407 streamlines (180 groups)
    Data per streamline (3):
      icvf_mean: 1000000 x 1
      isovf_mean: 1000000 x 1
      weights: 1000000 x 1
    Data per vertex (2):
      icvf: 129654918 x 1
      isovf: 129654918 x 1

4c — TRX combined-atlas connectome in one matrix

Because both atlas group sets are embedded in one TRX file, trx2connectome can emit a single matrix that includes:

  • within-atlas edges (Glasser-Glasser, 4S456-4S456), and
  • cross-atlas edges (Glasser-4S456),

without rerunning any node assignment.

source ../timing_utils.sh
mtime "trx2connectome combined-atlas matrix" \
trx2connectome \
    tracks.trx \
    connectome_combined_trx.csv \
    -tck_weights_in weights \
    -out_node_names combined_nodes.txt \
    -symmetric \
    -zero_diagonal \
    -force
trx2connectome: [WARNING] existing output files will be overwritten
trx2connectome: Building connectome from TRX groups... [==================================================]

┌─ trx2connectome combined-atlas matrix ─────────────────────────────────────────────┐
│  wall:     5.03s    user:     0.91s    sys:   4.05s    peak mem:  0.12 GB  │
└────────────────────────────────────────────────────────────────────────────┘
python3 - <<'EOF'
import collections
import itertools
import numpy as np

mat = np.loadtxt("connectome_combined_trx.csv", delimiter=",")
names = [line.strip() for line in open("combined_nodes.txt") if line.strip()]

def atlas_prefix(name: str) -> str:
    return name.split("_", 1)[0] if "_" in name else "(no_prefix)"

if mat.shape[0] != len(names):
    raise RuntimeError(f"matrix shape {mat.shape} does not match node-name list length {len(names)}")

prefix_counts = collections.Counter(atlas_prefix(n) for n in names)
print(f"combined matrix shape: {mat.shape}")
print("node counts by atlas prefix:")
for k in sorted(prefix_counts):
    print(f"  {k}: {prefix_counts[k]}")

indices = {k: [i for i, n in enumerate(names) if atlas_prefix(n) == k] for k in sorted(prefix_counts)}
print("non-zero edge counts by atlas block:")
for a, b in itertools.combinations_with_replacement(sorted(indices), 2):
    block = mat[np.ix_(indices[a], indices[b])]
    nnz = int((block > 0).sum())
    total = float(block.sum())
    print(f"  {a:>8s} x {b:<8s}: nnz={nnz:6d}, sum={total:.3f}")
EOF
combined matrix shape: (816, 816)
node counts by atlas prefix:
  4S456: 456
  glasser: 360
non-zero edge counts by atlas block:
     4S456 x 4S456   : nnz= 62316, sum=1024693.216
     4S456 x glasser : nnz= 47585, sum=1712355.375
   glasser x glasser : nnz= 36988, sum=706430.507

Step 6 — Verify equivalence

With -lut, trx2connectome orders rows and columns by numeric node ID — the same ordering tck2connectome uses. The matrices are directly comparable with no reordering.

python3 - <<'EOF'
import numpy as np, sys
ok = True
for atlas in [("Glasser", "connectome_glasser_classic.csv", "connectome_glasser_trx.csv"),
              ("4S456",   "connectome_4S456_classic.csv",   "connectome_4S456_trx.csv")]:
    name, cf, tf = atlas
    a = np.loadtxt(cf, delimiter=",")
    b = np.loadtxt(tf, delimiter=",")
    if a.shape != b.shape:
        print(f"{name}: SHAPE MISMATCH  classic={a.shape}  trx={b.shape}", file=sys.stderr)
        ok = False
        continue
    diff = np.abs(a - b)
    print(f"{name} {a.shape}  max|diff|={diff.max():.2e}  mean|diff|={diff.mean():.2e}")
    if diff.max() > 0.5:
        ok = False
        print(f"  FAIL: differences too large", file=sys.stderr)
if ok:
    print("All matrices match (within rounding).")
else:
    sys.exit(1)
EOF
Glasser (360, 360)  max|diff|=8.34e-07  mean|diff|=5.38e-10
4S456 (456, 456)  max|diff|=8.34e-07  mean|diff|=4.56e-10
All matrices match (within rounding).
python3 - <<'EOF'
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

pairs = [
    ("Glasser 360",    "connectome_glasser_classic.csv", "connectome_glasser_trx.csv"),
    ("Schaefer 4S456", "connectome_4S456_classic.csv",   "connectome_4S456_trx.csv"),
]

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for row, (name, cf, tf) in enumerate(pairs):
    classic = np.loadtxt(cf, delimiter=",")
    trx     = np.loadtxt(tf, delimiter=",")
    for col, (mat, title) in enumerate([(classic, f"{name}\nClassic"), (trx, f"{name}\nTRX")]):
        ax = axes[row][col]
        im = ax.imshow(np.log1p(mat), cmap="inferno", aspect="auto")
        ax.set_title(title)
        ax.set_xlabel("Node")
        ax.set_ylabel("Node")
        plt.colorbar(im, ax=ax, label="log(1 + weight)")

plt.tight_layout()
plt.savefig("../connectome_comparison.png", dpi=150, bbox_inches="tight")
print("Saved connectome_comparison.png")
EOF
Saved connectome_comparison.png

Connectome comparison

Step 7 — File inventory

echo "=== Classic: files required to reproduce either connectome ==="
ls -lh tracks.tck weights_classic.csv icvf.tsf isovf.tsf icvf_mean.txt isovf_mean.txt \
        connectome_glasser_classic.csv connectome_4S456_classic.csv

echo ""
echo "=== TRX: everything in one file ==="
ls -lh tracks.trx connectome_glasser_trx.csv connectome_4S456_trx.csv
=== Classic: files required to reproduce either connectome ===
-rw-r--r--@ 1 mcieslak  staff   1.3M Mar 19 20:22 connectome_4S456_classic.csv
-rw-r--r--@ 1 mcieslak  staff   804K Mar 19 20:22 connectome_glasser_classic.csv
-rw-r--r--@ 1 mcieslak  staff    12M Mar 19 20:22 icvf_mean.txt
-rw-r--r--@ 1 mcieslak  staff   498M Mar 19 20:22 icvf.tsf
-rw-r--r--@ 1 mcieslak  staff    13M Mar 19 20:22 isovf_mean.txt
-rw-r--r--@ 1 mcieslak  staff   498M Mar 19 20:22 isovf.tsf
-rw-r--r--@ 1 mcieslak  staff   1.5G Mar 19 20:08 tracks.tck
-rw-r--r--@ 1 mcieslak  staff    12M Mar 19 20:21 weights_classic.csv

=== TRX: everything in one file ===
-rw-r--r--@ 1 mcieslak  staff   1.3M Mar 19 20:23 connectome_4S456_trx.csv
-rw-r--r--@ 1 mcieslak  staff   804K Mar 19 20:23 connectome_glasser_trx.csv
-rw-r--r--@ 1 mcieslak  staff   2.4G Mar 19 20:22 tracks.trx

What’s next

TipOther commands that read/write TRX fields
Command TRX capability
tckedit Filters TRX by ROI, length, or count; remaps dps/dpv/groups to surviving streamlines
tcksift Produces a TRX subset via subset_streamlines — all metadata preserved
fixel2tsf Pass a bare field name as output to embed fixel scalars as dpv directly in TRX
tsfinfo / tsfvalidate Inspect and validate TRX dpv fields
mrview Loads TRX directly; colour by group, dps, or dpv field from the GUI