Transforming tck files using ANTS

While this topic has been covered here and here, and is documented, I am unable to get good results, and could use some help.

Given streamlines.tck, flair.nii.gz and b0.nii.gz, here’s my processing with version == mrtransform 3.0_RC1-44-g57e44453 ==:

# Register using ants
FIXED=flair.nii.gz
MOVING=b0.nii.gz
antsRegistration \
    --verbose 1 \
    --dimensionality 3 \
    --output transforms \
    --initial-moving-transform [ "${FIXED}", "${MOVING}", 1 ] \
    --initialize-transforms-per-stage 0 \
    --interpolation LanczosWindowedSinc \
    --transform Translation[ 0.1 ] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --transform Affine[ 0.1 ] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --winsorize-image-intensities [ 0.005, 0.955 ] | tee -a ants.log



warpinit b0.nii.gz identity_warp[].nii.gz -force
for i in {0..2}; do
  WarpImageMultiTransform 3 identity_warp${i}.nii.gz warp${i}.nii.gz -R flair.nii.gz transforms0GenericAffine.mat;
done;
# Recombine the warp file
warpcorrect -force warp[].nii.gz warp.mif

# Convert back to .tck files, depends on the patch...
tcknormalise -force streamlines.tck warp.mif flair_streamlines.tck

# View
mrview flair.nii.gz -tractography.load streamlines.tck -tractography.load flair_streamlines.tck 

The process mainly works, but I get bad streamlines here and there. I did use warpcorrect… Is it possible that the origin of the affine transformation is breaking things?

The original streamlines on the flair.nii.gz:

But after the warp process, I get stray streamlines in weird directions (the image doesn’t fully do it justice).

If I read the ANTs command correctly, you are trying to linearly register a b0 to the flair image.

I am not sure why some of your streamlines go haywire but I’d look at the warp first.

Here is what I get for the original warp?.nii.gz:

I guess that WarpImageMultiTransform writes the location of the centre of rotation for locations that are outside the FOV of the translated moving image. Not sure why they do that… This leads to large displacements outside the FOV:

Try this:

for i in {0..2}; do
  antsApplyTransforms -d 3 -i identity_warp${i}.nii.gz -o warp_2_${i}.nii.gz -r "$FIXED" -t transforms0GenericAffine.mat;
done

This produces more sensible warps (0 padded):

I am not sure if ANTs has changed but we might need to change the docs for the case that people only apply a linear transformation.

Also, since you are linearly registering the images, you shouldn’t need a warp. I’d try to convert the .mat file to an ITK plain text transformation (ConvertTransformFile) and use transformconvert (ITK) on that.

ConvertTransformFile 3 transforms0GenericAffine.mat affine.mat --hm

Gives a transformation in plain text but I think you’d need to multiply it component wise with:

  1  1 -1 -1
  1  1 -1 -1
 -1 -1  1  1
