Skip to content

Commit 3f00557

Browse files
committed
Merge pull request #506 from MRtrix3/fix_strides_for_FSL
Changing strides in scripts for input to FSL
2 parents e7783ef + da72e0c commit 3f00557

File tree

6 files changed

+77
-51
lines changed

6 files changed

+77
-51
lines changed

scripts/5ttgen

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ algorithm = getattr(_5ttgen, lib.app.args.algorithm)
4242
lib.app.checkOutputFile(lib.app.args.output)
4343
algorithm.checkOutputFiles()
4444

45-
runCommand('mrconvert ' + lib.app.args.input + ' ' + os.path.join(lib.app.tempDir, 'input.mif') + ' -stride +1,+2,+3')
45+
runCommand('mrconvert ' + lib.app.args.input + ' ' + os.path.join(lib.app.tempDir, 'input.mif'))
4646
algorithm.getInputFiles()
4747

4848
lib.app.gotoTempDir()

scripts/dwibiascorrect

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ lib.app.checkOutputFile(lib.app.args.bias)
6969

7070
runCommand('mrconvert ' + lib.app.args.input + ' ' + os.path.join(lib.app.tempDir, 'in.mif') + grad_import_option)
7171
if lib.app.args.mask:
72-
runCommand('mrconvert ' + lib.app.args.mask + ' ' + os.path.join(lib.app.tempDir, 'mask.nii') + ' -stride +1,+2,+3')
72+
runCommand('mrconvert ' + lib.app.args.mask + ' ' + os.path.join(lib.app.tempDir, 'mask.mif'))
7373

7474
lib.app.gotoTempDir()
7575

@@ -83,28 +83,30 @@ if len(DW_scheme) != int(dwi_sizes[3]):
8383

8484
# Generate a brain mask if required, or check the mask if provided
8585
if lib.app.args.mask:
86-
mask_sizes = getHeaderInfo('mask.nii', 'size').split()
86+
mask_sizes = getHeaderInfo('mask.mif', 'size').split()
8787
if not mask_sizes[:3] == dwi_sizes[:3]:
8888
errorMessage('Provided mask image does not match input DWI')
8989
else:
90-
runCommand('dwi2mask in.mif - | mrconvert - mask.nii -stride +1,+2,+3')
90+
runCommand('dwi2mask in.mif mask.mif')
9191

9292
# Make a NiFTI image since this is what the external softwares need
9393
runCommand('dwiextract in.mif bzeros.mif -bzero')
9494
bzero_sizes = getHeaderInfo('bzeros.mif', 'size')
9595
if len(bzero_sizes.split()) == 4:
96-
runCommand('mrmath bzeros.mif mean - -axis 3 | mrconvert - mean_bzero.nii -stride +1,+2,+3')
96+
runCommand('mrmath bzeros.mif mean - -axis 3 | mrconvert - mean_bzero.mif -stride +1,+2,+3')
9797
else:
98-
runCommand('mrconvert bzeros.mif mean_bzero.nii -stride +1,+2,+3')
98+
runCommand('mrconvert bzeros.mif mean_bzero.mif')
9999

100100
if lib.app.args.fsl:
101101
# FAST doesn't accept a mask input; therefore need to explicitly mask the input image
102-
runCommand('mrcalc mean_bzero.nii mask.nii -mult mean_bzero_masked.nii')
102+
runCommand('mrcalc mean_bzero.mif mask.mif -mult - | mrconvert - mean_bzero_masked.nii -stride -1,+2,+3')
103103
runCommand(fast_cmd + ' -t 2 -o fast -n 3 -b mean_bzero_masked.nii')
104104
bias_path = 'fast_bias' + fast_suffix
105105
elif lib.app.args.ants:
106106
# Use the brain mask as a weights image rather than a mask; means that voxels at the edge of the mask
107107
# will have a smoothly-varying bias field correction applied, rather than multiplying by 1.0 outside the mask
108+
runCommand('mrconvert mean_bzero.mif mean_bzero.nii -stride +1,+2,+3')
109+
runCommand('mrconvert mask.mif mask.nii -stride +1,+2,+3')
108110
bias_path = 'bias.nii'
109111
runCommand('N4BiasFieldCorrection -d 3 -i mean_bzero.nii -w mask.nii -o [corrected.nii,' + bias_path + '] -s 2 -b [150] -c [200x200,0.0]')
110112

