-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdicom_to_nifti.py
296 lines (236 loc) · 9.29 KB
/
dicom_to_nifti.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
"""
Module: dicom_to_nifti.py
Nov 28, 2019
David Gobbi
"""
import pydicom
import nibabel as nib
import numpy as np
import argparse
import glob
import sys
import os
import nibabel as nb
usage="""
Convert DICOM (MRI) files to NIFTI:
dicom_to_nifti -i /path/to/input/ -o /path/to/output.nii.gz
The input is a directory that contains a series of DICOM files.
It must contain only one series, corresponding to one 3D volume.
You can use either ".nii" or ".nii.gz" as the suffix for the
nifti files (use .nii.gz to write compressed files).
The output nifti file will be float32 (single precision).
"""
def write_nifti(filename, vol, affine):
"""Write a nifti file with an affine matrix.
"""
output = nib.Nifti1Image(vol.T, affine)
nib.save(output, filename)
def create_affine(ipp, iop, ps):
"""Generate a NIFTI affine matrix from DICOM IPP and IOP attributes.
The ipp (ImagePositionPatient) parameter should an Nx3 array, and
the iop (ImageOrientationPatient) parameter should be Nx6, where
N is the number of DICOM slices in the series.
The return values are the NIFTI affine matrix and the NIFTI pixdim.
Note the the output will use DICOM anatomical coordinates:
x increases towards the left, y increases towards the back.
"""
# solve Ax = b where x is slope, intecept
n = ipp.shape[0]
A = np.column_stack([np.arange(n), np.ones(n)])
x, r, rank, s = np.linalg.lstsq(A, ipp, rcond=None)
# round small values to zero
x[(np.abs(x) < 1e-6)] = 0.0
vec = x[0,:] # slope
pos = x[1,:] # intercept
# pixel spacing should be the same for all image
spacing = np.ones(3)
spacing[0:2] = ps[0,:]
if np.sum(np.abs(ps - spacing[0:2])) > spacing[0]*1e-6:
sys.stderr.write("Pixel spacing is inconsistent!\n");
# compute slice spacing
spacing[2] = np.round(np.sqrt(np.sum(np.square(vec))), 7)
# get the orientation
iop_average = np.mean(iop, axis=0)
u = iop_average[0:3]
u /= np.sqrt(np.sum(np.square(u)))
v = iop_average[3:6]
v /= np.sqrt(np.sum(np.square(v)))
# round small values to zero
u[(np.abs(u) < 1e-6)] = 0.0
v[(np.abs(v) < 1e-6)] = 0.0
# create the matrix
mat = np.eye(4)
mat[0:3,0] = u*spacing[0]
mat[0:3,1] = v*spacing[1]
mat[0:3,2] = vec
mat[0:3,3] = pos
# check whether slice vec is orthogonal to iop vectors
dv = np.dot(vec, np.cross(u, v))
qfac = np.sign(dv)
if np.abs(qfac*dv - spacing[2]) > 1e-6:
sys.stderr.write("Non-orthogonal volume!\n");
# compute the nifti pixdim array
pixdim = np.hstack([np.array(qfac), spacing])
return mat, pixdim
def convert_coords(vol, mat):
"""Convert a volume from DICOM coords to NIFTI coords or vice-versa.
For DICOM, x increases to the left and y increases to the back.
For NIFTI, x increases to the right and y increases to the front.
The conversion is done in-place (volume and matrix are modified).
"""
# the x direction and y direction are flipped
convmat = np.eye(4)
convmat[0,0] = -1.0
convmat[1,1] = -1.0
# apply the coordinate change to the matrix
mat[:] = np.dot(convmat, mat)
# look for x and y elements with greatest magnitude
xabs = np.abs(mat[:,0])
yabs = np.abs(mat[:,1])
xmaxi = np.argmax(xabs)
yabs[xmaxi] = 0.0
ymaxi = np.argmax(yabs)
# re-order the data to ensure these elements aren't negative
# (this may impact the way that the image is displayed, if the
# software that displays the image ignores the matrix).
if mat[xmaxi,0] < 0.0:
# flip x
vol[:] = np.flip(vol, 2)
mat[:,3] += mat[:,0]*(vol.shape[2] - 1)
mat[:,0] = -mat[:,0]
if mat[ymaxi,1] < 0.0:
# flip y
vol[:] = np.flip(vol, 1)
mat[:,3] += mat[:,1]*(vol.shape[1] - 1)
mat[:,1] = -mat[:,1]
# eliminate "-0.0" (negative zero) in the matrix
mat[mat == 0.0] = 0.0
def find_dicom_files(path):
"""Search for DICOM files at the provided location.
"""
if os.path.isdir(path):
# check for common DICOM suffixes
for ext in ("*.dcm", "*.DCM", "*.dc", "*.DC", "*.IMG"):
pattern = os.path.join(path, ext)
files = glob.glob(pattern)
if files:
break
# if no files with DICOM suffix are found, get all files
if not files:
pattern = os.path.join(path, "*")
contents = glob.glob(pattern)
files = [f for f in contents if os.path.isfile(f)]
elif os.path.isfile(path):
# if 'path' is a file (not a folder), return the file
files = [path]
else:
sys.stderr.write("Cannot open %s\n" % (path,))
return []
return files
def load_dicom_series(files):
"""Load a series of dicom files and return a list of datasets.
The resulting list will be sorted by InstanceNumber.
"""
# start by sorting filenames lexically
# sorted_files = sorted(files)
# create list of tuples (InstanceNumber, DataSet)
dataset_list = []
for f in files:
ds = pydicom.dcmread(f)
try:
i = int(ds.InstanceNumber)
except (AttributeError, ValueError):
i = -1
dataset_list.append( (i, ds) )
# sort by InstanceNumber (the first element of each tuple)
dataset_list.sort(key=lambda t: t[0])
# get the dataset from each tuple
series = [t[1] for t in dataset_list]
return series
# def window_image(img, window_center,window_width, intercept, slope, rescale=False):
# img = (img*slope +intercept) #for translation adjustments given in the dicom file.
# img_min = window_center - window_width//2 #minimum HU level
# img_max = window_center + window_width//2 #maximum HU level
# img[img<img_min] = img_min #set img_min for all HU levels less than minimum HU level
# img[img>img_max] = img_max #set img_max for all HU levels higher than maximum HU level
# if rescale:
# img = (img - img_min) / (img_max - img_min)*255.0
# return img
def dicom_to_volume(dicom_series):
"""Convert a DICOM series into a float32 volume with orientation.
The input should be a list of 'dataset' objects from pydicom.
The output is a tuple (voxel_array, voxel_spacing, affine_matrix)
"""
# Create numpy arrays for volume, pixel spacing (ps),
# slice position (ipp or ImagePositinPatient), and
# slice orientation (iop or ImageOrientationPatient)
n = len(dicom_series)
shape = (n,) + dicom_series[0].pixel_array.shape
vol = np.empty(shape, dtype=np.float32)
ps = np.empty((n,2), dtype=np.float64)
ipp = np.empty((n,3), dtype=np.float64)
iop = np.empty((n,6), dtype=np.float64)
for i, ds in enumerate(dicom_series):
# create a single complex-valued image from real,imag
image = ds.pixel_array
image[image <= -1000] = 0
try:
slope = float(ds.RescaleSlope)
except (AttributeError, ValueError):
slope = 1.0
try:
intercept = float(ds.RescaleIntercept)
except (AttributeError, ValueError):
intercept = 0.0
vol[i,:,:] = image*slope + intercept
ps[i,:] = dicom_series[i].PixelSpacing
ipp[i,:] = dicom_series[i].ImagePositionPatient
iop[i,:] = dicom_series[i].ImageOrientationPatient
# create nibabel-style affine matrix and pixdim
# (these give DICOM LPS coords, not NIFTI RAS coords)
affine, pixdim = create_affine(ipp, iop, ps)
return vol, pixdim, affine
def window_image(image, window_center, window_width):
img_min = window_center - window_width // 2
img_max = window_center + window_width // 2
window_image = image.copy()
window_image[window_image < img_min] = img_min
window_image[window_image > img_max] = img_max
return window_image
def converter(input_dicoms, output_nifti, window_center, window_width):
"""Callable entry point.
"""
# create a list of the dicom files
files = find_dicom_files(input_dicoms)
if not files:
sys.stderr.write("No DICOM files found.\n")
return 1
# load the files to create a list of slices
series = load_dicom_series(files)
if not series:
sys.stderr.write("Unable to read DICOM files.\n")
return 1
# reconstruct the images into a volume
vol, pixdim, mat = dicom_to_volume(series)
# convert DICOM coords to NIFTI coords (in-place)
convert_coords(vol, mat)
# write the file (note that the volume will be transposed so that the
# indices will be ordered the way that nibabel prefers: pixel,row,slice
# instead of slice,row,pixel)
vol = window_image(vol, window_center, window_width)
write_nifti(output_nifti, vol, mat)
# """Write a nifti file with an affine matrix.
# """
# output = nib.Nifti1Image(vol.T, affine)
# nib.save(output, filename)
# # testar volume aqui
# fileName = 'exame_linha_teste2_.nii.gz'
# img = nb.Nifti1Image(vol.T, np.eye(4)) # Save axis for data (just identity)
# nb.save(img, os.path.join('build', fileName))
return vol, mat
if __name__ == '__main__':
input_dicoms = r"E:\PycharmProjects\pythonProject\exame\CQ500CT257\Unknown Study\CT 0.625mm"
output_nifti = r"E:\PycharmProjects\pythonProject\build\CQ500CT257.nii.gz"
r = converter(input_dicoms, output_nifti)
if r is not None:
sys.exit(r)