-
Notifications
You must be signed in to change notification settings - Fork 290
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
[ENH] Parallelize conformation #666
[ENH] Parallelize conformation #666
Conversation
Hi, thanks for this. I think if we're going to do this at this point, we should probably simplify the |
Hi @effigies ! Thank you for the feedback. I believe it is possible to abstract the nb.as_closest_canonical as a Step, and put it before PruneExcessiveZoom, so the wf become:
and the |
I think the As to the name, maybe "Reorient", "OrientRAS"? I'm not too picky here. Also, WRT "PruneExcessiveZoom", that's only one part of what that step does (and actually the rarest, in my experience). The main purpose it serves is to calculate the template dimensions. "TemplateDimensions", "CalcTemplateSize", "TemplateParameters", or something along those lines would be better. What do you think? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a first pass review of the code. Once we decide how to go on these issues and see what the result looks like, I'll do a more fiddly, detailed review.
fmriprep/interfaces/images.py
Outdated
target_shape = traits.List(traits.Int, | ||
desc='Target shape information') | ||
target_span = traits.List(traits.Float, | ||
desc='Target span information') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't bother with target_span
, since it's just the dot-product of target_zooms
and target_shape
. Doing the multiplication avoids checking for the case where span != zooms * shape
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was in doubt to parametrize it or compute the dot product on the next step, so I will now compute it. 👌
fmriprep/interfaces/images.py
Outdated
t1w_valid_list = OutputMultiPath(exists=True, desc='valid T1w images') | ||
|
||
target_zooms = traits.List(traits.Float, | ||
desc='Target zoom information') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a known length, you can either set a min/max length of 3, or do:
target_zooms = traits.Tuple(traits.Float, traits.Float, traits.Float, desc='...')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect! I didn't know this format. Will use it now :)
fmriprep/interfaces/images.py
Outdated
|
||
class PruneExcessiveZoomOutputSpec(TraitedSpec): | ||
t1w_dropped_list = OutputMultiPath(exists=True, desc='invalid T1w images') | ||
t1w_valid_list = OutputMultiPath(exists=True, desc='valid T1w images') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The t1w_dropped_list
isn't going to be used anywhere, is it? I'm not sure that this is going to be generally useful outside of fmriprep, so we don't need to add pieces we won't use. (If we move it into nipype because the people are clamoring for it, we can revisit.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Initially, it was to be used for a posterior reporting, but I ended adding reporting in this same interface. I'll remove it for now.
fmriprep/interfaces/images.py
Outdated
|
||
self._results['t1w_list'] = out_names | ||
self._results['target_zooms'] = target_zooms.tolist() | ||
self._results['target_shape'] = target_shape.astype(np.int64).tolist() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we need 64 bits to describe the shape of an array, we're in trouble. This should already be an int, so I think you can skip the astype
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe indeed! I cast it because it was using floats, but I will double check it, just in case. If it is not possible to remove this casting, I will switch np.int64
to int
.
fmriprep/interfaces/images.py
Outdated
t1w = File(exists=True, desc='conformed T1w image') | ||
|
||
|
||
class ConformSeries(SimpleInterface): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the series is being mapped over, perhaps "ConformImage" or possibly "Conform"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conform
sounds good!
fmriprep/interfaces/images.py
Outdated
@@ -279,6 +228,93 @@ def _run_interface(self, runtime): | |||
return runtime | |||
|
|||
|
|||
class ConformSeriesInputSpec(BaseInterfaceInputSpec): | |||
t1w = File(exists=True, mandatory=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reasons for using t1w_list
are now obviated, so I'd switch to in_file
, to make this more generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do
fmriprep/interfaces/images.py
Outdated
target_shape = traits.List(traits.Int, | ||
desc='Target shape information') | ||
target_span = traits.List(traits.Float, | ||
desc='Target span information') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'd drop target_span
.
fmriprep/interfaces/images.py
Outdated
|
||
|
||
class ConformSeriesOutputSpec(TraitedSpec): | ||
t1w = File(exists=True, desc='conformed T1w image') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out_file = File(...)
@anibalsolon No rush, but let us know if there's anything you need from us. |
Hi @effigies, sorry for the delay! Thank you for the detailed review. If there is something I can improve, please let me know. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few quick comments.
Also, have you tested this on any of your own datasets with multiple T1w images?
fmriprep/interfaces/images.py
Outdated
while valid.any(): | ||
target_zooms = all_zooms[valid].min(axis=0) | ||
scales = all_zooms[valid] / target_zooms | ||
if np.all(scales < max_scale): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's only used once, don't bother setting max_scale = self.inputs.max_scale
above. Just use self.inputs.max_scale
here.
fmriprep/interfaces/images.py
Outdated
desc='input T1w images') | ||
|
||
max_scale = traits.Float(3.0, usedefault=True, | ||
desc='Maximum scaling factor in images to accept') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these can each be on a single line (max line length: 99), but I haven't checked.
In any event, we don't need a blank space between t1w_list
and max_scale
.
These comments apply to all of the Input/OutputSpecs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Do you know if it is possible to enforce this pattern of attr spacing via flake?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I know of.
fmriprep/interfaces/images.py
Outdated
out_report = File(exists=True, desc='conformation report') | ||
|
||
|
||
CONFORMATION_TEMPLATE = """\t\t<h3 class="elem-title">Anatomical Conformation</h3> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep these above the input/output specs. So the pattern is:
CONSTANTS
class XInputSpec(...):
...
class XOutputSpec(...):
...
class X(...):
...
fmriprep/interfaces/images.py
Outdated
#. Orient to RAS (left-right, posterior-anterior, inferior-superior) | ||
#. Along each dimension, resample to minimum voxel size, maximum number of voxels | ||
class TemplateDimensions(SimpleInterface): | ||
"""Filter a series of T1w images based on the requirements for up-sampling. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposed replacement for this first line, focusing on the main goal of this interface:
Finds template target dimensions for a series of T1w images, filtering low-resolution images, if necessary.
Along each axis, the minimum voxel size (zoom) and the maximum number of voxels (shape) are found
across images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great 👌
fmriprep/interfaces/images.py
Outdated
else: | ||
copyfile(fname, out_name, copy=True, use_hardlink=True) | ||
|
||
self._results['out_file'] = out_name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not bother copying, just point to the original file:
def _run_interface(self, runtime):
# Load image, orient as RAS
fname = self.inputs.in_file
orig_img = nb.load(fname)
reoriented = nb.as_closest_canonical(orig_img)
if reoriented is not orig_img:
out_name = fname_presuffix(fname, suffix='_ras', newpath=runtime.cwd)
reoriented.to_filename(out_name)
else:
out_name = fname
self._results['out_file'] = out_name
fmriprep/interfaces/images.py
Outdated
if reoriented is not orig_img: | ||
reoriented.to_filename(out_name) | ||
else: | ||
copyfile(fname, out_name, copy=True, use_hardlink=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing here, wrt copying.
I tested on Poldrack's MyConnectome dataset, as suggested in issue #613 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Thanks. One last minor comment on the docs, but I think this is good to go. Could you go ahead and merge master? I'll run it on a couple subjects we've had issues with in the past to make sure there are no regressions.
fmriprep/interfaces/images.py
Outdated
Performs two basic functions: | ||
|
||
#. Orient to RAS (left-right, posterior-anterior, inferior-superior) | ||
#. Along each dimension, resample to minimum voxel size, maximum number of voxels |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is no longer calculating the zooms, shape, we can just say:
"resample to target zooms (voxel sizes) and shape (number of voxels)"
Awesome! I cannot merge because I don't have write access. If there are some datasets that I can help testing, just let me know. |
I think @effigies meant merging "master" branch from the upstream (poldracklab/fmriprep) into your branch. |
Ok! Merged with the upstream. Thanks @chrisfilo |
Blocking merge on following tests (will check as I finish):
|
Good to go. Thanks a lot! |
Step
t1_conform
can become a bottleneck since image resampling is executed in series. However, it is possible to run it in parallel, for multiple T1w images.This PR intends to parallelize this process by breaking it into two steps:
This process is executed before and after
t1_merge
, so a sub-workflow was created to wrap both steps.This PR solves issue #613 .