scripts/dwipreproc

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ lib.app.gotoTempDir()
103103
series_size = getHeaderInfo('series.mif', 'size').split()
104104
grad = getHeaderInfo('series.mif', 'dwgrad').split('\n')
105105
stride = getHeaderInfo('series.mif', 'stride')
106-
transform = getHeaderInfo('series.mif', 'transform')
107106
if PE_design == 'Pair':
108107
Pair1_size = getHeaderInfo('pair1.mif', 'size').split()
109108
Pair2_size = getHeaderInfo('pair2.mif', 'size').split()
@@ -183,17 +182,17 @@ if not PE_design == 'None':
183182

184183
# Convert the input files as necessary for FSL tools
185184
if PE_design == 'None':
186-
runCommand('mrconvert ' + series_path + ' dwi_pre_topup.nii -stride +1,+2,+3,+4')
185+
runCommand('mrconvert ' + series_path + ' dwi_pre_topup.nii -stride -1,+2,+3,+4')
187186
if PE_design == 'Pair':
188-
runCommand('mrconvert ' + series_path + ' dwi_pre_topup.nii -stride +1,+2,+3,+4')
189-
runCommand('mrcat ' + pair1_path + ' ' + pair2_path + ' - -axis 3 | mrconvert - topup_in.nii -stride +1,+2,+3,+4')
187+
runCommand('mrconvert ' + series_path + ' dwi_pre_topup.nii -stride -1,+2,+3,+4')
188+
runCommand('mrcat ' + pair1_path + ' ' + pair2_path + ' - -axis 3 | mrconvert - topup_in.nii -stride -1,+2,+3,+4')
190189
elif PE_design == 'All':
191190
runCommand('dwiextract ' + series_path + ' -bzero pair1.mif')
192191
runCommand('dwiextract ' + series2_path + ' -bzero pair2.mif')
193-
runCommand('mrcat pair1.mif pair2.mif - -axis 3 | mrconvert - topup_in.nii -stride +1,+2,+3,+4')
194-
runCommand('mrconvert ' + series_path + ' dwi1_pre_topup.nii -stride +1,+2,+3,+4')
192+
runCommand('mrcat pair1.mif pair2.mif - -axis 3 | mrconvert - topup_in.nii -stride -1,+2,+3,+4')
193+
runCommand('mrconvert ' + series_path + ' dwi1_pre_topup.nii -stride -1,+2,+3,+4')
195194
delFile(series_path)
196-
runCommand('mrconvert ' + series2_path + ' dwi2_pre_topup.nii -stride +1,+2,+3,+4')
195+
runCommand('mrconvert ' + series2_path + ' dwi2_pre_topup.nii -stride -1,+2,+3,+4')
197196
delFile(series2_path)
198197
runCommand('mrcat dwi1_pre_topup.nii dwi2_pre_topup.nii dwi_pre_eddy.nii -axis 3')
199198

@@ -249,8 +248,8 @@ eddy_in_topup = ''
249248
if PE_design == 'None':
250249

251250
# Generate a processing mask for eddy based on the input series
252-
runCommand('dwi2mask ' + series_path + ' - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride +1,+2,+3')
253-
runCommand('mrconvert ' + series_path + ' - -stride +1,+2,+3,+4 | mrinfo - -export_grad_fsl bvecs bvals')
251+
runCommand('dwi2mask ' + series_path + ' - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride -1,+2,+3')
252+
runCommand('mrconvert ' + series_path + ' - -stride -1,+2,+3,+4 | mrinfo - -export_grad_fsl bvecs bvals')
254253

