@@ -27,23 +27,27 @@ class WrapperError(Exception):
27
27
pass
28
28
29
29
30
- def wrapper_from_file (file_like ):
30
+ def wrapper_from_file (file_like , * args , ** kwargs ):
31
31
''' Create DICOM wrapper from `file_like` object
32
32
33
33
Parameters
34
34
----------
35
35
file_like : object
36
36
filename string or file-like object, pointing to a valid DICOM
37
37
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
+
39
43
Returns
40
44
-------
41
45
dcm_w : ``dicomwrappers.Wrapper`` or subclass
42
46
DICOM wrapper corresponding to DICOM data type
43
47
'''
44
48
import dicom
45
49
fobj = allopen (file_like )
46
- dcm_data = dicom .read_file (fobj )
50
+ dcm_data = dicom .read_file (fobj , * args , ** kwargs )
47
51
return wrapper_from_data (dcm_data )
48
52
49
53
@@ -115,11 +119,8 @@ def __init__(self, dcm_data=None):
115
119
@one_time
116
120
def image_shape (self ):
117
121
''' 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.
121
122
'''
122
- shape = (self .get ('Columns ' ), self .get ('Rows ' ))
123
+ shape = (self .get ('Rows ' ), self .get ('Columns ' ))
123
124
if None in shape :
124
125
return None
125
126
return shape
@@ -140,12 +141,22 @@ def slice_normal(self):
140
141
141
142
@one_time
142
143
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
+ '''
143
151
iop = self .image_orient_patient
144
152
s_norm = self .slice_normal
145
153
if None in (iop , s_norm ):
146
154
return None
147
155
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 )
149
160
R [:,2 ] = s_norm
150
161
# check this is in fact a rotation matrix
151
162
assert np .allclose (np .eye (3 ),
@@ -156,11 +167,10 @@ def rotation_matrix(self):
156
167
@one_time
157
168
def voxel_sizes (self ):
158
169
''' 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.
163
170
'''
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
164
174
pix_space = self .get ('PixelSpacing' )
165
175
if pix_space is None :
166
176
return None
@@ -169,7 +179,7 @@ def voxel_sizes(self):
169
179
zs = self .get ('SliceThickness' )
170
180
if zs is None :
171
181
zs = 1
172
- return tuple (pix_space [:: - 1 ] + [zs ])
182
+ return tuple (pix_space + [zs ])
173
183
174
184
@one_time
175
185
def image_position (self ):
@@ -249,7 +259,7 @@ def get(self, key, default=None):
249
259
250
260
def get_affine (self ):
251
261
''' Return mapping between voxel and DICOM coordinate system
252
-
262
+
253
263
Parameters
254
264
----------
255
265
None
@@ -260,7 +270,13 @@ def get_affine(self):
260
270
Affine giving transformation between voxels in data array and
261
271
mm in the DICOM patient coordinate system.
262
272
'''
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
263
277
orient = self .rotation_matrix
278
+ # therefore, these voxel sizes are in the right order (row,
279
+ # column, slice)
264
280
vox = self .voxel_sizes
265
281
ipp = self .image_position
266
282
if None in (orient , vox , ipp ):
@@ -280,18 +296,16 @@ def get_pixel_array(self):
280
296
def get_data (self ):
281
297
''' Get scaled image data from DICOMs
282
298
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
287
301
288
302
Returns
289
303
-------
290
304
data : array
291
305
array with data as scaled from any scaling in the DICOM
292
306
fields.
293
307
'''
294
- return self ._scale_data (self .get_pixel_array ()). T
308
+ return self ._scale_data (self .get_pixel_array ())
295
309
296
310
def is_same_series (self , other ):
297
311
''' Return True if `other` appears to be in same series
@@ -520,11 +534,8 @@ def image_shape(self):
520
534
if None in (rows , cols ):
521
535
return None
522
536
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 ),
528
539
self .n_mosaic )
529
540
530
541
@one_time
@@ -544,20 +555,24 @@ def image_position(self):
544
555
position in mm of voxel (0,0,0) in Mosaic array
545
556
'''
546
557
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' ))
548
560
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 ):
551
563
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
556
569
# apply algorithm for undoing mosaic translation error - see
557
570
# ``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 ()
561
576
562
577
def get_data (self ):
563
578
''' Get scaled image data from DICOMs
@@ -578,11 +593,13 @@ def get_data(self):
578
593
data = self .get_pixel_array ()
579
594
v4 = data .reshape (mosaic_size ,n_rows ,
580
595
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 ))
583
600
# 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 )
586
603
587
604
588
605
def none_or_close (val1 , val2 , rtol = 1e-5 , atol = 1e-6 ):
0 commit comments