Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

data.dmri has mismatches between reading/writting/representation #79

Open
jhlegarreta opened this issue Jan 26, 2025 · 0 comments · May be fixed by #82
Open

data.dmri has mismatches between reading/writting/representation #79

jhlegarreta opened this issue Jan 26, 2025 · 0 comments · May be fixed by #82
Labels
bug Something isn't working

Comments

@jhlegarreta
Copy link
Contributor

jhlegarreta commented Jan 26, 2025

What happened?

The DWI reading/saving/representation logic has some bugs.

  • The test_data_mri::test_load is reading the bvals/bvecs files
    dwi_from_nifti2 = load(
    dwi_nifti_path,
    bvec_file=bvecs_path,
    bval_file=bvals_path,

    that are being written by the test function
    np.savetxt(str(bvecs_path), grad_table[:3])
    np.savetxt(str(bvals_path), grad_table[-1])

    based on the grad_table variable, which is effectively inserting a b0 value to the bvals/bvecs file (having shapes (1, 103), (3, 103)), instead of writing the bval/bvec file pair that is written by the DWI.to_nifti method, which is what we should be testing.
  • When insert_b0 is True, as it is the case in the test, the DWI.to_nifti method:
    if not insert_b0:
    # Parent's to_nifti to handle the primary NIfTI export.
    super().to_nifti(filename)
    else:
    data = np.concatenate((self.bzero[..., np.newaxis], self.dataobj), axis=-1)
    nii = nb.Nifti1Image(data, self.affine, self.datahdr)
    if self.datahdr is None:
    nii.header.set_xyzt_units("mm")
    nii.to_filename(filename)
    # Convert filename to a Path object.
    out_root = Path(filename).absolute()
    # Remove .gz if present, then remove .nii if present.
    # This yields the base stem for writing .bvec / .bval.
    if out_root.suffix == ".gz":
    out_root = out_root.with_suffix("") # remove '.gz'
    if out_root.suffix == ".nii":
    out_root = out_root.with_suffix("") # remove '.nii'
    # Construct sidecar file paths.
    bvecs_file = out_root.with_suffix(".bvec")
    bvals_file = out_root.with_suffix(".bval")
    # Save bvecs and bvals to text files
    # Each row of bvecs is one direction (3 rows, N columns).
    np.savetxt(bvecs_file, self.gradients[:3, ...].T, fmt="%.6f")
    np.savetxt(bvals_file, self.gradients[:3, ...], fmt="%.6f")

    does not insert any null gradient to the bval/bvecs files (having shapes (1, 102), (3, 102)). Thus, if we had read those files in the test, the test would be failing due to a shape mismatch error in
    dataobj=fulldata[..., gradmsk],

as the data contained by the NIfTI files will have 103 DWI volumes (102 corresponding to non-b0 data, and the one inserted by virtue of insert_b0 being True), and the gradients read will have 102 values. Note that the represented DWI data has non-null (not None), built-in bzero data as well (besides the b0 data that the writer is asked to write following the insert_b0 flag).

However, there is a deeper bug here:

  • If we change the DWI.to_nifti method to add a null gradient to the bval/bvecs files:
       bvecs = self.bvecs
       bvals = self.bvals

        if not insert_b0:
            # Parent's to_nifti to handle the primary NIfTI export.
            super().to_nifti(filename)
        else:
            data = np.concatenate((self.bzero[..., np.newaxis], self.dataobj), axis=-1)
            bvecs = np.concatenate((np.zeros(3)[:, np.newaxis], bvecs), axis=-1)
            bvals = np.concatenate((np.zeros(1), bvals))
            nii = nb.Nifti1Image(data, self.affine, self.datahdr)
            if self.datahdr is None:
                nii.header.set_xyzt_units("mm")
            nii.to_filename(filename)

        # Convert filename to a Path object.
        out_root = Path(filename).absolute()

        # Get the base stem for writing .bvec / .bval.
        out_root = out_root.parent / out_root.name.replace("".join(out_root.suffixes), "")

        # Construct sidecar file paths.
        bvecs_file = out_root.with_suffix(".bvec")
        bvals_file = out_root.with_suffix(".bval")

        # Save bvecs and bvals to text files
        # Each row of bvecs is one direction (3 rows, N columns).
        np.savetxt(bvecs_file, bvecs, fmt="%.6f")
        np.savetxt(bvals_file, bvals[np.newaxis, :], fmt="%.6f")

and if insert_b0 is True, the test will pass. However, it will not pass if insert_b0 is False. None of the cases on lines

if b0_file:
b0img = nb.load(str(b0_file))
b0vol = np.asanyarray(b0img.dataobj)
# We'll assume your DWI class has a bzero: np.ndarray | None attribute
dwi_obj.bzero = b0vol
# Otherwise, if any volumes remain outside gradmsk, compute a median B0:
elif np.any(~gradmsk):
# The b=0 volumes are those that did NOT pass b0_thres
b0_volumes = fulldata[..., ~gradmsk]
# A simple approach is to take the median across that last dimension
# Note that axis=3 is valid only if your data is 4D (x, y, z, volumes).
dwi_obj.bzero = np.median(b0_volumes, axis=3)
are True and thus the read dwi_obj will have a None value in its bzero property, whereas the test object dwi_h5 does have a bzero volume, thus assert np.allclose(dwi_h5.bzero, dwi_from_nifti.bzero) will fail.

So the logic behind this needs to be reworked. Some thoughts:

  • The above shows a clear mismatch of what the object holds, and what is told to write: if we tell to insert a bzero, the written file becomes out of sync with its original representation.
  • It is confusing to have a bzero value (as the original instance read form the h5 file has), but not to have a 0-valued bval/bvec in the gradients.
  • Commonly, we expect the non-weighted values to be present in the bval/bvec data.
  • It is equally confusing the fact that the load method accepts an optional b0_file, but no b0_file is being written. Not writing an additional b0 file seems natural, as one expects the b0 data to be saved together with the non-b0 data, but then allowing to read one does not seem natural.
  • It is confusing to ask to insert a b0 when writing to some data that already has b0 data: how should this be interpreted and written?
  • What happens when a DWI volume has multiple b0 volumes?
  • test_load should be parameterized with insert_b0 to [False, True] to test all of the above properly.

What command did you use?

pytest test_data_mri.py

What version of the software are you running?

HEAD (commit 7322e93) with the necessary fixes in PR #72

How are you running this software?

Local installation ("bare-metal")

Is your data BIDS valid?

Yes

Are you reusing any previously computed results?

No

Please copy and paste any relevant log output.

For the issue (i.e. not writing the bval/bvec files that should be read, and not writing the null gradient to the bval/bvec files):

>       dwi_from_nifti1 = load(
            dwi_nifti_path,
            bvec_file=bvecs_path,
            bval_file=bvals_path,
        )
(...)

        # The shape checking is somewhat flexible: (4, N) or (N, 4)
        dwi_obj = DWI(
>           dataobj=fulldata[..., gradmsk],
            affine=affine,
            # We'll assign the filtered gradients below.
        )
E       IndexError: boolean index did not match indexed array along dimension 3; dimension is 103 but corresponding boolean dimension is 102

For the second issue (having made such that the above passes add a null gradient to the written bval and bvec files but using False for insert_b0):

>       assert np.allclose(dwi_h5.bzero, dwi_from_nifti1.bzero)
(...)
E       TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

Additional information / screenshots

X-ref #72 (comment).

@jhlegarreta jhlegarreta added the bug Something isn't working label Jan 26, 2025
jhlegarreta added a commit to jhlegarreta/nifreeze that referenced this issue Jan 31, 2025
@jhlegarreta jhlegarreta linked a pull request Jan 31, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant