Skip to content

Commit 39ef6e6

Browse files
authored
Merge pull request #3146 from MRtrix3/fix_pehandling_307
Fix phase encoding handling on 3.0.7
2 parents 5ded6ca + ad3148e commit 39ef6e6

13 files changed

Lines changed: 213 additions & 45 deletions

File tree

bin/dwifslpreproc

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ def execute(): #pylint: disable=unused-variable
506506
# This may be required by -rpe_all for extracting b=0 volumes while retaining phase-encoding information
507507
import_dwi_pe_table_option = ''
508508
if dwi_manual_pe_scheme:
509-
phaseencoding.save('dwi_manual_pe_scheme.txt', dwi_manual_pe_scheme)
509+
phaseencoding.save_table('dwi_manual_pe_scheme.txt', dwi_manual_pe_scheme)
510510
import_dwi_pe_table_option = ' -import_pe_table dwi_manual_pe_scheme.txt'
511511

512512

@@ -744,7 +744,7 @@ def execute(): #pylint: disable=unused-variable
744744
# This may be required when setting up the topup call
745745
se_epi_manual_pe_table_option = ''
746746
if se_epi_manual_pe_scheme:
747-
phaseencoding.save('se_epi_manual_pe_scheme.txt', se_epi_manual_pe_scheme)
747+
phaseencoding.save_table('se_epi_manual_pe_scheme.txt', se_epi_manual_pe_scheme)
748748
se_epi_manual_pe_table_option = ' -import_pe_table se_epi_manual_pe_scheme.txt'
749749

750750

@@ -825,8 +825,12 @@ def execute(): #pylint: disable=unused-variable
825825
# Apply the warp field to the input image series to get an initial corrected volume estimate
826826
# applytopup can't receive the complete DWI input and correct it as a whole, because the phase-encoding
827827
# details may vary between volumes
828-
run.command('mrconvert ' + dwi_path + import_dwi_pe_table_option + STRIDES_OPTION_FSL_COMPAT_4D + ' - | '
829-
'mrinfo - -export_pe_eddy applytopup_config.txt applytopup_indices.txt')
828+
run.command('mrconvert ' + dwi_path + ' applytopup_in.nii'
829+
+ import_dwi_pe_table_option
830+
+ STRIDES_OPTION_FSL_COMPAT_4D
831+
+ ' -export_pe_eddy applytopup_config.txt applytopup_indices.txt'
832+
+ ' -export_grad_fsl applytopup_in.bvec applytopup_in.bval'
833+
+ ' -json_export applytopup_in.json')
830834

831835
applytopup_config = matrix.load_matrix('applytopup_config.txt')
832836
if any(row[2] for row in applytopup_config):
@@ -846,20 +850,36 @@ def execute(): #pylint: disable=unused-variable
846850
app.debug('applytopup_indices: ' + str(applytopup_indices))
847851
app.debug('applytopup_volumegroups: ' + str(applytopup_volumegroups))
848852
for index, group in enumerate(applytopup_volumegroups):
849-
prefix = os.path.splitext(dwi_path)[0] + '_pe' + str(index+1)
853+
prefix = 'applytopup_in_pe' + str(index+1)
850854
input_path = prefix + '.nii'
851855
json_path = prefix + '.json'
856+
bvec_path = prefix + '.bvec'
857+
bval_path = prefix + '.bval'
852858
temp_path = prefix + '_applytopup.nii'
853859
output_path = prefix + '_applytopup.mif'
854-
run.command('mrconvert ' + dwi_path + ' ' + input_path + ' -coord 3 ' + ','.join(str(value) for value in group) + STRIDES_OPTION_FSL_COMPAT_4D + ' -json_export ' + json_path)
860+
run.command('mrconvert applytopup_in.nii ' + input_path
861+
+ ' -json_import applytopup_in.json'
862+
+ ' -fslgrad applytopup_in.bvec applytopup_in.bval'
863+
+ ' -coord 3 ' + ','.join(str(value) for value in group)
864+
+ STRIDES_OPTION_FSL_COMPAT_4D
865+
+ ' -json_export ' + json_path
866+
+ ' -export_grad_fsl ' + bvec_path + ' ' + bval_path)
855867
run.command(applytopup_cmd + ' --imain=' + input_path + ' --datain=applytopup_config.txt --inindex=' + str(index+1) + ' --topup=field --out=' + temp_path + ' --method=jac')
856868
app.cleanup(input_path)
857869
temp_path = fsl.find_image(temp_path)
858-
run.command('mrconvert ' + temp_path + ' ' + output_path + ' -json_import ' + json_path)
870+
run.command('mrconvert ' + temp_path + ' ' + output_path
871+
+ ' -json_import ' + json_path
872+
+ ' -fslgrad ' + bvec_path + ' ' + bval_path)
859873
app.cleanup(json_path)
860874
app.cleanup(temp_path)
875+
app.cleanup(bvec_path)
876+
app.cleanup(bval_path)
861877
applytopup_image_list.append(output_path)
862878
index += 1
879+
app.cleanup('applytopup_in.nii')
880+
app.cleanup('applytopup_in.json')
881+
app.cleanup('applytopup_in.bvec')
882+
app.cleanup('applytopup_in.bval')
863883

