[ENH]: Preprocessing step to warp any image to any standard space #301
No reviewers
Labels
No labels
CRITICAL
Stale
WIP
bug
concept
coordinate
dataset
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
maintenance
maps
marker
mask
on hold
parcellation
preprocess
question
ready
storage
template-space
triage
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
juaml/junifer!301
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/space-warper"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Are you requiring a new dataset or marker?
Which feature do you want to include?
So far, the
BOLDWarperallows to warp the BOLD image.However, given that we have multiple MNI spaces, it would be a good idea to have a
SpaceWarperpreprocessor (or similar) that exposes theonparameter to the user.How do you imagine this integrated in junifer?
A
SpaceWarperor similar class.Do you have a sample code that implements this outside of junifer?
No response
Anything else to say?
No response
While looking at the
BOLDWarper, I realised that due to not having enough context back then, we had_ApplyWarperand_AntsApplyTransformsWarperwhich were then used insideBOLDWarper. This approach led us to having both entries forBOLDWarper._EXT_DEPENDENCIES(FSL and ANTs) as optional which then should check inside the two tool-specific helper classes for the respective_EXT_DEPENDENCIES. Looking at it now while implementingSpaceWarper, it's incorrect imo and adds complexity which was needed back then (as we first had support for native space and then with templateflow had access to other spaces) but can be solved differently now. I have two approaches now both with own pros and cons:SpaceWarperclass likeBOLDWarperbut make both FSL and ANTs mandatory in_EXT_DEPENDENCIESFSLSpaceWarperandANTsSpaceWarperwhich keep it simpleNow, both approaches would require
ANTsto be mandatory as warping to template spaces (not native space) would require ANTs and we can't foresee that.I don't have a particular preference here but would lean towards 2. only because of easier maintenance. Would like to hear opinions on this @juaml/junifer-core .
Indeed extra dependencies are a big issue in neuroimaging. If we start doing this (requiring dependencies that are not used), we will end with a huge software installation system for a basic analysis.
On the one hand, I don't want the user to have to select the internal implementation if we can do it automatically. We discussed something similar on #161 with the
backend. For me, a backend or choice of method is exposed when there's a trade-off between computing time/resources, dependencies, without affecting results.E.g. Think about someone sharing a YAML in which the
SpaceWarperis used. If one has some C-level optimisations and the other not, then maybe for the user receiving the YAML, the computation will take some more time, but the result will be the same (give or take some numerical differences). Of course the receiving user will have to change the "backend" parameter, which will change the meta, which will change the feature MD5.On the other hand, like we have with ReHo, the results with different backends are different (python vs afni). Now this is a big thing, as changing the backend has quite some impact. In this case, this should not be a "backend", but a different version of the marker.
I think here we are faced with a similar issue.
In short, we have to define:
My take:
In the case of ReHo, we have a paramater
use_afni(not backend). We also have "afni" as optional dependency. This solves the mandatory dependency issue, but it will raise an exception in fit time if the dependency is not met. The solution is to have "conditional dependencies".We have some code here than can be changed to support this:
github.com/juaml/junifer@6ff3fd3b44/junifer/pipeline/pipeline_step_mixin.py (L136-L139)Basically, exploit the
use_Xattributes for conditional dependencies.We can have internals/private
FSLSpaceWarperandANTsSpaceWarper, while exposingSpaceWarper. The SpaceWarper should haveuse_fslanduse_afniattributes (one and only one must be true). This should trigger the correct conditional dependencies.In this way, we get both pros and no cons.
The logic of the "conditional" dependency should be: check the dependency X only if the
use_Xattribute isTrue. We can potentially allow for"auto"to autodetect. However, I'm not sure how this will behave at the level of the feature's meta.I agree and we need to update it when we get the chance.
I agree with your definition of an implementation which gives similar results give or take numerical differences.
Anything which is a component of the pipeline except DataGrabber and Storage, although a "step" but not a "pipeline step" imo.
Which can take multiple values according to a set of constraints.
Extending your line of your thought,
use_afniis a parameter which should be allowed if we have comparable implementations not different implementations. The current ReHo-style does not do justice imo. So, if you have something likeJuniferReHoandAFNIReHo, it's explicit and carry forwards the intent of implementation and further usage by users. Checking the metadata, one would know for sure."Conditional dependencies" like you put it has come across my mind and I find it fairly incoherent and adds complexity. A small example to show a small problem:
Let's say we get ReHo from SPM now. We now have to add a parameter
use_spmto ReHo constructor which not only increases implementation complexity but also increases usage complexity. A change in the parameter for an user would mean the feature MD5 being different.Now, we go with explicit
SPMReHowhich is a marker in its own right and doesn't confuse anyone user what it uses and does and also makes it easier for us to implement and maintain.From looking at different implementations of a concept by different softwares, I'm convinced they all have some or the other differences and results would not match 1:1 even without numerical differences.
You clearly see here how it gets complex to implement and maintain and for anyone to understand later on.
Now, having said everything, I'm not against "conditional dependencies"; I just don't think they are elegant enough with our current arguments and thoughts. As we follow the philosophy of "fail first" in the pipeline, there should be no surprises or automatic detection of anything which can lead to ambiguous results (take your example of ReHo above as a case). Just to make it concrete: we raise error if someone uses
BOLDWarperand has the same space for both source and target; the strictness and opinionated view pays off by not letting the user do anything stupid. So, I don't see a reason why we should stop that for the case of markers or preprocessors. They should be as explicit and as strict to not let users do anything stupid and make our lives harder due to the implementation complexity.Well, conceptually, the marker is ReHo, not AFNIReHo. So I do not agree with having different "markers". Now
use_afniis not the backend, but the method used to compute the markerNow look at the issue without considering as backend but as method or kind. An example would be nilearn's FC (https://nilearn.github.io/stable/modules/generated/nilearn.connectome.ConnectivityMeasure.html). You can compute a ConnectivityMeasure by looking at the "covariance", "correlation", etc. Another example is the "method" parameter when you do corrections for multiple comparisons using statsmodels: https://www.statsmodels.org/dev/generated/statsmodels.stats.multitest.multipletests.html
In our case, the marker is ReHo, or the preprocessing is a
SpaceWarper. The difference is that we have a parameter to specify which "method" to use. Either the AFNI way, the FSL way, the SPM way, etc. etc.We can do SPMReho or AFNIReHo as classes, but I insist that the marker is ReHo. Like you said, the concept is the marker, no the implementation. So we should not have implementations as markers.
That was one possible way (
use_Xattribute). We can change it for "method" in the constructor and keep the_use_Xattribute private. There are many ways in which this can be done in order to provide a simple interface for users while keeping everything modular from our side.I agree with the "fail first". Conditional dependencies will fail on the "check" phase, not on the running phase. If the YAML has ReHo with
method="afni", then on the check, we will check for the afni dependencies for ReHo. If the YAML has theSpaceWarperwithmethod="FSL", then on the check, we will check for the FSL commands. The code snippet I sent with thecheck_ext_dependenciescall is on thevalidatefunction.Fair enough.
Gathering from your replies but quoting only this, I definitely support a single parameter; let's finalise it as
methodorusingwhich goes in the constructor of every marker / preprocessor having this behaviour. I would want this parameter to be positional and not optional with no default value and no value as"auto". This would keep the metadata consistent. Now, after that the check happens as you already described above.Now extending that and describing some concrete situations:
ReHo or (f)ALFF: will have optional external dependency on AFNI; acceptable values are
"junifer", "afni"; when you set"afni", the dependency is mandatory as it's nowBOLDWarper or SpaceWarper: will have optional external dependency on FSL and ANTs; acceptable values are
"fsl", "ants"; this will require further checks as one can do native warp with only FSL but will need ANTs when warping it to other template spaces or if the transformation matrix is in ANTs formatOne thought that I have is we could also add a key to the
_EXT_DEPENDENCIESentries calleddepends_onwhich lists the classes that the primary class (ReHo for example) depends on (AFNIReHo) for example. So, thevalidatecrawls further into the hierarchy and check the tool-specific class'EXT_DEPENDENCIES. Of course, this is a very different way of solving this but would can be considered.On another note, we can deprecate
BOLDWarperin due time asSpaceWarperwill be capable enough to do what the former does and much more.100% agree.
Perfect.
the
validatefunction can be used to check as much as we can before running. So this is done with the limited information we have at that time. Unfortunately we can't know the format of the transformation matrix, so this will be a "runtime error".This is definitely an implementation decision. Don't take the current code as something correct. The main issue of the "depends on" is that we need to decide which is the correct class based on the "method" parameter.
We can think of something in the lines of... (Brainstorming here):
ReHoBase's_EXT_DEPENDENCIES:By setting "conditional", we will check the
methodparameter matches the "conditional" value. And instead of asking there for the afni command's, we rely on AFNIReHo's class.and then
AFNIReHos:Definitely.
That is correct, can't do much about that.
Agree with
AFNIReHobut I differ a bit forReHoBase:Shall we then take this idea and use it for #161 as well? Like have
Smoothingfor user butNilearnSmoothing,AFNISmoothingandFSLSmoothingfor implementations.So to clarify,
"using"is for conditional dependencies and"name"for concrete (mandatory)? We don't have "optional" dependencies anymore.As I said, it was just a brainstorming. Feel free to propose any solution that you might find a better fit.
Indeed, this should be a generic solution for any step with more than one implementation that has external dependencies.
That's kind of what I have in my mind as of now, at least we got the base set. Will update after I get the code ready.
perfect!
I like the organization of the code. I think it paid off to do the conditional dependency PR
The
SpaceWarperlogin needs some work.I did not check the tests, will check with the next review.
@ -0,0 +1,167 @@"""Provide class for space warping via ANTs antsApplyTransforms."""It also uses nibabel
@ -0,0 +1,109 @@"""Provide class for space warping via FSL FLIRT."""It also uses nibabel
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""I think there are quite some valid inputs more than just T1W and BOLD. Basically you can warp any image.
This conflicts with the
usingattribute. I think that it should logically check that the using matches the warp file format.This should be done by the ANTSWarper
Nibabel is now a dependency for this class too.
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""Which others would you suggest keeping here?
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""The
ANTsWarperworks and behaves in a different way as it also usesResampleImage, I wouldn't want to keep this there. We can create a new helper class with an entry in_EXT_DEPENDENCIES.@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""ANTSTemplateWarper?@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""For the moment, all of this but
BOLD_confounds: https://juaml.github.io/junifer/main/understanding/data.html#data-types@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""Sounds good to me.
Codecov Report
Attention: Patch coverage is
62.26415%with40 linesin your changes are missing coverage. Please review.Additional details and impacted files
87.98% <62.26%> (-0.57%)Flags with carried forward coverage won't be shown. Click here to find out more.
100.00% <100.00%> (ø)39.65% <ø> (ø)100.00% <100.00%> (ø)66.66% <66.66%> (ø)40.90% <40.90%> (ø)67.39% <67.39%> (ø)... and 1 file with indirect coverage changes
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""From our conversation above:
I wouldn't want to make exception for any component. I agreed with you for
BOLDWarperauto-detecting considering we had small scope. For this I disagree and my rationale is quoted.IMO it wouldn't hurt the user to add one extra line when in return it would give us complete knowledge of the component. I don't want to trade clarity for saving one line in the YAML.
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""Unfortunately, there is no other current approach for warping templates in junifer so having a forced check for
usingdoesn't really make sense. Of course we can make it but it doesn't give anything extra in return. I can make the baseif...elsestronger if that eases this out.@fraimondo Shall we merge this?
Can we also add some tests for the errors?
@ -0,0 +1,203 @@"""Provide class for warping data to other template spaces."""@ -0,0 +152,4 @@"""logger.info(f"Warping to {self.reference} space using SpaceWarper")# Transform to native spaceif self.using in ["fsl", "ants"] and self.reference == "T1w":I'm missing one condition here (which should raise an error)
using == "fsl"andreference != T1w"@ -0,0 +182,4 @@"should remove the SpaceWarper from the preprocess ""step."),klass=RuntimeError,If this is an error, then the message should not be "skipped...", "can remove...".
Other option is to make it a warning.
Following the strict pipeline policy, we should keep it an error and change the message indicating that the user MUST remove that preprocessing step.
LGTM!