255254
else:
256255

@@ -276,7 +275,7 @@ else:
276275
# Create the diffusion gradient table in FSL format
277276
# Make sure the strides are identical to the image actually being passed to eddy before exporting the gradient table
278277
if PE_design == 'Pair':
279-
runCommand('mrconvert ' + series_path + ' - -stride +1,+2,+3,+4 | mrinfo - -export_grad_fsl bvecs bvals')
278+
runCommand('mrconvert ' + series_path + ' - -stride -1,+2,+3,+4 | mrinfo - -export_grad_fsl bvecs bvals')
280279
else:
281280
# Concatenate the diffusion gradient table twice
282281
with open('grad.b', 'w') as outfile:
@@ -288,7 +287,7 @@ else:
288287

289288

290289
# Use the initial corrected image series from applytopup to derive a processing mask for eddy
291-
runCommand('mrconvert dwi_post_topup' + fsl_suffix + ' -fslgrad bvecs bvals - | dwi2mask - - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride +1,+2,+3')
290+
runCommand('mrconvert dwi_post_topup' + fsl_suffix + ' -fslgrad bvecs bvals - | dwi2mask - - | maskfilter - dilate - | mrconvert - mask.nii -datatype float32 -stride -1,+2,+3')
292291

293292
eddy_in_topup = ' --topup=field'
294293

@@ -356,27 +355,36 @@ else: # 'All'
356355
bvals = bvals[0:num_volumes]
357356
with open('bvals_combined', 'w') as f:
358357
f.write(' '.join(bvals))
359-
360358

361-
# Derive the weight images
362-
# To properly get a voxel displacement field, need to scale the field map (which is output in Hz) appropriately
363-
# Scaling appears to be correct; however FSL is a bit ambiguous when it comes to image transforms & strides
364-
# Looks like topup flips the x-axis on output (both corrected images and field), and erases image transform
365-
# Eddy seems to expect this and operates accordingly; but since we're doing things external to FSL and
366-
# voxel-by-voxel, need to do the flip explicitly; also import the transform from the first input image
367-
# for the sake of visualisation
368-
with open( 'transform.txt', 'w' ) as f:
369-
for line in transform:
370-
f.write (line)
371-
runCommand('mrtransform field_map' + fsl_suffix + ' - -flip 0 -linear transform.txt -replace | mrconvert - field_map_flip.mif' + stride_option)
372-
delFile('field_map' + fsl_suffix)
359+
# Prior to 5.0.8, a bug resulted in the output field map image from topup having an identity transform,
360+
# regardless of the transform of the input image
361+
# Detect this, and manually replace the transform if necessary
362+
# (even if this doesn't cause an issue with the subsequent mrcalc command, it may in the future, it's better for
363+
# visualising the script temporary files, and it gives the user a warning about an out-of-date FSL)
364+
input_transform_text = getHeaderInfo('topup_in.nii', 'transform')
365+
input_transform = [ float(f) for f in input_transform_text.replace('\n', ' ').replace(',', ' ').split() ]
366+
topup_transform = [ float(f) for f in getHeaderInfo('field_map' + fsl_suffix, 'transform').replace('\n', ' ').replace(',', ' ').split() ]
367+
transform_error = False
368+
field_map_image = 'field_map' + fsl_suffix
369+
for i, t in zip(input_transform, topup_transform):
370+
if (abs(i-t) / (0.5*(i+t))) > 0.001:
371+
transform_error = True
372+
if transform_error:
373+
warnMessage('topup output field image has incorrect transform; recommend updating FSL to version 5.0.8 or later')
374+
with open( 'transform.txt', 'w' ) as f:
375+
for line in input_transform_text:
376+
f.write (line)
377+
field_map_image = 'field_map_fix' + fsl_suffix
378+
runCommand('mrtransform field_map' + fsl_suffix + ' -linear transform.txt -replace ' + field_map_image)
379+
delFile('field_map' + fsl_suffix)
373380

