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

Rewrite -add/-sub/-mul/-div to match expected behavior for 3D/4D images #3808

Merged
merged 28 commits into from Jun 27, 2022

Conversation

joshuacwnewton
Copy link
Member

@joshuacwnewton joshuacwnewton commented Jun 14, 2022

Checklist

GitHub

PR contents

Description

This PR adds new tests to check the dimensions of the output for -add/-sub/-mul/-div. On master, this test fails for:

  • -add/-mul with 4D images
  • -sub/-div with lists of images

So, this PR also adds some rewriting to simplify the underlying functions (get_data_or_scalar(), concatenate_along_4th_dimension()) to make the tests pass.

Finally, this PR improves the documentation of the functions to make the behaviors clearer (#3806 (comment), #3806 (comment)).

Linked issues

Fixes #3806.

There's no need to call either `get_data_or_scalar` or `concatenate_along_4th`,
since all we're really doing in sct_create_mask is setting the data array to zero.

The reason I'm doing this is that I'm going to be making changes to the sct_maths
functions in future commits. So, it saves some headache to decouple sct_maths
from sct_create_mask.
This will allow us to more easily refactor the `concatenate` function in the next commit.
This results in the following behavior change:

- Before: 3D volumes within a 4D image are summed ("[sum(4D) ==> 3D] + [sum(4D) ==> 3D] = 3D")
- After: 4D images are summed directly ("4D + 4D = 4D")
This results in `-sub` and `-div` now being able to accept lists of images.
This allows us to add/mul 3D/4D volumes together without throwing an error.
@joshuacwnewton joshuacwnewton added bug category: fixes an error in the code sct_maths context: SCT API: math.py context: labels Jun 14, 2022
@joshuacwnewton joshuacwnewton added this to the 5.7 milestone Jun 14, 2022
@codecov
Copy link

codecov bot commented Jun 14, 2022

Codecov Report

Merging #3808 (83b4ff4) into master (e275419) will increase coverage by 0.07%.
The diff coverage is 90.00%.

Flag Coverage Δ
api-tests 21.97% <2.50%> (-0.01%) ⬇️
cli-tests 58.43% <90.00%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
spinalcordtoolbox/math.py 56.71% <50.00%> (-1.62%) ⬇️
spinalcordtoolbox/scripts/sct_create_mask.py 88.95% <100.00%> (-0.26%) ⬇️
spinalcordtoolbox/scripts/sct_maths.py 51.69% <100.00%> (+7.78%) ⬆️

Copy link
Member

@mguaypaq mguaypaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the new feature and the incidental cleanup! I don't want to increase the scope of this PR too much, but I do think that get_data_or_scalar and get_data should be made less convoluted. Also, the zero-argument version of -add should be copied over to -mul too (but not -sub or -div).

Sorry for the many comments; they reflect the state of the existing code more than your changes (which are already definite improvements).

Feel free to ping me on Slack if you'd like a more interactive review and/or brainstorming for solutions.

spinalcordtoolbox/scripts/sct_create_mask.py Show resolved Hide resolved
testing/cli/test_cli_sct_maths.py Outdated Show resolved Hide resolved
testing/cli/test_cli_sct_maths.py Outdated Show resolved Hide resolved
testing/cli/test_cli_sct_maths.py Outdated Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Outdated Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Outdated Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Outdated Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Outdated Show resolved Hide resolved
spinalcordtoolbox/scripts/sct_maths.py Outdated Show resolved Hide resolved
If the files don't exist, then we can just rely on the error thrown by
`Image()`. As written, the try/except block didn't work as expected.
Dimension-checking is only relevant for "Case 2", since Case 1 guarantees matching input shapes.

Also, we don't need to check all the images against each other; we only need to check against the input image.
This disallows combining 3D/4D images, so we remove those
now-failing test cases.
This makes it clearer that `get_data_or_scalar` is specifically
designed to parse specific arguments in sct_maths
Allows the 3D volumes within a 4D image to be multiplied together.
…tions

By removing this message, it makes the help description a little less verbose/confusing. Since, "dimensions must match" doesn't make any sense for scalar arguments or bare -add/-mul argument

So, I think it might be better to assume that the user will pass images with matching dimensions. And, if they don't, they will find out the correct usage via the error message in the parsing action.
@joshuacwnewton
Copy link
Member Author

I don't want to increase the scope of this PR too much, but I do think that get_data_or_scalar and get_data should be made less convoluted.

I appreciate this nudge! I agree that the functions are unnecessarily complex, and much could be done to make them simpler.

I've tried rewriting the function into a parsing action that incorporates your suggestions. Let me know what you think!

Copy link
Member

@mguaypaq mguaypaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This custom parser is much cleaner, thanks!

Out of curiosity, do you know how it reacts to missing or unreadable files, either in the -i argument or in, say, an -add argument? I imagine it exits, but I wonder how nice the error message and/or traceback is.

testing/cli/test_cli_sct_maths.py Outdated Show resolved Hide resolved
testing/cli/test_cli_sct_maths.py Outdated Show resolved Hide resolved
joshuacwnewton and others added 2 commits June 22, 2022 11:59
Co-authored-by: Mathieu Guay-Paquet <mathieu.guaypaquet@gmail.com>
Co-authored-by: Mathieu Guay-Paquet <mathieu.guaypaquet@gmail.com>
@joshuacwnewton
Copy link
Member Author

Out of curiosity, do you know how it reacts to missing or unreadable files, either in the -i argument or in, say, an -add argument? I imagine it exits, but I wonder how nice the error message and/or traceback is.

Ahh, good thinking! Since we're using nibabel to load images, here are its tracebacks:

(venv_sct) joshua@tadpole:~/repos/spinalcordtoolbox$ sct_maths -i non-existent-file.nii.gz -add

--
Spinal Cord Toolbox (git-jn/3806-fix_sct_maths_add_4d_data-05615b8a148e995b583db1c52bf9332e2fd8ecd4)

sct_maths -i non-existent-file.nii.gz -add
--

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 42, in load
    stat_result = os.stat(filename)
FileNotFoundError: [Errno 2] No such file or directory: '/home/joshua/repos/spinalcordtoolbox/non-existent-file.nii.gz'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 524, in <module>
    main(sys.argv[1:])
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 307, in main
    arguments = parser.parse_args(argv)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1755, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1787, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1993, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1933, in consume_optional
    take_action(action, args, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1861, in take_action
    action(self, namespace, argument_values, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 46, in __call__
    data_in = Image(namespace.i).data
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 285, in __init__
    self.loadFromPath(param, verbose)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 406, in loadFromPath
    im_file = nib.load(self.absolutepath, mmap=(not sys.platform.startswith('win32')))
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 44, in load
    raise FileNotFoundError(f"No such file or no access: '{filename}'")
FileNotFoundError: No such file or no access: '/home/joshua/repos/spinalcordtoolbox/non-existent-file.nii.gz'
(venv_sct) joshua@tadpole:~/repos/spinalcordtoolbox$ sct_maths -i non-existent-file.nii.gz -add

--
Spinal Cord Toolbox (git-jn/3806-fix_sct_maths_add_4d_data-05615b8a148e995b583db1c52bf9332e2fd8ecd4)

sct_maths -i non-existent-file.nii.gz -add
--

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 42, in load
    stat_result = os.stat(filename)
FileNotFoundError: [Errno 2] No such file or directory: '/home/joshua/repos/spinalcordtoolbox/non-existent-file.nii.gz'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 524, in <module>
    main(sys.argv[1:])
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 307, in main
    arguments = parser.parse_args(argv)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1755, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1787, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1993, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1933, in consume_optional
    take_action(action, args, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1861, in take_action
    action(self, namespace, argument_values, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 46, in __call__
    data_in = Image(namespace.i).data
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 285, in __init__
    self.loadFromPath(param, verbose)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 406, in loadFromPath
    im_file = nib.load(self.absolutepath, mmap=(not sys.platform.startswith('win32')))
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 44, in load
    raise FileNotFoundError(f"No such file or no access: '{filename}'")
FileNotFoundError: No such file or no access: '/home/joshua/repos/spinalcordtoolbox/non-existent-file.nii.gz'
(venv_sct) joshua@tadpole:~/repos/spinalcordtoolbox$ sct_maths -i batch_processing.sh -add

--
Spinal Cord Toolbox (git-jn/3806-fix_sct_maths_add_4d_data-05615b8a148e995b583db1c52bf9332e2fd8ecd4)

sct_maths -i batch_processing.sh -add
--

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 524, in <module>
    main(sys.argv[1:])
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 307, in main
    arguments = parser.parse_args(argv)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1755, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1787, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1993, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1933, in consume_optional
    take_action(action, args, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1861, in take_action
    action(self, namespace, argument_values, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 46, in __call__
    data_in = Image(namespace.i).data
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 285, in __init__
    self.loadFromPath(param, verbose)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 406, in loadFromPath
    im_file = nib.load(self.absolutepath, mmap=(not sys.platform.startswith('win32')))
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 55, in load
    raise ImageFileError(f'Cannot work out file type of "{filename}"')
nibabel.filebasedimages.ImageFileError: Cannot work out file type of "/home/joshua/repos/spinalcordtoolbox/batch_processing.sh"
(venv_sct) joshua@tadpole:~/repos/spinalcordtoolbox$ sct_maths -i data/sct_example_data/t1/t1.nii.gz -add test.nii.gz

--
Spinal Cord Toolbox (git-jn/3806-fix_sct_maths_add_4d_data-05615b8a148e995b583db1c52bf9332e2fd8ecd4)

sct_maths -i data/sct_example_data/t1/t1.nii.gz -add test.nii.gz
--

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 42, in load
    stat_result = os.stat(filename)
FileNotFoundError: [Errno 2] No such file or directory: '/home/joshua/repos/spinalcordtoolbox/test.nii.gz'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 524, in <module>
    main(sys.argv[1:])
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 307, in main
    arguments = parser.parse_args(argv)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1755, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1787, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1993, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1933, in consume_optional
    take_action(action, args, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/argparse.py", line 1861, in take_action
    action(self, namespace, argument_values, option_string)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/scripts/sct_maths.py", line 55, in __call__
    data = Image(val).data
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 285, in __init__
    self.loadFromPath(param, verbose)
  File "/home/joshua/repos/spinalcordtoolbox/spinalcordtoolbox/image.py", line 406, in loadFromPath
    im_file = nib.load(self.absolutepath, mmap=(not sys.platform.startswith('win32')))
  File "/home/joshua/repos/spinalcordtoolbox/python/envs/venv_sct/lib/python3.7/site-packages/nibabel/loadsave.py", line 44, in load
    raise FileNotFoundError(f"No such file or no access: '{filename}'")
FileNotFoundError: No such file or no access: '/home/joshua/repos/spinalcordtoolbox/test.nii.gz'

On the one hand, it's not the prettiest? But on the other hand, it's more or less identical to every other traceback we get when loading image files.

Before, we were parametrizing add/mul differently than sub/div, so it made sense to have different tests. But, now we use the same set of input image dimensions, so we no longer need to separate the tests.
NB: The parser should catch the error before the Python API ever does, so the
error handling code in the API doesn't ever get tested. But, maybe this is fine?
@joshuacwnewton joshuacwnewton force-pushed the jn/3806-fix_sct_maths_add_4d_data branch from 702a278 to b5ee93d Compare June 27, 2022 14:54
@joshuacwnewton
Copy link
Member Author

I did some additional clean-up, since I realized there were some stray details that needed to be fixed after we removed support for mixed 3D/4D images in -add/-mul.

One last tiny review for the new changes and this should be good to go?

Copy link
Member

@mguaypaq mguaypaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on the last few changes.

@joshuacwnewton joshuacwnewton merged commit ae312ed into master Jun 27, 2022
@joshuacwnewton joshuacwnewton deleted the jn/3806-fix_sct_maths_add_4d_data branch June 27, 2022 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug category: fixes an error in the code SCT API: math.py context: sct_maths context:
Projects
None yet
Development

Successfully merging this pull request may close these issues.

For multi-echo data, -add does not match the expected results compared to fsl_maths
3 participants