23
23
)
24
24
25
25
26
- class DisplacementsFieldTransform (TransformBase ):
27
- """Represents a dense field of displacements (one vector per voxel) ."""
26
+ class DenseFieldTransform (TransformBase ):
27
+ """Represents dense field ( voxel-wise) transforms ."""
28
28
29
- __slots__ = [ "_field" ]
29
+ __slots__ = ( "_field" , "_deltas" )
30
30
31
- def __init__ (self , field , reference = None ):
31
+ def __init__ (self , field = None , is_deltas = True , reference = None ):
32
32
"""
33
- Create a dense deformation field transform.
33
+ Create a dense field transform.
34
+
35
+ Converting to a field of deformations is straightforward by just adding the corresponding
36
+ displacement to the :math:`(x, y, z)` coordinates of each voxel.
37
+ Numerically, deformation fields are less susceptible to rounding errors
38
+ than displacements fields.
39
+ SPM generally prefers deformations for that reason.
40
+
41
+ Parameters
42
+ ----------
43
+ field : :obj:`numpy.array_like` or :obj:`nibabel.SpatialImage`
44
+ The field of deformations or displacements (*deltas*). If given as a data array,
45
+ then the reference **must** be given.
46
+ is_deltas : :obj:`bool`
47
+ Whether this is a displacements (deltas) field (default), or deformations.
48
+ reference : :obj:`ImageGrid`
49
+ Defines the domain of the transform. If not provided, the domain is defined from
50
+ the ``field`` input.
34
51
35
52
Example
36
53
-------
37
- >>> DisplacementsFieldTransform (test_dir / "someones_displacement_field.nii.gz")
38
- <DisplacementFieldTransform [3D] (57, 67, 56)>
54
+ >>> DenseFieldTransform (test_dir / "someones_displacement_field.nii.gz")
55
+ <DenseFieldTransform [3D] (57, 67, 56)>
39
56
40
57
"""
58
+ if field is None and reference is None :
59
+ raise TransformError ("DenseFieldTransforms require a spatial reference" )
60
+
41
61
super ().__init__ ()
42
62
43
- field = _ensure_image (field )
44
- self ._field = np .squeeze (
45
- np .asanyarray (field .dataobj ) if hasattr (field , "dataobj" ) else field
46
- )
63
+ if field is not None :
64
+ field = _ensure_image (field )
65
+ self ._field = np .squeeze (
66
+ np .asanyarray (field .dataobj ) if hasattr (field , "dataobj" ) else field
67
+ )
68
+ else :
69
+ self ._field = np .zeros ((* reference .shape , reference .ndim ), dtype = "float32" )
70
+ is_deltas = True
47
71
48
72
try :
49
73
self .reference = ImageGrid (
@@ -59,45 +83,61 @@ def __init__(self, field, reference=None):
59
83
ndim = self ._field .ndim - 1
60
84
if self ._field .shape [- 1 ] != ndim :
61
85
raise TransformError (
62
- "The number of components of the displacements (%d) does not "
86
+ "The number of components of the field (%d) does not match "
63
87
"the number of dimensions (%d)" % (self ._field .shape [- 1 ], ndim )
64
88
)
65
89
90
+ if is_deltas :
91
+ self ._deltas = self ._field
92
+ # Convert from displacements (deltas) to deformations fields
93
+ # (just add its origin to each delta vector)
94
+ self ._field += self .reference .ndcoords .T .reshape (self ._field .shape )
95
+
66
96
def __repr__ (self ):
67
97
"""Beautify the python representation."""
68
- return f"<DisplacementFieldTransform [{ self ._field .shape [- 1 ]} D] { self ._field .shape [:3 ]} >"
98
+ return f"<{ self . __class__ . __name__ } [{ self ._field .shape [- 1 ]} D] { self ._field .shape [:3 ]} >"
69
99
70
100
def map (self , x , inverse = False ):
71
101
r"""
72
102
Apply the transformation to a list of physical coordinate points.
73
103
74
104
.. math::
75
- \mathbf{y} = \mathbf{x} + D (\mathbf{x}),
105
+ \mathbf{y} = \mathbf{x} + \Delta (\mathbf{x}),
76
106
\label{eq:2}\tag{2}
77
107
78
- where :math:`D (\mathbf{x})` is the value of the discrete field of displacements
79
- :math:`D ` interpolated at the location :math:`\mathbf{x}`.
108
+ where :math:`\Delta (\mathbf{x})` is the value of the discrete field of displacements
109
+ :math:`\Delta ` interpolated at the location :math:`\mathbf{x}`.
80
110
81
111
Parameters
82
112
----------
83
- x : N x D numpy.ndarray
113
+ x : N x D :obj:` numpy.array_like`
84
114
Input RAS+ coordinates (i.e., physical coordinates).
85
- inverse : bool
115
+ inverse : :obj:` bool`
86
116
If ``True``, apply the inverse transform :math:`x = f^{-1}(y)`.
87
117
88
118
Returns
89
119
-------
90
- y : N x D numpy.ndarray
120
+ y : N x D :obj:` numpy.array_like`
91
121
Transformed (mapped) RAS+ coordinates (i.e., physical coordinates).
92
122
93
123
Examples
94
124
--------
95
- >>> xfm = DisplacementsFieldTransform(test_dir / "someones_displacement_field.nii.gz")
125
+ >>> xfm = DenseFieldTransform(
126
+ ... test_dir / "someones_displacement_field.nii.gz",
127
+ ... is_deltas=False,
128
+ ... )
96
129
>>> xfm.map([-6.5, -36., -19.5]).tolist()
97
- [[-6.5 , -36.475167989730835, -19.5 ]]
130
+ [[0.0 , -0.47516798973083496, 0.0 ]]
98
131
99
132
>>> xfm.map([[-6.5, -36., -19.5], [-1., -41.5, -11.25]]).tolist()
100
- [[-6.5, -36.475167989730835, -19.5], [-1.0, -42.038356602191925, -11.25]]
133
+ [[0.0, -0.47516798973083496, 0.0], [0.0, -0.538356602191925, 0.0]]
134
+
135
+ >>> xfm = DenseFieldTransform(
136
+ ... test_dir / "someones_displacement_field.nii.gz",
137
+ ... is_deltas=True,
138
+ ... )
139
+ >>> xfm.map([[-6.5, -36., -19.5], [-1., -41.5, -11.25]]).tolist()
140
+ [[-6.5, -36.47516632080078, -19.5], [-1.0, -42.03835678100586, -11.25]]
101
141
102
142
"""
103
143
@@ -106,9 +146,51 @@ def map(self, x, inverse=False):
106
146
ijk = self .reference .index (x )
107
147
indexes = np .round (ijk ).astype ("int" )
108
148
if np .any (np .abs (ijk - indexes ) > 0.05 ):
109
- warnings .warn ("Some coordinates are off-grid of the displacements field." )
149
+ warnings .warn ("Some coordinates are off-grid of the field." )
110
150
indexes = tuple (tuple (i ) for i in indexes .T )
111
- return x + self ._field [indexes ]
151
+ return self ._field [indexes ]
152
+
153
+ def __matmul__ (self , b ):
154
+ """
155
+ Compose with a transform on the right.
156
+
157
+ Examples
158
+ --------
159
+ >>> deff = DenseFieldTransform(
160
+ ... test_dir / "someones_displacement_field.nii.gz",
161
+ ... is_deltas=False,
162
+ ... )
163
+ >>> deff2 = deff @ TransformBase()
164
+ >>> deff == deff2
165
+ True
166
+
167
+ >>> disp = DenseFieldTransform(test_dir / "someones_displacement_field.nii.gz")
168
+ >>> disp2 = disp @ TransformBase()
169
+ >>> disp == disp2
170
+ True
171
+
172
+ """
173
+ retval = b .map (
174
+ self ._field .reshape ((- 1 , self ._field .shape [- 1 ]))
175
+ ).reshape (self ._field .shape )
176
+ return DenseFieldTransform (retval , is_deltas = False , reference = self .reference )
177
+
178
+ def __eq__ (self , other ):
179
+ """
180
+ Overload equals operator.
181
+
182
+ Examples
183
+ --------
184
+ >>> xfm1 = DenseFieldTransform(test_dir / "someones_displacement_field.nii.gz")
185
+ >>> xfm2 = DenseFieldTransform(test_dir / "someones_displacement_field.nii.gz")
186
+ >>> xfm1 == xfm2
187
+ True
188
+
189
+ """
190
+ _eq = np .array_equal (self ._field , other ._field )
191
+ if _eq and self ._reference != other ._reference :
192
+ warnings .warn ("Fields are equal, but references do not match." )
193
+ return _eq
112
194
113
195
@classmethod
114
196
def from_filename (cls , filename , fmt = "X5" ):
@@ -123,7 +205,7 @@ def from_filename(cls, filename, fmt="X5"):
123
205
return cls (_factory [fmt ].from_filename (filename ))
124
206
125
207
126
- load = DisplacementsFieldTransform .from_filename
208
+ load = DenseFieldTransform .from_filename
127
209
128
210
129
211
class BSplineFieldTransform (TransformBase ):
@@ -169,8 +251,9 @@ def to_field(self, reference=None, dtype="float32"):
169
251
# 1 x Nvox : (1 x K) @ (K x Nvox)
170
252
field [:, d ] = self ._coeffs [..., d ].reshape (- 1 ) @ self ._weights
171
253
172
- return DisplacementsFieldTransform (
173
- field .astype (dtype ).reshape (* _ref .shape , - 1 ), reference = _ref )
254
+ return DenseFieldTransform (
255
+ field .astype (dtype ).reshape (* _ref .shape , - 1 ), reference = _ref
256
+ )
174
257
175
258
def apply (
176
259
self ,
0 commit comments