Skip to content

Commit a86048d

Browse files
committed
RF - adjust dicomwrappers to new data returning policy
1 parent 6e97ff7 commit a86048d

File tree

2 files changed

+62
-43
lines changed

2 files changed

+62
-43
lines changed

nibabel/dicom/dicomwrappers.py

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,27 @@ class WrapperError(Exception):
2727
pass
2828

2929

30-
def wrapper_from_file(file_like):
30+
def wrapper_from_file(file_like, *args, **kwargs):
3131
''' Create DICOM wrapper from `file_like` object
3232
3333
Parameters
3434
----------
3535
file_like : object
3636
filename string or file-like object, pointing to a valid DICOM
3737
file readable by ``pydicom``
38-
38+
*args : positional
39+
**kwargs : keyword
40+
args to ``dicom.read_file`` command. ``force=True`` might be a
41+
likely keyword argument.
42+
3943
Returns
4044
-------
4145
dcm_w : ``dicomwrappers.Wrapper`` or subclass
4246
DICOM wrapper corresponding to DICOM data type
4347
'''
4448
import dicom
4549
fobj = allopen(file_like)
46-
dcm_data = dicom.read_file(fobj)
50+
dcm_data = dicom.read_file(fobj, *args, **kwargs)
4751
return wrapper_from_data(dcm_data)
4852

4953

@@ -115,11 +119,8 @@ def __init__(self, dcm_data=None):
115119
@one_time
116120
def image_shape(self):
117121
''' The array shape as it will be returned by ``get_data()``
118-
119-
Note that we transpose the array from the stanard DICOM
120-
understaning, to match the affine.
121122
'''
122-
shape = (self.get('Columns'), self.get('Rows'))
123+
shape = (self.get('Rows'), self.get('Columns'))
123124
if None in shape:
124125
return None
125126
return shape
@@ -140,12 +141,22 @@ def slice_normal(self):
140141