374381

382+
# Derive the weight images
375383
# Scaling term for field map is identical to the bandwidth provided in the topup config file
376384
# (converts Hz to pixel count; that way a simple image gradient can be used to get the Jacobians)
377385
# Let mrfilter apply the default 1 voxel size gaussian smoothing filter before calculating the field gradient
378-
runCommand('mrcalc field_map_flip.mif 0.1 -mult - | mrfilter - gradient - | mrconvert - field_deriv_pe.mif -coord 3 1 -axes 0,1,2')
379-
delFile('field_map_flip.mif')
386+
runCommand('mrcalc ' + field_map_image + ' 0.1 -mult - | mrfilter - gradient - | mrconvert - field_deriv_pe.mif -coord 3 1 -axes 0,1,2')
387+
delFile(field_map_image)
380388
runCommand('mrcalc 1.0 field_deriv_pe.mif -add 0.0 -max jacobian1.mif')
381389
runCommand('mrcalc 1.0 field_deriv_pe.mif -sub 0.0 -max jacobian2.mif')
382390
delFile('field_deriv_pe.mif')

scripts/labelsgmfix

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ with open(lib.app.args.config) as f:
7676

7777
# Get the parcellation and T1 images into the temporary directory, with conversion of the T1 into the correct format for FSL
7878
runCommand('mrconvert ' + lib.app.args.parc + ' ' + os.path.join(lib.app.tempDir, 'parc.mif'))
79-
runCommand('mrconvert ' + lib.app.args.t1 + ' ' + os.path.join(lib.app.tempDir, 'T1.nii') + ' -stride +1,+2,+3')
79+
runCommand('mrconvert ' + lib.app.args.t1 + ' ' + os.path.join(lib.app.tempDir, 'T1.nii') + ' -stride -1,+2,+3')
8080

8181
lib.app.gotoTempDir()
8282