```

I think @maxpietsch is probably right, this is most likely related to some of the streamline vertices at the edge of the FOV, where there is no transformation information in the warp. ANTs tends to write zeros in these places, but not always (use the -f option to antsApplyTransforms to force it to use zero). tcknormalise should work OK if these voxels contain NaNs, but doesn’t treat zero any different from any other value (it’s the origin of the coordinate system, after all). The warpcorrect command is a really simple app that simply replaces a triplet of (0,0,0) with (NaN,NaN,NaN) - relying on the fact that the chances of an exact zero position in the warp is very small (not a perfect assumption by any stretch).

In your case however, given that you have used warpcorrect, and yet some vertices are being misplaced, that suggests that those voxels outside the warp field have been assigned non-zero values. It would also explain why these vertices don’t end up at the origin, but somewhere else altogether (although I can’t really tell from your image, they do seem to converge somewhere near the middle of the brain?).

So what I suggest is to use the -f option to antsApplyTransforms, using @maxpietsch’s suggested script modification, but assign very large values to those voxels, e.g. -f 10000 - values that are clearly a long way outside the FOV (unfortunately, it’s not possible to use -f nan, that would solve the problem straight away…). Then, instead of using warpcorrect, just use mrcalc to clean up the transforms:

$ mrcalc -force warp[].nii.gz 9999 -lt warp[].nii.gz nan -if warp.mif
mrcalc: [WARNING] existing output files will be overwritten
mrcalc: [100%] uncompressing image "warp[].nii.gz"
mrcalc: [100%] computing: ((warp[].nii.gz < 9999) ? warp[].nii.gz : nan)

and then try tcknormalise again. Hopefully that’ll fix the issue…

@jdtournier & @maxpietsch, thanks for the helpful advice.

Rather than mapping to an atlas as most users of tcknormalise do, my streamlines are mapped using an affine to the FLAIR image, but I couldn’t find the equivalent of mrtransform for .tck files. Suppose it wouldn’t be all that hard to write…

I tried the -f 10000 flag, and using mrcalc to transform my warp, and constructed a test set of 3 streamlines using this code:

idx = 0

with open('test_streamline{}.txt'.format(idx), 'w') as fid:
  for z in range(-200,200,1):
    fid.write('0 0 {}\n'.format(z))

idx += 1    
with open('test_streamline{}.txt'.format(idx), 'w') as fid:
  for z in range(-200,200,1):
    fid.write('0 {} 0\n'.format(z))
    
idx += 1    
with open('test_streamline{}.txt'.format(idx), 'w') as fid:
  for z in range(-200,200,1):
    fid.write('{} 0 0\n'.format(z))

"""
tckconvert -force test_streamline[].txt test.tck
tckconvert -force test_streamline0.txt test_si.tck
tckconvert -force test_streamline1.txt test_ap.tck
tckconvert -force test_streamline2.txt test_lr.tck

tcknormalise test.tck warp.mif test_flair.tck
tcknormalise test_si.tck warp.mif test_si_flair.tck
tcknormalise test_ap.tck warp.mif test_ap_flair.tck
tcknormalise test_lr.tck warp.mif test_lr_flair.tck
<img src="/uploads/default/original/1X/6b50846943dfff0d88cc31a54736a9b53bbf5797.png" width="605" height="500">
mrview flair.mif -tractography.load test.tck \
-tractography.load test_flair.tck \
-tractography.load test_si_flair.tck \
-tractography.load test_ap_flair.tck \
-tractography.load test_lr_flair.tck
"""    

When I load them, the nan regions don’t seem to be mapped correctly as the arrows show. The slanted streamlines go across the entire volume.

I converted the warped streamlines back to text:

tckconvert test_si_flair.tck test_si_flair[].txt
tckconvert test_lr_flair.tck test_lr_flair[].txt
tckconvert test_ap_flair.tck test_ap_flair[].txt

Looks like the few lines are suspicious:

437: head -5 test_??_flair*.txt
==> test_ap_flair0000000.txt <==
-5.41682e-24 4.59163e-41 0
0 0 0
0 0 0
0 0 0
0 0 0

==> test_lr_flair0000000.txt <==
-5.41682e-24 4.59163e-41 0
0 0 0
0 6.63571e-07 6.63745e-07
2.64008e-06 8.62685e-33 0.000947706
0.000166119 4.10284e-08 4.0057e-11

==> test_si_flair0000000.txt <==
5.06778e+16 4.59163e-41 0
0 0 0
0 0 0
0 0 0
0 0 0

Not sure what the expected values should be…

I did figure out a work around. First calculate a mask where the transform is valid:

# calculate a mask
mrcalc -force warp0.nii.gz 9999 -lt warp1.nii.gz 9999 -lt warp2.nii.gz 9999 -lt -mult -mult warp_mask.mif

Truncate the streamlines in the diffusion space:

tckedit -force -mask warp_mask.mif streamlines.tck streamlines_masked.tck

And then transform using tcknormalise:

tcknormalise -force streamlines_masked.tck warp.mif flair_streamlines.tck

And the results look good, i.e. no streamlines streaking off to infinity!

1 Like

Your workaround is also our workaround, just ours relies on the fact that ANTs produces zeros outside the FOV which it did not.

So for reference, this should work:

FIXED=anat.nii.gz
MOVING=dwi.nii.gz
antsRegistration \
    --verbose 1 \
    --dimensionality 3 \
    --output transforms \
    --initial-moving-transform [ "${FIXED}", "${MOVING}", 1 ] \
    --initialize-transforms-per-stage 0 \
    --interpolation LanczosWindowedSinc \
    --transform Affine[ 0.1 ] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --transform GaussianDisplacementField[0.1,1,1] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --winsorize-image-intensities [ 0.005, 0.955 ] | tee -a ants.log

warpinit "$MOVING" identity_warp[].nii.gz -force
for i in {0..2}; do
  antsApplyTransforms -d 3 -i identity_warp${i}.nii.gz -o warp${i}.nii.gz -r "$FIXED" -t transforms0GenericAffine.mat -f 123456789
  mrcalc warp${i}.nii.gz 123456789 -eq nan warp${i}.nii.gz -if warp${i}.mif -force && rm warp${i}.nii.gz
done;

# Recombine the warp file
warpcorrect -force warp[].mif warp.mif
mrtransform "$MOVING" -warp warp.mif moving_transformed.mif

Hi, all,

Thank you for the nice advice. It is very helpful!
Just one quick question: does the warping method change the fiber tracts number?

I followed @maxpietsch’s instruction, and it seems work nicely. However, I find it does not preserve the original fiber tracts number. The original tracts number is 100000 and the output tracts number is 101134. I use TBSS method to register FA image to MNI152 space. Then I runtcksample: [WARNING] Track file reports 100000 tracks, but contains 101134

Thanks,
Chaoqing


The output tracts:

The transforming process:

FIXED="../../RegT1_to_MNI152/T1FS.nii.gz"
MOVING="/home/MNI152_Template_FSL/MNI152_T1_1mm.nii"
antsRegistration \
    --verbose 1 \
    --dimensionality 3 \
    --output transforms \
    --initial-moving-transform [ "${FIXED}", "${MOVING}", 1 ] \
    --initialize-transforms-per-stage 0 \
    --interpolation LanczosWindowedSinc \
    --transform Affine[ 0.1 ] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --transform GaussianDisplacementField[0.1,1,1] \
    --metric MI[ "${FIXED}", "${MOVING}", 1, 32, Regular, 0.25 ] \
    --convergence [ 1000x500x250x0, 1e-06, 10 ] \
    --smoothing-sigmas 3x2x1x0vox \
    --shrink-factors 8x4x2x1 \
    --use-estimate-learning-rate-once 1 \
    --use-histogram-matching 1 \
    --winsorize-image-intensities [ 0.005, 0.955 ] | tee -a ants.log

warpinit “$MOVING” WarpMNI152FSL/WarpMNI152FSL-.nii.gz -force

for i in {0…2};
do
antsApplyTransforms -d 3 -i WarpMNI152FSL/WarpMNI152FSL-{i}.nii -o MNI152FSL2tck/MNI152FSL2tck-{i}.nii -r "FIXED" -t transforms0GenericAffine.mat -f 123456789 mrcalc MNI152FSL2tck/MNI152FSL2tck-{i}.nii 123456789 -eq nan MNI152FSL2tck/MNI152FSL2tck-${i}.nii
done;

warpcorrect -force MNI152FSL2tck/MNI152FSL2tck-.nii MNI152FSL2tck/MNI152FSL2tck_correct.mif

mrtransform “$MOVING” -warp MNI152FSL2tck/MNI152FSL2tck_correct.mif MNI152FSL2tck/MNI152_transformed.mif

tcknormalise …/Output_ACT/DTI_30_average-6_preprocessed_ACT_5TTwmmask_seed_dynamic_length20_0.1M.tck MNI152FSL2tck/MNI152_transformed.mif …/Output_ACT/DTI_30_average-6_preprocessed_ACT_5TTwmmask_seed_dynamic_length20_0.1M_warpMNI152FSL.tck

Hi @SuperClear,

This will be the same issue discussed in this thread.

While we’re working on a more comprehensive solution, the best fix for right now is to ensure that the FoV of your warp field is sufficiently large such that no streamline vertices are outside the warp.

Rob