-
Notifications
You must be signed in to change notification settings - Fork 16
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: Extend the nonlinear transforms API #166
Conversation
Codecov Report
@@ Coverage Diff @@
## master #166 +/- ##
==========================================
+ Coverage 98.60% 98.62% +0.01%
==========================================
Files 13 13
Lines 1217 1232 +15
Branches 184 187 +3
==========================================
+ Hits 1200 1215 +15
Misses 10 10
Partials 7 7
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
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.
Conceptually, a DisplacementsFieldTransform
is not a DeformationFieldTransform
. A DeformationFieldTransform
has a coherent __matmul__
implementation, while that same implementation does not make sense for DisplacementsFieldTransform
, so this inheritance is bound to cause confusion.
I think we're mixing up composition and application and calling both of them "transforms". Your DeformationFieldTransform._field
is equivalent to Affine._matrix
, while your DisplacementsFieldTransform._field
is equivalent to having already applied Affine.map()
to the voxel indices, turning it into a lookup table.
Maybe we need to think about a typing structure like:
class Composable:
def __matmul__(self, other): ...
class Applicable:
def map(self, points): ...
Deformation fields and reference-less affines are composable, while displacement fields and affines with references are applicable.
I'm confused, why a deformation field is not applicable? and conversely, why a displacements field is not composable? ITK generates displacements fields and you can ask ANTS to generate "composite" transforms, which in effect are a new displacements field. Similarly, if you use SPM, nonlinear transforms most often will be given in deformation convention (if I correctly understood J. Ashburner in our meeting in Boston back in 2018) and again, you can apply/compose as you wish. Perhaps, the missing piece here is a convenience function to convert from deformation to displacements and define |
One more reason to use the proposed design is #155. With the current implementation, that one becomes trivial for both types of fields. |
My understanding here is that I have not tested the following, but here's my rough expectation from reading this code: >>> dfm = DeformationFieldTransform()
>>> disp = DisplacementsFieldTransform() Application >>> x, y, z = some_coord
>>> np.array_equal(dfm.map([x, y, z]), [x, y, z]))
True
>>> np.array_equal(disp.map([x, y, z]), [x, y, z]))
True Composition >>> dfm @ dfm == dfm
True
>>> disp @ disp == disp
False |
Maybe a better way to put this is that a |
Okay, I think I now understand what you mean. If you assume that: >>> dfm = DeformationFieldTransform()
>>> disp = DisplacementsFieldTransform() will create identity transforms for both structures then:
Then, mapping is equivalent for both. Regarding composition, in both cases: >>> dfm @ dfm == dfm
True
>>> disp @ disp == disp
True iff they are identity. For both of them, if they are not identity, then both satisfy: >>> dfm @ dfm == dfm
False
>>> disp @ disp == disp
False which will become more apparent with the implementation of #155. In the case of Perhaps you would feel more comfortable with a single representation structure: class DenseFieldTransform(TransformBase):
def __init__(self, deformation=None, displacements=None, reference=None):
... where:
Does this make sense? |
Okay, I sketched out an alternative API. Let me know what you think. |
3039d60
to
9507cdd
Compare
Co-authored-by: Chris Markiewicz <[email protected]>
9507cdd
to
f8650e4
Compare
Okay, reading the new API, I think some things that were unclear are clicking. Now I'm just left to wonder: Why do we want to have the class DenseFieldTransform(TransformBase):
def __init__(self, deltas, reference=None):
self._deltas = deltas
self._reference = reference
def __matmul__(self, other):
deltas = b.map(
self._deltas.reshape((-1, self._deltas.shape[-1]))
).reshape(self._deltas.shape)
return DenseFieldTransform(deltas, reference=self.reference)
def map(self, coords, inverse=False):
indices = calculate_indices(coords)
return coords + self._deltas[indices]
@classmethod
def from_deformation_field(cls, deformations, reference=None):
if reference is None:
reference = ...
deltas = deformations - reference.ndcoords.T.reshape(deformations.shape)
return cls(deltas, reference=reference) Finally, are we sure that we want to use Apologies if I'm jumping all over the place. Trying to understand this somewhat quickly and may be flailing a bit... |
I think this is just for reproducibility purposes -- to be able to write it out to disk (possibly in a different format such as X5) without alteration of the array contents.
As commented in the code, AFAIK the deformations are numerically more stable and that's why I made them the default.
I opened an issue to remind me of this good point.
On the contrary, this is very helpful. Thanks for your time. I'll use your |
15bdb61
to
b97e55c
Compare
This PR lays the ground for future work on #56, and #89, by defining the matrix multiplication operator on field-based transforms.