@@ -90,7 +90,7 @@ runCommand(first_cmd + ' -s ' + ','.join(structure_map.keys()) + ' -i T1.nii' +
9090
# In this use case, don't want the PVE images; want to threshold at 0.5
9191
mask_list = [ ]
9292
for struct in structure_map.keys():
93-
image_path = struct + '_mask.nii'
93+
image_path = struct + '_mask.mif'
9494
mask_list.append (image_path)
9595
vtk_in_path = 'first-' + struct + '_first.vtk'
9696
if not os.path.exists(vtk_in_path):
@@ -111,8 +111,8 @@ for struct in structure_map.keys():
111111
# Detect any overlapping voxels between these masks
112112
# These will be set to 0 in the final parcellated image
113113
result_path = 'result' + os.path.splitext(lib.app.args.output)[1]
114-
runCommand('mrmath ' + ' '.join(mask_list) + ' sum - | mrcalc - 1 -gt overlap_mask.nii')
115-
runCommand('mrcalc overlap_mask.nii 0 parc.mif -if result.mif')
114+
runCommand('mrmath ' + ' '.join(mask_list) + ' sum - | mrcalc - 1 -gt overlap_mask.mif')
115+
runCommand('mrcalc overlap_mask.mif 0 parc.mif -if result.mif')
116116
runCommand('mrconvert result.mif ' + os.path.join(lib.app.workingDir, lib.app.args.output) + lib.app.mrtrixForce)
117117

118118
lib.app.complete()

scripts/src/_5ttgen/fsl.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def getInputFiles():
1919
import lib.app
2020
from lib.runCommand import runCommand
2121
if hasattr(lib.app.args, 'mask') and lib.app.args.mask is not None:
22-
runCommand('mrconvert ' + lib.app.args.mask + ' ' + os.path.join(lib.app.tempDir, 'mask.mif') + ' -datatype bit -stride +1,+2,+3')
22+
runCommand('mrconvert ' + lib.app.args.mask + ' ' + os.path.join(lib.app.tempDir, 'mask.mif') + ' -datatype bit -stride -1,+2,+3')
2323

2424

2525

@@ -76,23 +76,23 @@ def execute():
7676
if lib.app.args.sgm_amyg_hipp:
7777
sgm_structures.extend([ 'L_Amyg', 'R_Amyg', 'L_Hipp', 'R_Hipp' ])
7878

79-
runCommand('mrconvert input.mif T1.nii')
79+
runCommand('mrconvert input.mif T1.nii -stride -1,+2,+3')
8080

8181
# Decide whether or not we're going to do any brain masking
8282
if os.path.exists('mask.mif'):
8383

8484
# Check to see if the dimensions match the T1 image
85-
T1_size = getHeaderInfo('input.mif', 'size')
85+
T1_size = getHeaderInfo('T1.nii', 'size')
8686
mask_size = getHeaderInfo('mask.mif', 'size')
8787
if mask_size == T1_size:
88-
runCommand('mrcalc input.mif mask.mif -mult T1_masked.nii')
88+
runCommand('mrcalc input.mif mask.mif -mult - | mrconvert - T1_masked' + fsl_suffix + ' -stride -1,+2,+3')
8989
else:
90-
runCommand('mrtransform mask.mif mask_regrid.mif -template input.mif')
91-
runCommand('mrcalc input.mif mask_regrid.mif -mult T1_masked' + fsl_suffix)
90+
runCommand('mrtransform mask.mif mask_regrid.mif -template T1.nii')
91+
runCommand('mrcalc input.mif mask_regrid.mif -mult - | mrconvert - T1_masked' + fsl_suffix)
9292

9393
elif lib.app.args.premasked:
9494

95-
runCommand('mrconvert input.mif T1_masked' + fsl_suffix)
95+
runCommand('mrconvert input.mif T1_masked' + fsl_suffix + ' -stride -1,+2,+3')
9696

9797
else:
9898

@@ -121,7 +121,7 @@ def execute():
121121

122122
if not os.path.isfile('T1_preBET' + fsl_suffix):
123123
warnMessage('FSL command ' + ssroi_cmd + ' appears to have failed; passing T1 directly to BET')
124-
runCommand('mrconvert input.mif T1_preBET' + fsl_suffix)
124+
runCommand('mrconvert input.mif T1_preBET' + fsl_suffix + ' -stride -1,+2,+3')
125125

126126
# BET
127127
runCommand(bet_cmd + ' T1_preBET' + fsl_suffix + ' T1_masked' + fsl_suffix + ' -f 0.15 -R')
@@ -140,16 +140,16 @@ def execute():
140140
# Convert FIRST meshes to partial volume images
141141
pve_image_list = [ ]
142142
for struct in sgm_structures:
143-
pve_image_path = 'mesh2pve_' + struct + '.nii'
143+
pve_image_path = 'mesh2pve_' + struct + '.mif'
144144
vtk_in_path = 'first-' + struct + '_first.vtk'
145145
vtk_temp_path = struct + '.vtk'
146146
if not os.path.exists(vtk_in_path):
147147
errorMessage('Missing .vtk file for structure ' + struct + '; run_first_all must have failed')
148-
runCommand('meshconvert ' + vtk_in_path + ' ' + vtk_temp_path + ' -transform_first2real input.mif')
148+
runCommand('meshconvert ' + vtk_in_path + ' ' + vtk_temp_path + ' -transform_first2real T1.nii')
149149
runCommand('mesh2pve ' + vtk_temp_path + ' T1_masked' + fsl_suffix + ' ' + pve_image_path)
150150
pve_image_list.append(pve_image_path)
151151
pve_cat = ' '.join(pve_image_list)
152-
runCommand('mrmath ' + pve_cat + ' sum - | mrcalc - 1.0 -min all_sgms.nii')
152+
runCommand('mrmath ' + pve_cat + ' sum - | mrcalc - 1.0 -min all_sgms.mif')
153153

154154
# Looks like FAST in 5.0 ignores FSLOUTPUTTYPE when writing the PVE images
155155
# Will have to wait and see whether this changes, and update the script accordingly
@@ -163,7 +163,7 @@ def execute():
163163
runCommand('mrthreshold T1_masked_pve_2' + fast_suffix + ' - -abs 0.001 | maskfilter - connect wm_mask.mif -largest')
164164
# Step 2: Generate the images in the same fashion as the 5ttgen command
165165
runCommand('mrconvert T1_masked_pve_0' + fast_suffix + ' csf.mif')
166-
runCommand('mrcalc 1 csf.mif -sub all_sgms.nii -min sgm.mif')
166+
runCommand('mrcalc 1 csf.mif -sub all_sgms.mif -min sgm.mif')
167167
runCommand('mrcalc 1.0 csf.mif sgm.mif -add -sub T1_masked_pve_1' + fast_suffix + ' T1_masked_pve_2' + fast_suffix + ' -add -div multiplier.mif')
168168
runCommand('mrcalc multiplier.mif -finite multiplier.mif 0.0 -if multiplier_noNAN.mif')
169169
runCommand('mrcalc T1_masked_pve_1' + fast_suffix + ' multiplier_noNAN.mif -mult cgm.mif')

src/dwi/gradient.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
* For more details, see www.mrtrix.org
1313
*
1414
*/
15+
1516
#include "dwi/gradient.h"
17+
#include "file/nifti1_utils.h"
1618

1719
namespace MR
1820
{
@@ -80,10 +82,17 @@ namespace MR
8082
if (bvals.cols() != bvecs.cols() || bvals.cols() != header.size (3))
8183
throw Exception ("bvals and bvecs files must have same number of diffusion directions as DW-image");
8284

85+
// bvecs format actually assumes a LHS coordinate system even if image is
86+
// stored using RHS - x axis is flipped to make linear 3x3 part of
87+
// transform have negative determinant:
88+
std::vector<size_t> order;
89+
auto adjusted_transform = File::NIfTI::adjust_transform (header, order);
90+
if (adjusted_transform.linear().determinant() > 0.0)
91+
bvecs.row(0) = -bvecs.row(0);
92+
8393
// account for the fact that bvecs are specified wrt original image axes,
8494
// which may have been re-ordered and/or inverted by MRtrix to match the
8595
// expected anatomical frame of reference:
86-
std::vector<size_t> order = Stride::order (header, 0, 3);
8796
MatrixXd G (bvecs.cols(), 3);
8897
for (ssize_t n = 0; n < G.rows(); ++n) {
8998
G(n,order[0]) = header.stride(order[0]) > 0 ? bvecs(0,n) : -bvecs(0,n);
@@ -116,7 +125,8 @@ namespace MR
116125

117126
// deal with FSL requiring gradient directions to coincide with data strides
118127
// also transpose matrices in preparation for file output
119-
auto order = Stride::order (header, 0, 3);
128+
std::vector<size_t> order;
129+
auto adjusted_transform = File::NIfTI::adjust_transform (header, order);
120130
MatrixXd bvecs (3, grad.rows());
121131
MatrixXd bvals (1, grad.rows());
122132
for (ssize_t n = 0; n < G.rows(); ++n) {
@@ -126,6 +136,12 @@ namespace MR
126136
bvals(0,n) = grad(n,3);
127137
}
128138

139+
// bvecs format actually assumes a LHS coordinate system even if image is
140+
// stored using RHS - x axis is flipped to make linear 3x3 part of
141+
// transform have negative determinant:
142+
if (adjusted_transform.linear().determinant() > 0.0)
143+
bvecs.row(0) = -bvecs.row(0);
144+
129145
save_matrix (bvecs, bvecs_path);
130146
save_matrix (bvals, bvals_path);
131147
}

0 commit comments

Comments
 (0)