864884
# Use the initial corrected volumes to derive a brain mask for eddy
865885
if not app.ARGS.eddy_mask:

cmd/mrinfo.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ void usage ()
7878
"raw gradient table information as stored in the image header, i.e. without "
7979
"MRtrix3 back-end processing, use \"-property dw_scheme\"."
8080

81-
+ "The -petable option exports the MRtrix3 internal representation of the phase encoding table, "
82-
"regardless of whether the relevant metadata are stored in the BIDS fields \"PhaseEncodingDirection\" "
83-
"and \"TotalReadoutTime\" or the MRtrix3-specific \"pe_scheme\". The contents of this query "
84-
"should however *not* be provided to FSL tools: despite the contents being of the same format, "
85-
"the phase encoding directions may be erroneously interpreted. If extracting phase encoding information "
86-
"to interface with FSL tools, use the -export_pe_topup or -export_pe_eddy options."
87-
8881
+ DWI::bvalue_scaling_description;
8982

9083
ARGUMENTS
@@ -115,7 +108,7 @@ void usage ()
115108
+ Option ("shell_indices", "list the image volumes attributed to each b-value shell")
116109

117110
+ Metadata::PhaseEncoding::ExportOptions
118-
+ Option ("petable", "print the MRtrix internal representation of the phase encoding table (see Description)")
111+
+ Option ("petable", "print the phase encoding table")
119112

120113
+ OptionGroup ("Handling of piped images")
121114
+ Option ("nodelete", "don't delete temporary images or images passed to mrinfo via Unix pipes");

core/header.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ namespace MR
108108
auto scheme = DWI::resolve_DW_scheme (parse_matrix (item.second), parse_matrix (it->second));
109109
DWI::set_DW_scheme (new_keyval, scheme);
110110
} catch (Exception& e) {
111-
WARN("Unable to merge inconsistent DW gradient tables between headers");
111+
INFO("Unable to merge inconsistent DW gradient tables between headers");
112112
new_keyval["dw_scheme"] = "variable";
113113
}
114114
}
@@ -342,7 +342,8 @@ namespace MR
342342
}
343343
parser.calculate_padding (Pdim);
344344