141142
@one_time
142143
def rotation_matrix(self):
144+
''' Return rotation matrix between array indices and mm
145+
146+
Note that we swap the two columns of the 'ImageOrientPatient'
147+
when we create the rotation matrix. This is takes into account
148+
the slightly odd ij transpose construction of the DICOM
149+
orientation fields - see doc/theory/dicom_orientaiton.rst.
150+
'''
143151
iop = self.image_orient_patient
144152
s_norm = self.slice_normal
145153
if None in (iop, s_norm):
146154
return None
147155
R = np.eye(3)
148-
R[:,:2] = iop
156+
# fliplr accounts for the fact that the first column in ``iop``
157+
# refers to changes in column index, and the second to changes
158+
# in row index. See doc/theory/dicom_orientation.rst
159+
R[:,:2] = np.fliplr(iop)
149160
R[:,2] = s_norm
150161
# check this is in fact a rotation matrix
151162
assert np.allclose(np.eye(3),
@@ -156,11 +167,10 @@ def rotation_matrix(self):
156167
@one_time
157168
def voxel_sizes(self):
158169
''' voxel sizes for array as returned by ``get_data()``
159-
160-
Note that the first returned value refers to what DICOM would
161-
call the 'Column' spacing, and the second to 'Row' spacing.
162-
This is to match the returned data.
163170
'''
171+
# pix space gives (row_spacing, column_spacing). That is, the
172+
# mm you move when moving from one row to the next, and the mm
173+
# you move when moving from one column to the next
164174
pix_space = self.get('PixelSpacing')
165175
if pix_space is None:
166176
return None
@@ -169,7 +179,7 @@ def voxel_sizes(self):
169179
zs = self.get('SliceThickness')
170180
if zs is None:
171181
zs = 1
172-
return tuple(pix_space[::-1] + [zs])
182+
return tuple(pix_space + [zs])
173183

174184
@one_time
175185
def image_position(self):
@@ -249,7 +259,7 @@ def get(self, key, default=None):
249259

250260
def get_affine(self):
251261
''' Return mapping between voxel and DICOM coordinate system
252-
262+
253263
Parameters
254264
----------
255265
None
@@ -260,7 +270,13 @@ def get_affine(self):
260270
Affine giving transformation between voxels in data array and
261271
mm in the DICOM patient coordinate system.
262272
'''
273+
# rotation matrix already accounts for the ij transpose in the
274+
# DICOM image orientation patient transform. So. column 0 is
275+
# direction cosine for changes in row index, column 1 is
276+
# direction cosine for changes in column index
263277
orient = self.rotation_matrix
278+
# therefore, these voxel sizes are in the right order (row,
279+
# column, slice)
264280
vox = self.voxel_sizes
265281
ipp = self.image_position
266282
if None in (orient, vox, ipp):
@@ -280,18 +296,16 @@ def get_pixel_array(self):
280296
def get_data(self):
281297
''' Get scaled image data from DICOMs
282298
283-
Note that this array will be transposed compared to DICOM's
284-
understanding of rows and columns, thus, what DICOM calls
285-
'rows', will be columns, and vice versa. This is to match the
286-
affine matrix.
299+
We return the data as DICOM understands it, first dimension is
300+
rows, second dimension is columns
287301
288302
Returns
289303
-------
290304
data : array
291305
array with data as scaled from any scaling in the DICOM
292306
fields.
293307
'''
294-
return self._scale_data(self.get_pixel_array()).T
308+
return self._scale_data(self.get_pixel_array())
295309

296310
def is_same_series(self, other):
297311
''' Return True if `other` appears to be in same series
@@ -520,11 +534,8 @@ def image_shape(self):
520534
if None in (rows, cols):
521535
return None
522536
mosaic_size = self.mosaic_size
523-
# the columns and rows are transposed to match the way we're
524-
# returning the data, which in turn matches the way we're
525-
# returning the affine
526-
return (int(cols / mosaic_size),
527-
int(rows / mosaic_size),
537+
return (int(rows / mosaic_size),
538+
int(cols / mosaic_size),
528539
self.n_mosaic)
529540

530541
@one_time
@@ -544,20 +555,24 @@ def image_position(self):
544555
position in mm of voxel (0,0,0) in Mosaic array
545556
'''
546557
ipp = self.get('ImagePositionPatient')
547-
o_rows, o_cols = (self.get('Rows'), self.get('Columns'))
558+
# mosaic image size
559+
md_rows, md_cols = (self.get('Rows'), self.get('Columns'))
548560
iop = self.image_orient_patient
549-
vox = self.voxel_sizes
550-
if None in (ipp, o_rows, o_cols, iop, vox):
561+
pix_spacing = self.get('PixelSpacing')
562+
if None in (ipp, md_rows, md_cols, iop, pix_spacing):
551563
return None
552-
# size of mosaic array before rearranging to 3D
553-
md_xy = np.array([o_rows, o_cols])
554-
# size of slice X, Y in array after reshaping to 3D
555-
rd_xy = md_xy / self.mosaic_size
564+
# size of mosaic array before rearranging to 3D. Note cols /
565+
# rows order - see doc referenced above for explanation.
566+
md_cr = np.array([md_cols, md_rows])
567+
# size of slice array after reshaping to 3D
568+
rd_cr = md_cr / self.mosaic_size
556569
# apply algorithm for undoing mosaic translation error - see
557570
# ``dicom_mosaic`` doc
558-
vox_trans_fixes = (md_xy - rd_xy) / 2
559-
M = iop * vox[:2]
560-
return ipp + np.dot(M, vox_trans_fixes[:,None]).ravel()
571+
vox_trans_fixes = (md_cr - rd_cr) / 2
572+
# again, see doc for why cols, rows are in this order
573+
row_spacing, col_spacing = pix_spacing
574+
Q = iop * [col_spacing, row_spacing]
575+
return ipp + np.dot(Q, vox_trans_fixes[:,None]).ravel()
561576

562577
def get_data(self):
563578
''' Get scaled image data from DICOMs
@@ -578,11 +593,13 @@ def get_data(self):
578593
data = self.get_pixel_array()
579594
v4=data.reshape(mosaic_size,n_rows,
580595
mosaic_size,n_cols)
581-
v4p=np.rollaxis(v4,2,1)
582-
v3=v4p.reshape(mosaic_size*mosaic_size,n_rows,n_cols)
596+
# move the mosaic dims to the end
597+
v4=v4.transpose((1,3,0,2))
598+
# pool mosaic-generated dims
599+
v3=v4.reshape((n_rows, n_cols, mosaic_size*mosaic_size))
583600
# delete any padding slices
584-
v3 = v3[:n_mosaic]
585-
return self._scale_data(v3).T
601+
v3 = v3[...,:n_mosaic]
602+
return self._scale_data(v3)
586603

587604

588605
def none_or_close(val1, val2, rtol=1e-5, atol=1e-6):

nibabel/dicom/tests/test_dicomwrappers.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@
2525

2626
# this affine from our converted image was shown to match our image
2727
# spatially with an image from SPM DICOM conversion. We checked the
28-
# matching with SPM check reg.
28+
# matching with SPM check reg. We have flipped the first and second
29+
# rows to allow for rows, cols tranpose.
2930
EXPECTED_AFFINE = np.array(
3031
[[ -1.796875, 0, 0, 115],
3132
[0, -1.79684984, -0.01570896, 135.028779],
3233
[0, -0.00940843750, 2.99995887, -78.710481],
33-
[0, 0, 0, 1]])
34+
[0, 0, 0, 1]])[:,[1,0,2,3]]
3435

35-
# from Guys and Matthew's SPM code, with Y flip reversed
36-
EXPECTED_PARAMS = [992.05050247, (0.99997450,
37-
0.00507649,
36+
# from Guys and Matthew's SPM code, undoing SPM's Y flip, and swapping
37+
# first two values in vector, to account for data rows, cols difference.
38+
EXPECTED_PARAMS = [992.05050247, (0.00507649,
39+
0.99997450,
3840
-0.005023611)]
3941

4042

0 commit comments

Comments
 (0)