@@ -48,14 +48,19 @@ def __init__(self, matrix=None, reference=None):
|
48 | 48 |
|
49 | 49 | '''
|
50 | 50 | if matrix is None:
|
51 |
| -matrix = np.eye(4) |
| 51 | +matrix = [np.eye(4)] |
| 52 | + |
| 53 | +if np.ndim(matrix) == 2: |
| 54 | +matrix = [matrix] |
52 | 55 |
|
53 | 56 | self._matrix = np.array(matrix)
|
54 |
| -assert self._matrix.ndim in (2, 3), 'affine matrix should be 2D or 3D' |
55 |
| -assert self._matrix.shape[0] == self._matrix.shape[1], 'affine matrix is not square' |
| 57 | +assert self._matrix.ndim == 3, 'affine matrix should be 3D' |
| 58 | +assert self._matrix.shape[-2] == self._matrix.shape[-1], 'affine matrix is not square' |
56 | 59 | super(Affine, self).__init__()
|
57 | 60 |
|
58 | 61 | if reference:
|
| 62 | +if isinstance(reference, str): |
| 63 | +reference = loadimg(reference) |
59 | 64 | self.reference = reference
|
60 | 65 |
|
61 | 66 | @property
|
@@ -116,28 +121,55 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
|
116 | 121 | file=sys.stderr)
|
117 | 122 | reference = moving
|
118 | 123 |
|
| 124 | +nvols = 1 |
| 125 | +if len(moving.shape) > 3: |
| 126 | +nvols = moving.shape[3] |
| 127 | + |
| 128 | +movaff = moving.affine |
| 129 | +movingdata = moving.get_data() |
| 130 | +if nvols == 1: |
| 131 | +movingdata = movingdata[..., np.newaxis] |
| 132 | + |
| 133 | +nmats = self._matrix.shape[0] |
| 134 | +if nvols != nmats and nmats > 1: |
| 135 | +raise ValueError("""\ |
| 136 | +The moving image contains {0} volumes, while the transform is defined for \ |
| 137 | +{1} volumes""".format(nvols, nmats)) |
| 138 | + |
| 139 | +singlemat = None |
| 140 | +if nmats == 1: |
| 141 | +singlemat = np.linalg.inv(movaff).dot(self._matrix[0].dot(reference.affine)) |
| 142 | + |
| 143 | +if singlemat is not None and nvols > nmats: |
| 144 | +print('Warning: resampling a 4D volume with a single affine matrix', |
| 145 | +file=sys.stderr) |
| 146 | + |
119 | 147 | # Compose an index to index affine matrix
|
120 |
| -matrix = np.linalg.inv(moving.affine).dot(self._matrix.dot(reference.affine)) |
121 |
| -mdata = moving.get_data() |
122 |
| -moved = ndi.affine_transform( |
123 |
| -mdata, matrix=matrix[:mdata.ndim, :mdata.ndim], |
124 |
| -offset=matrix[:mdata.ndim, mdata.ndim], |
125 |
| -output_shape=reference.shape, order=order, mode=mode, |
126 |
| -cval=cval, prefilter=prefilter) |
127 |
| - |
128 |
| -moved_image = moving.__class__(moved, reference.affine, moving.header) |
| 148 | +moved = [] |
| 149 | +for i in range(nvols): |
| 150 | +i2imat = singlemat if singlemat is not None else np.linalg.inv( |
| 151 | +movaff).dot(self._matrix[i].dot(reference.affine)) |
| 152 | +mdata = movingdata[..., i] |
| 153 | +moved += [ndi.affine_transform( |
| 154 | +mdata, matrix=i2imat[:-1, :-1], |
| 155 | +offset=i2imat[:-1, -1], |
| 156 | +output_shape=reference.shape, order=order, mode=mode, |
| 157 | +cval=cval, prefilter=prefilter)] |
| 158 | + |
| 159 | +moved_image = moving.__class__( |
| 160 | +np.squeeze(np.stack(moved, -1)), reference.affine, moving.header) |
129 | 161 | moved_image.header.set_data_dtype(output_dtype)
|
130 | 162 |
|
131 | 163 | return moved_image
|
132 | 164 |
|
133 |
| -def map_point(self, coords, forward=True): |
| 165 | +def map_point(self, coords, index=0, forward=True): |
134 | 166 | coords = np.array(coords)
|
135 |
| -if coords.shape[0] == self._matrix.shape[0] - 1: |
| 167 | +if coords.shape[0] == self._matrix[index].shape[0] - 1: |
136 | 168 | coords = np.append(coords, [1])
|
137 |
| -affine = self._matrix if forward else np.linalg.inv(self._matrix) |
| 169 | +affine = self._matrix[index] if forward else np.linalg.inv(self._matrix[index]) |
138 | 170 | return affine.dot(coords)[:-1]
|
139 | 171 |
|
140 |
| -def map_voxel(self, index, moving=None): |
| 172 | +def map_voxel(self, index, nindex=0, moving=None): |
141 | 173 | try:
|
142 | 174 | reference = self.reference
|
143 | 175 | except ValueError:
|
@@ -152,16 +184,16 @@ def map_voxel(self, index, moving=None):
|
152 | 184 | raise ValueError('Reference and moving spaces are both undefined')
|
153 | 185 |
|
154 | 186 | index = np.array(index)
|
155 |
| -if index.shape[0] == self._matrix.shape[0] - 1: |
| 187 | +if index.shape[0] == self._matrix[nindex].shape[0] - 1: |
156 | 188 | index = np.append(index, [1])
|
157 | 189 |
|
158 |
| -matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine))) |
| 190 | +matrix = reference.affine.dot(self._matrix[nindex].dot(np.linalg.inv(moving.affine))) |
159 | 191 | return tuple(matrix.dot(index)[:-1])
|
160 | 192 |
|
161 | 193 | def _to_hdf5(self, x5_root):
|
162 |
| -x5_root.create_dataset('Transform', data=self._matrix[:3, :]) |
163 |
| -x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix)[:3, :]) |
164 |
| -x5_root['Type'] = 'affine' |
| 194 | +xform = x5_root.create_dataset('Transform', data=self._matrix) |
| 195 | +xform.attrs['Type'] = 'affine' |
| 196 | +x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix)) |
165 | 197 |
|
166 | 198 | if self._reference:
|
167 | 199 | self.reference._to_hdf5(x5_root.create_group('Reference'))
|
@@ -171,24 +203,26 @@ def to_filename(self, filename, fmt='X5', moving=None):
|
171 | 203 | '''
|
172 | 204 |
|
173 | 205 | if fmt.lower() in ['itk', 'ants', 'elastix', 'nifty']:
|
174 |
| -parameters = LPS.dot(self.matrix.dot(LPS)) |
175 |
| -parameters = parameters[:3, :3].reshape(-1).tolist() + parameters[:3, 3].tolist() |
176 |
| -itkfmt = """\ |
177 |
| -#Insight Transform File V1.0 |
178 |
| -#Transform 0 |
| 206 | +with open(filename, 'w') as f: |
| 207 | +f.write('#Insight Transform File V1.0\n') |
| 208 | + |
| 209 | +for i in range(self.matrix.shape[0]): |
| 210 | +parameters = LPS.dot(self.matrix[i].dot(LPS)) |
| 211 | +parameters = parameters[:3, :3].reshape(-1).tolist() + \ |
| 212 | +parameters[:3, 3].tolist() |
| 213 | +itkfmt = """\ |
| 214 | +#Transform {0} |
179 | 215 | Transform: MatrixOffsetTransformBase_double_3_3
|
180 |
| -Parameters: {} |
| 216 | +Parameters: {1} |
181 | 217 | FixedParameters: 0 0 0\n""".format
|
182 |
| -with open(filename, 'w') as f: |
183 |
| -f.write(itkfmt(' '.join(['%g' % p for p in parameters]))) |
| 218 | +f.write(itkfmt(i, ' '.join(['%g' % p for p in parameters]))) |
184 | 219 | return filename
|
185 | 220 |
|
186 | 221 | if fmt.lower() == 'afni':
|
187 |
| -parameters = LPS.dot(self.matrix.dot(LPS)) |
188 |
| -parameters = parameters[:3, :].reshape(-1).tolist() |
189 |
| -np.savetxt(filename, np.atleast_2d(parameters), |
190 |
| -delimiter='\t', header="""\ |
191 |
| -3dvolreg matrices (DICOM-to-DICOM, row-by-row):""") |
| 222 | +parameters = np.swapaxes(LPS.dot(self.matrix.dot(LPS)), 0, 1) |
| 223 | +parameters = parameters[:, :3, :].reshape((self.matrix.shape[0], -1)) |
| 224 | +np.savetxt(filename, parameters, delimiter='\t', header="""\ |
| 225 | +3dvolreg matrices (DICOM-to-DICOM, row-by-row):""", fmt='%g') |
192 | 226 | return filename
|
193 | 227 |
|
194 | 228 | if fmt.lower() == 'fsl':
|
@@ -209,8 +243,15 @@ def to_filename(self, filename, fmt='X5', moving=None):
|
209 | 243 | moving.affine)))
|
210 | 244 |
|
211 | 245 | # Compose FSL transform
|
212 |
| -mat = np.linalg.inv(post.dot(self.matrix.dot(pre))) |
213 |
| -np.savetxt(filename, mat, delimiter=' ', fmt='%g') |
| 246 | +mat = np.linalg.inv( |
| 247 | +np.swapaxes(post.dot(self.matrix.dot(pre)), 0, 1)) |
| 248 | + |
| 249 | +if self.matrix.shape[0] > 1: |
| 250 | +for i in range(self.matrix.shape[0]): |
| 251 | +np.savetxt('%s.%03d' % (filename, i), mat[i], |
| 252 | +delimiter=' ', fmt='%g') |
| 253 | +else: |
| 254 | +np.savetxt(filename, mat[0], delimiter=' ', fmt='%g') |
214 | 255 | return filename
|
215 | 256 | return super(Affine, self).to_filename(filename, fmt=fmt)
|
216 | 257 |
|
@@ -222,17 +263,29 @@ def load(filename, fmt='X5', reference=None):
|
222 | 263 | with open(filename) as itkfile:
|
223 | 264 | itkxfm = itkfile.read().splitlines()
|
224 | 265 |
|
225 |
| -parameters = np.fromstring(itkxfm[3].split(':')[-1].strip(), dtype=float, sep=' ') |
226 |
| -offset = np.fromstring(itkxfm[4].split(':')[-1].strip(), dtype=float, sep=' ') |
227 |
| -if len(parameters) == 12: |
228 |
| -matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:]) |
229 |
| -c_neg = from_matvec(np.eye(3), offset * -1.0) |
230 |
| -c_pos = from_matvec(np.eye(3), offset) |
231 |
| -matrix = LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS)))) |
232 |
| - |
233 |
| -# if fmt.lower() == 'afni': |
| 266 | +if 'Insight Transform File' not in itkxfm[0]: |
| 267 | +raise ValueError( |
| 268 | +"File %s does not look like an ITK transform text file." % filename) |
| 269 | + |
| 270 | +matlist = [] |
| 271 | +for nxfm in range(len(itkxfm[1:]) // 4): |
| 272 | +parameters = np.fromstring(itkxfm[nxfm * 4 + 3].split(':')[-1].strip(), |
| 273 | +dtype=float, sep=' ') |
| 274 | +offset = np.fromstring(itkxfm[nxfm * 4 + 4].split(':')[-1].strip(), |
| 275 | +dtype=float, sep=' ') |
| 276 | +if len(parameters) == 12: |
| 277 | +matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:]) |
| 278 | +c_neg = from_matvec(np.eye(3), offset * -1.0) |
| 279 | +c_pos = from_matvec(np.eye(3), offset) |
| 280 | +matlist.append(LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS))))) |
| 281 | +matrix = np.stack(matlist) |
| 282 | +# elif fmt.lower() == 'afni': |
234 | 283 | # parameters = LPS.dot(self.matrix.dot(LPS))
|
235 | 284 | # parameters = parameters[:3, :].reshape(-1).tolist()
|
| 285 | +elif fmt.lower() in ('x5', 'bids'): |
| 286 | +raise NotImplementedError |
| 287 | +else: |
| 288 | +raise NotImplementedError |
236 | 289 |
|
237 | 290 | if reference and isinstance(reference, str):
|
238 | 291 | reference = loadimg(reference)
|
|
0 commit comments