Skip to content

mrconvert: fix silent DirectIO axis misbinding in copy_permute(); dwifslpreproc: fix missing f-string in field-map crop - #3432

Open
Rad-dude wants to merge 3 commits into
MRtrix3:devfrom
Rad-dude:fix/mrconvert-direct-io-arg-binding
Open

mrconvert: fix silent DirectIO axis misbinding in copy_permute(); dwifslpreproc: fix missing f-string in field-map crop#3432
Rad-dude wants to merge 3 commits into
MRtrix3:devfrom
Rad-dude:fix/mrconvert-direct-io-arg-binding

Conversation

@Rad-dude

@Rad-dude Rad-dude commented Aug 7, 2026

Copy link
Copy Markdown

Two independent fixes:

  1. Fixes #<3431>
    mrconvert: silent DirectIO axis misbinding in copy_permute().
    Image<T>::create() gained a new std::optional<DirectIO> direct_io
    parameter (Fix deprecated std::shared_ptr<>::unique() on image writeback #3315), inserted between template_header and
    add_to_command_history. copy_permute()'s call site was never updated
    for the new signature, so add_to_command_history (a bool, almost
    always true) silently binds to the new 3rd parameter (direct_io)
    instead of the 4th — true promotes to 1, so every mrconvert output
    image silently gets DirectIO(axis=1). This reorders the on-disk voxel
    layout without a corresponding update to the exported gradient table,
    producing an image/gradient-table mismatch on every NIfTI+FSL bvec
    export via -export_grad_fsl, independent of whether -strides is
    explicitly requested. Fix: pass std::nullopt explicitly for
    direct_io so add_to_command_history binds to its intended parameter.
    Includes a regression test.

  2. dwifslpreproc: missing f-string prefix in field-map crop command.
    The mrconvert call cropping the eddy-derived field map for
    -eddyqc_all output was a plain string literal instead of an f-string,
    so {field_map_image} and {dwi_post_eddy_crop_option} were passed to
    mrconvert as literal uninterpolated text rather than the actual image
    path and crop option -- mrconvert then received 3 positional arguments
    instead of 2 and failed immediately ("Expected exactly 2 arguments (3
    supplied)"). Reproduces on any -eddyqc_all run where topup produces a
    field map, independent of vendor/scan platform. One-character fix (add
    the missing f), matching the pattern already used by the two identical
    sibling mrconvert calls immediately before and after it.

Image<T>::create() gained a new std::optional<DirectIO> direct_io
parameter (PR MRtrix3#3315, inserted between template_header and
add_to_command_history). copy_permute()'s call site was never updated
for the new signature and still passes 3 positional arguments:

    Image<T>::create(output_filepath, header_out, add_to_command_history)

add_to_command_history (a bool, almost always true) silently binds to
the new 3rd parameter (direct_io) instead of the 4th, because bool
implicitly promotes to int, which DirectIO's `DirectIO(int axis)`
constructor accepts. `true` becomes `1`, so every mrconvert output
image is created with an unintended DirectIO(axis=1) request -- i.e.
"make axis 1 the fastest-varying axis in memory"
(Stride::contiguous_along_axis(1, header)).

This silently reorders the in-RAM/on-disk voxel layout without a
corresponding update to the exported gradient table: -export_grad_fsl
(DWI::save_bvecs_bvals(), gradient.cpp) computes bvecs assuming the
image's own axis order via File::NIfTI::adjust_transform(), which
correctly reacts to the now-corrupted Stride::get(header) -- so the
export logic itself is not at fault, it's given bad input. The result
is an image/gradient-table mismatch on every NIfTI+FSL bvec export,
independent of whether -strides is explicitly requested.

Minimal repro (no eddy or -strides needed):

    mrconvert dwi.mif out.nii -export_grad_fsl out.bvec out.bval
    mrconvert out.nii rt.mif -fslgrad out.bvec out.bval
    mrinfo dwi.mif -export_grad_mrtrix orig.b
    mrinfo rt.mif -export_grad_mrtrix rt.b
    diff orig.b rt.b   # non-identical before this fix; identical after

The real add_to_command_history value was masked because it happened
to fall back to its own default (true) either way, which is why this
was invisible from a command-history perspective and only surfaced as
silent gradient-orientation corruption in downstream DWI processing
(e.g. dwifslpreproc's eddy round-trip, or any other -export_grad_fsl
use).

Fix: pass std::nullopt explicitly for direct_io so
add_to_command_history correctly binds to its intended 4th parameter.

Verified against KUL_NIS's KUL_clinical_fmridti.sh clinical DTI
pipeline (dwifslpreproc -> dwibiascorrect -> dwi2fod -> tckgen):
DEC map orientation and dwigradcheck-verified gradient/image
consistency restored end-to-end.
Round-trips the shared dwi.mif test asset through NIfTI + FSL
bvecs/bvals and checks the re-imported gradient table against the
original via testing_diff_matrix. Catches the DirectIO argument
misbinding fixed in the previous commit: fails with a clear per-element
mismatch against the pre-fix binary (verified directly by reverting the
fix, rebuilding, and confirming this test fails with exactly the
row/column mismatch expected from the axis-1/axis-0 swap), and passes
against the fix.

Uses dwi.mif since it's already a shared asset referenced by several
other binary tests (amp2response, amp2sh, dwi2tensor, dwiextract) --
no new test_data additions needed.
The mrconvert call that crops the eddy-derived field map for eddyqc
output was a plain string literal instead of an f-string:

    run.command('mrconvert {field_map_image} field_map_unpad.nii {dwi_post_eddy_crop_option}')

while both of its sibling calls immediately before and after it
(cropping eddy_mask.nii and the post-eddy DWI series, same pattern)
correctly use f-strings. {field_map_image} and
{dwi_post_eddy_crop_option} were therefore passed to mrconvert as
literal, uninterpolated text rather than the actual image path and
crop option, so mrconvert received 3 positional arguments instead of
2 and failed immediately:

    mrconvert: [ERROR] Expected exactly 2 arguments (3 supplied)
    mrconvert: [ERROR] Usage: mrconvert input output
    mrconvert: [ERROR] Yours: mrconvert {field_map_image} field_map_unpad.nii {dwi_post_eddy_crop_option}

Only reached when topup produces a field_map image and -eddyqc_all is
requested -- an -eddyqc_all + real topup field map run is enough to
hit this on any dataset/vendor, independent of the -rpe_* mode or scan
platform. Confirmed on origin/dev HEAD (fetched live, unfixed there
too) -- not something introduced on this branch or specific to this
checkout.

Fix: add the missing f prefix so both placeholders interpolate
correctly, matching the two neighbouring calls.

Reported via a real -eddyqc_all + -align_seepi -rpe_header run
(dwifslpreproc on Siemens multi-shell DWI data) that failed after
~60 minutes of eddy correction with exactly this error.
@Rad-dude Rad-dude changed the title mrconvert: fix silent DirectIO axis misbinding in copy_permute() mrconvert: fix silent DirectIO axis misbinding in copy_permute(); dwifslpreproc: fix missing f-string in field-map crop Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants