11
11
In addition to the standard ODL requirements, this library also requires:
12
12
13
13
- tqdm
14
- - dicom
15
- - A copy of the Mayo dataset, see
16
- https://www.aapm.org/GrandChallenge/LowDoseCT/#registration
14
+ - pydicom
15
+ - Samples from the Mayo dataset, see
16
+ https://ctcicblog.mayo.edu/hubcap/patient-ct-projection-data-library/
17
17
"""
18
18
19
19
from __future__ import division
23
23
import pydicom
24
24
import odl
25
25
import tqdm
26
+ from functools import partial
26
27
27
28
from pydicom .datadict import DicomDictionary , keyword_dict
28
29
from odl .discr .discr_utils import linear_interpolator
38
39
__all__ = ('load_projections' , 'load_reconstruction' )
39
40
40
41
41
- def _read_projections (folder , indices ):
42
- """Read mayo projections from a folder ."""
42
+ def _read_projections (dir , indices ):
43
+ """Read mayo projections from a directory ."""
43
44
datasets = []
44
45
data_array = []
45
46
46
47
# Get the relevant file names
47
- file_names = sorted ([f for f in os .listdir (folder ) if f .endswith (".dcm" )])
48
+ file_names = sorted ([f for f in os .listdir (dir ) if f .endswith (".dcm" )])
48
49
49
50
if len (file_names ) == 0 :
50
- raise ValueError ('No DICOM files found in {}' .format (folder ))
51
+ raise ValueError ('No DICOM files found in {}' .format (dir ))
51
52
52
53
file_names = file_names [indices ]
53
54
54
55
for i , file_name in enumerate (tqdm .tqdm (file_names ,
55
56
'Loading projection data' )):
56
57
# read the file
57
58
try :
58
- dataset = pydicom .read_file (folder + os .path .sep + file_name )
59
+ dataset = pydicom .read_file (os .path .join ( dir , file_name ) )
59
60
except :
60
61
print ("corrupted file: {}" .format (file_name ), file = sys .stderr )
61
62
print ("error:\n {}" .format (sys .exc_info ()[1 ]), file = sys .stderr )
62
- print ("skipping to next file.." , file = sys .stderr )
63
- continue
63
+ raise
64
64
65
65
if not data_array :
66
66
# Get some required data
@@ -76,29 +76,28 @@ def _read_projections(folder, indices):
76
76
assert rescale_intercept == dataset .RescaleIntercept
77
77
assert rescale_slope == dataset .RescaleSlope
78
78
79
-
80
79
# Load the array as bytes
81
80
proj_array = np .array (np .frombuffer (dataset .PixelData , 'H' ),
82
81
dtype = 'float32' )
83
82
proj_array = proj_array .reshape ([cols , rows ])
84
83
data_array .append (proj_array [:, ::- 1 ])
85
84
datasets .append (dataset )
86
85
87
- data_array = np .array (data_array )
86
+ data_array = np .stack (data_array )
88
87
# Rescale array
89
88
data_array *= rescale_slope
90
89
data_array += rescale_intercept
91
90
92
91
return datasets , data_array
93
92
94
93
95
- def load_projections (folder , indices = None , use_ffs = True ):
96
- """Load geometry and data stored in Mayo format from folder .
94
+ def load_projections (dir , indices = None , use_ffs = True ):
95
+ """Load geometry and data stored in Mayo format from dir .
97
96
98
97
Parameters
99
98
----------
100
- folder : str
101
- Path to the folder where the Mayo DICOM files are stored.
99
+ dir : str
100
+ Path to the directory where the Mayo DICOM files are stored.
102
101
indices : optional
103
102
Indices of the projections to load.
104
103
Accepts advanced indexing such as slice or list of indices.
@@ -114,7 +113,7 @@ def load_projections(folder, indices=None, use_ffs=True):
114
113
Projection data, given as the line integral of the linear attenuation
115
114
coefficient (g/cm^3). Its unit is thus g/cm^2.
116
115
"""
117
- datasets , data_array = _read_projections (folder , indices )
116
+ datasets , data_array = _read_projections (dir , indices )
118
117
119
118
# Get the angles
120
119
angles = np .array ([d .DetectorFocalCenterAngularPosition for d in datasets ])
@@ -139,8 +138,8 @@ def load_projections(folder, indices=None, use_ffs=True):
139
138
# For unknown reasons, mayo does not include the tag
140
139
# "TableFeedPerRotation", which is what we want.
141
140
# Instead we manually compute the pitch
142
- table_dist = datasets [- 1 ].DetectorFocalCenterAxialPosition - \
143
- datasets [0 ].DetectorFocalCenterAxialPosition
141
+ table_dist = ( datasets [- 1 ].DetectorFocalCenterAxialPosition -
142
+ datasets [0 ].DetectorFocalCenterAxialPosition )
144
143
num_rot = (angles [- 1 ] - angles [0 ]) / (2 * np .pi )
145
144
pitch = table_dist / num_rot
146
145
@@ -160,17 +159,19 @@ def load_projections(folder, indices=None, use_ffs=True):
160
159
detector_partition = odl .uniform_partition (det_minp , det_maxp , det_shape )
161
160
162
161
# Convert offset to odl definitions
163
- offset_along_axis = datasets [0 ].DetectorFocalCenterAxialPosition - \
164
- angles [0 ] / (2 * np .pi ) * pitch
162
+ offset_along_axis = ( datasets [0 ].DetectorFocalCenterAxialPosition -
163
+ angles [0 ] / (2 * np .pi ) * pitch )
165
164
166
165
# Assemble geometry
167
166
angle_partition = odl .nonuniform_partition (angles )
168
167
169
168
# Flying focal spot
170
169
src_shift_func = None
171
170
if use_ffs :
172
- src_shift_func = lambda angle : odl .tomo .flying_focal_spot (
173
- angle , apart = angle_partition , shifts = shifts )
171
+ src_shift_func = partial (
172
+ odl .tomo .flying_focal_spot , apart = angle_partition , shifts = shifts )
173
+ else :
174
+ src_shift_func = None
174
175
175
176
geometry = odl .tomo .ConeBeamGeometry (angle_partition ,
176
177
detector_partition ,
@@ -203,8 +204,8 @@ def interpolate_flat_grid(data_array, range_grid, radial_dist):
203
204
"""
204
205
205
206
# convert coordinates
206
- theta , up , vp = range_grid .meshgrid #ray_trafo.range.grid.meshgrid
207
- # d = src_radius + det_radius
207
+ theta , up , vp = range_grid .meshgrid
208
+
208
209
u = radial_dist * np .arctan (up / radial_dist )
209
210
v = radial_dist / np .sqrt (radial_dist ** 2 + up ** 2 ) * vp
210
211
@@ -218,13 +219,13 @@ def interpolate_flat_grid(data_array, range_grid, radial_dist):
218
219
return proj_data
219
220
220
221
221
- def load_reconstruction (folder , slice_start = 0 , slice_end = - 1 ):
222
- """Load a volume from folder , also returns the corresponding partition.
222
+ def load_reconstruction (dir , slice_start = 0 , slice_end = - 1 ):
223
+ """Load a volume from dir , also returns the corresponding partition.
223
224
224
225
Parameters
225
226
----------
226
- folder : str
227
- Path to the folder where the DICOM files are stored.
227
+ dir : str
228
+ Path to the directory where the DICOM files are stored.
228
229
slice_start : int
229
230
Index of the first slice to use. Used for subsampling.
230
231
slice_end : int
@@ -249,10 +250,10 @@ def load_reconstruction(folder, slice_start=0, slice_end=-1):
249
250
This function should handle all of these peculiarities and give a volume
250
251
with the correct coordinate system attached.
251
252
"""
252
- file_names = sorted ([f for f in os .listdir (folder ) if f .endswith (".dcm" )])
253
+ file_names = sorted ([f for f in os .listdir (dir ) if f .endswith (".dcm" )])
253
254
254
255
if len (file_names ) == 0 :
255
- raise ValueError ('No DICOM files found in {}' .format (folder ))
256
+ raise ValueError ('No DICOM files found in {}' .format (dir ))
256
257
257
258
volumes = []
258
259
datasets = []
@@ -261,7 +262,7 @@ def load_reconstruction(folder, slice_start=0, slice_end=-1):
261
262
262
263
for file_name in tqdm .tqdm (file_names , 'loading volume data' ):
263
264
# read the file
264
- dataset = pydicom .read_file (folder + os .path .sep + file_name )
265
+ dataset = pydicom .read_file (os .path .join ( dir , file_name ) )
265
266
266
267
# Get parameters
267
268
pixel_size = np .array (dataset .PixelSpacing )
0 commit comments