345-
345+
// FIXME This fails to appropriately assign rows of these schemes to images
346+
// if splitting 4D image into 2D images
346347
const bool split_4d_schemes = (parser.ndim() == 1 && template_header.ndim() == 4);
347348
Eigen::MatrixXd dw_scheme, pe_scheme;
348349
try {

core/metadata/phase_encoding.cpp

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ namespace MR {
2727
using namespace App;
2828
const OptionGroup ImportOptions =
2929
OptionGroup("Options for importing phase-encode tables")
30+
+ Option("import_pe_table", "import a phase-encoding table from file")
31+
+ Argument("file").type_file_in()
3032
+ Option("import_pe_topup", "import a phase-encoding table intended for FSL TOPUP from file")
3133
+ Argument("file").type_file_in()
3234
+ Option("import_pe_eddy", "import phase-encoding information from an EDDY-style config / index file pair")
@@ -44,6 +46,8 @@ namespace MR {
4446

4547
const OptionGroup ExportOptions =
4648
OptionGroup("Options for exporting phase-encode tables")
49+
+ Option("export_pe_table", "export phase-encoding table to file")
50+
+ Argument("file").type_file_out()
4751
+ Option("export_pe_topup", "export phase-encoding table to a file intended for FSL topup")
4852
+ Argument("file").type_file_out()
4953
+ Option("export_pe_eddy", "export phase-encoding information to an EDDY-style config / index file pair")
@@ -180,15 +184,18 @@ namespace MR {
180184
DEBUG("searching for suitable phase encoding data...");
181185
using namespace App;
182186

187+
const auto opt_table = get_options("import_pe_table");
183188
const auto opt_topup = get_options("import_pe_topup");
184189
const auto opt_eddy = get_options("import_pe_eddy");
185-
if (opt_topup.size() + opt_eddy.size() > 1)
190+
if (opt_table.size() + opt_topup.size() + opt_eddy.size() > 1)
186191
throw Exception("Cannot specify more than one command-line option"
187192
" for importing phase encoding information from external file(s)");
188193

189194
scheme_type result;
190195
try {
191-
if (!opt_topup.empty())
196+
if (!opt_table.empty())
197+
result = load_table(opt_table[0][0], header);
198+
else if (!opt_topup.empty())
192199
result = load_topup(opt_topup[0][0], header);
193200
else if (!opt_eddy.empty())
194201
result = load_eddy(opt_eddy[0][0], opt_eddy[0][1], header);
@@ -220,8 +227,13 @@ namespace MR {
220227
try {
221228
pe_scheme = parse_scheme(keyval, H);
222229
} catch (Exception& e) {
223-
WARN("Unable to conform phase encoding information to image realignment "
224-
" for image \"" + H.name() + "\"; erasing");
230+
if ((keyval.find("PhaseEncodingDirection") != keyval.end()
231+
&& keyval["PhaseEncodingDirection"] != "variable")
232+
|| (keyval.find("pe_scheme") != keyval.end()
233+
&& keyval["pe_scheme"] != "variable")) {
234+
WARN("Unable to conform phase encoding information to image realignment"
235+
" for image \"" + H.name() + "\"; erasing");
236+
}
225237
clear_scheme(keyval);
226238
return;
227239
}
@@ -344,7 +356,11 @@ namespace MR {
344356

345357
auto scheme = parse_scheme(header.keyval(), header);
346358

347-
auto opt = get_options("export_pe_topup");
359+
auto opt = get_options("export_pe_table");
360+
if (!opt.empty())
361+
save_table(check(scheme), header, opt[0][0]);
362+
363+
opt = get_options("export_pe_topup");
348364
if (!opt.empty())
349365
save_topup(check(scheme), header, opt[0][0]);
350366

@@ -355,7 +371,32 @@ namespace MR {
355371

356372

357373

374+
scheme_type load_table(const std::string& path, const Header& header) {
375+
if (Path::has_suffix(header.name(), {".nii", ".nii.gz", ".img", ".mgh", "mgz"})) {
376+
WARN("Note use of -import_pe_table in conjunction with MGH / NIfTI image"
377+
" interprets phase encoding directions as being strictly with respect to image axes,"
378+
" not with respect to the FSL internal convention;"
379+
" consider if -import_pe_topup is more appropriate for your use case"
380+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
381+
}
382+
const scheme_type PE = load_matrix(path);
383+
check(PE, header);
384+
// As with JSON import, need to query the header to discover if the
385+
// strides / transform were modified on image load to make the image
386+
// data appear approximately axial, in which case we need to apply the
387+
// same transforms to the phase encoding data on load
388+
return transform_for_image_load(PE, header);
389+
}
390+
391+
392+
358393
scheme_type load_topup(const std::string& path, const Header& header) {
394+
if (!Path::has_suffix(header.name(), {".nii", ".nii.gz", ".img", ".mgh", "mgz"})) {
395+
WARN("Loading FSL topup format phase encoding information"
396+
" accompanying image \"" + header.name() + "\" that is not MGH / NIfTI format"
397+
" may be erroneous due to possible flipping of first image axis"
398+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
399+
}
359400
scheme_type PE = load_matrix(path);
360401
check(PE, header);
361402
// Flip of first image axis based on determinant of image transform
@@ -369,6 +410,12 @@ namespace MR {
369410

370411

371412
scheme_type load_eddy(const std::string& config_path, const std::string& index_path, const Header& header) {
413+
if (!Path::has_suffix(header.name(), {".nii", ".nii.gz", ".img", ".mgh", "mgz"})) {
414+
WARN("Loading FSL eddy format phase encoding information"
415+
" accompanying image \"" + header.name() + "\" that is not MGH / NIfTI format"
416+
" may be erroneous due to possible flipping of first image axis"
417+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
418+
}
372419
const Eigen::MatrixXd config = load_matrix(config_path);
373420
const Eigen::Array<int, Eigen::Dynamic, 1> indices = load_vector<int>(index_path);
374421
scheme_type PE = eddy2topup(config, indices);

core/metadata/phase_encoding.h

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "header.h"
2828
#include "metadata/bids.h"
2929
#include "types.h"
30+
#include "version.h"
3031

3132
namespace MR {
3233
namespace Metadata {
@@ -118,7 +119,8 @@ namespace MR {
118119

119120
if (Path::has_suffix(header.name(), {".mgh", ".mgz", ".nii", ".nii.gz", ".img"})) {
120121
WARN("External phase encoding table \"" + path + "\" for image \"" + header.name() + "\""
121-
" may not be suitable for FSL topup; consider use of -export_pe_topup instead");
122+
" may not be suitable for FSL topup; consider use of -export_pe_topup instead"
123+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
122124
save_table(transform_for_nifti_write(PE, header), path, true);
123125
} else {
124126
save_table(PE, path, true);
@@ -133,10 +135,11 @@ namespace MR {
133135
throw Exception(e, "Cannot export phase-encoding table to file \"" + path + "\"");
134136
}
135137

136-
// TODO Should this check be in place?
137-
if (!Path::has_suffix(header.name(), {".mgh", ".mgz", ".nii", ".nii.gz", ".img"}))
138-
throw Exception("Only export phase encoding table to FSL topup format"
139-
" in conjunction with MGH / NIfTI format images");
138+
if (!Path::has_suffix(header.name(), {".mgh", ".mgz", ".nii", ".nii.gz", ".img"})) {
139+
WARN("Beware use of -export_pe_topup in conjunction image format other than MGH / NIfTI;"
140+
" -export_pe_table may be more suitable"
141+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
142+
}
140143

141144
scheme_type table = transform_for_nifti_write(PE, header);
142145
// The flipping of first axis based on the determinant of the image header transform
@@ -154,9 +157,12 @@ namespace MR {
154157
const HeaderType& header,
155158
const std::string& config_path,
156159
const std::string& index_path) {
157-
if (!Path::has_suffix(header.name(), {".mgh", ".mgz", ".nii", ".nii.gz", ".img"}))
158-
throw Exception("Only export phase encoding table to FSL eddy format"
159-
" in conjunction with MGH / NIfTI format images");
160+
if (!Path::has_suffix(header.name(), {".mgh", ".mgz", ".nii", ".nii.gz", ".img"})) {
161+
WARN("Exporting phase encoding table to FSL eddy format"
162+
" in conjunction with format other than MGH / NIfTI"
163+
" risks erroneous interpretation due to possible flipping of first image axis"
164+
" (see: mrtrix.readthedocs.org/en/" MRTRIX_BASE_VERSION "/concepts/pe_scheme.html#reference-axes-for-phase-encoding-directions)");
165+
}
160166
scheme_type table = transform_for_nifti_write(PE, header);
161167
Axes::permutations_type order;
162168
const auto adjusted_transform = File::NIfTI::adjust_transform(header, order);

0 commit comments

Comments
 (0)