dicom_grouper.py
12.4 KB
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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
#---------------------------------------------------------------------
# Software: InVesalius Software de Reconstrucao 3D de Imagens Medicas
# Copyright: (c) 2001 Centro de Pesquisas Renato Archer
# Homepage: http://www.softwarepublico.gov.br
# Contact: invesalius@cenpra.gov.br
# License: GNU - General Public License version 2 (LICENSE.txt/
# LICENCA.txt)
#
# Este programa eh software livre; voce pode redistribui-lo e/ou
# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
# publicada pela Free Software Foundation; de acordo com a versao 2
# da Licenca.
#
# Este programa eh distribuido na expectativa de ser util, mas SEM
# QUALQUER GARANTIA; sem mesmo a garantia implicita de
# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
# detalhes.
#---------------------------------------------------------------------
# ---------------------------------------------------------
# PROBLEM 1
# There are times when there are lots of groups on dict, but
# each group contains only one slice (DICOM file).
#
# Equipments / manufacturer:
# TODO
#
# Cases:
# TODO 0031, 0056, 1093
#
# What occurs in these cases:
# <dicom.image.number> and <dicom.acquisition.series_number>
# were swapped
# -----------------------------------------------------------
# PROBLEM 2
# Two slices (DICOM file) inside a group have the same
# position.
#
# Equipments / manufacturer:
# TODO
#
# Cases:
# TODO 0031, 0056, 1093
#
# What occurs in these cases:
# <dicom.image.number> and <dicom.acquisition.series_number>
# were swapped
import gdcm
import invesalius.utils as utils
ORIENT_MAP = {"SAGITTAL":0, "CORONAL":1, "AXIAL":2, "OBLIQUE":2}
class DicomGroup:
general_index = -1
def __init__(self):
DicomGroup.general_index += 1
self.index = DicomGroup.general_index
# key:
# (dicom.patient.name, dicom.acquisition.id_study,
# dicom.acquisition.series_number,
# dicom.image.orientation_label, index)
self.key = ()
self.title = ""
self.slices_dict = {} # slice_position: Dicom.dicom
# IDEA (13/10): Represent internally as dictionary,
# externally as list
self.nslices = 0
self.zspacing = 1
self.dicom = None
def AddSlice(self, dicom):
if not self.dicom:
self.dicom = dicom
pos = tuple(dicom.image.position)
#Case to test: \other\higroma
#condition created, if any dicom with the same
#position, but 3D, leaving the same series.
if not "DERIVED" in dicom.image.type:
#if any dicom with the same position
if pos not in self.slices_dict.keys():
self.slices_dict[pos] = dicom
self.nslices += 1
return True
else:
return False
else:
self.slices_dict[dicom.image.number] = dicom
self.nslices += 1
return True
def GetList(self):
# Should be called when user selects this group
# This list will be used to create the vtkImageData
# (interpolated)
return self.slices_dict.values()
def GetFilenameList(self):
# Should be called when user selects this group
# This list will be used to create the vtkImageData
# (interpolated)
filelist = [dicom.image.file for dicom in
self.slices_dict.values()]
# Sort slices using GDCM
if (self.dicom.image.orientation_label <> "CORONAL"):
#Organize reversed image
sorter = gdcm.IPPSorter()
sorter.SetComputeZSpacing(True)
sorter.SetZSpacingTolerance(1e-10)
sorter.Sort(filelist)
filelist = sorter.GetFilenames()
# for breast-CT of koning manufacturing (KBCT)
if self.slices_dict.values()[0].parser.GetManufacturerName() == "Koning":
filelist.sort()
return filelist
def GetHandSortedList(self):
# This will be used to fix problem 1, after merging
# single DicomGroups of same study_id and orientation
list_ = self.slices_dict.values()
dicom = list_[0]
axis = ORIENT_MAP[dicom.image.orientation_label]
#list_ = sorted(list_, key = lambda dicom:dicom.image.position[axis])
list_ = sorted(list_, key = lambda dicom:dicom.image.number)
return list_
def UpdateZSpacing(self):
list_ = self.GetHandSortedList()
if (len(list_) > 1):
dicom = list_[0]
axis = ORIENT_MAP[dicom.image.orientation_label]
p1 = dicom.image.position[axis]
dicom = list_[1]
p2 = dicom.image.position[axis]
self.zspacing = abs(p1 - p2)
else:
self.zspacing = 1
def GetDicomSample(self):
size = len(self.slices_dict)
dicom = self.GetHandSortedList()[size/2]
return dicom
class PatientGroup:
def __init__(self):
# key:
# (dicom.patient.name, dicom.patient.id)
self.key = ()
self.groups_dict = {} # group_key: DicomGroup
self.nslices = 0
self.ngroups = 0
self.dicom = None
def AddFile(self, dicom, index=0):
# Given general DICOM information, we group slices according
# to main series information (group_key)
# WARN: This was defined after years of experience
# (2003-2009), so THINK TWICE before changing group_key
# Problem 2 is being fixed by the way this method is
# implemented, dinamically during new dicom's addition
group_key = (dicom.patient.name,
dicom.acquisition.id_study,
dicom.acquisition.serie_number,
dicom.image.orientation_label,
index) # This will be used to deal with Problem 2
if not self.dicom:
self.dicom = dicom
self.nslices += 1
# Does this group exist? Best case ;)
if group_key not in self.groups_dict.keys():
group = DicomGroup()
group.key = group_key
group.title = dicom.acquisition.series_description
group.AddSlice(dicom)
self.ngroups += 1
self.groups_dict[group_key] = group
# Group exists... Lets try to add slice
else:
group = self.groups_dict[group_key]
slice_added = group.AddSlice(dicom)
if not slice_added:
# If we're here, then Problem 2 occured
# TODO: Optimize recursion
self.AddFile(dicom, index+1)
#Getting the spacing in the Z axis
group.UpdateZSpacing()
def Update(self):
# Ideally, AddFile would be sufficient for splitting DICOM
# files into groups (series). However, this does not work for
# acquisitions / equipments and manufacturers.
# Although DICOM is a protocol, each one uses its fields in a
# different manner
# Check if Problem 1 occurs (n groups with 1 slice each)
is_there_problem_1 = False
utils.debug("n slice %d" % self.nslices)
utils.debug("len %d" % len(self.groups_dict))
if (self.nslices == len(self.groups_dict)) and\
(self.nslices > 1):
is_there_problem_1 = True
# Fix Problem 1
if is_there_problem_1:
utils.debug("Problem1")
self.groups_dict = self.FixProblem1(self.groups_dict)
def GetGroups(self):
glist = self.groups_dict.values()
glist = sorted(glist, key = lambda group:group.title,
reverse=True)
return glist
def GetDicomSample(self):
return self.dicom
def FixProblem1(self, dict):
"""
Merge multiple DICOM groups in case Problem 1 (description
above) occurs.
WARN: We've implemented an heuristic to try to solve
the problem. There is no scientific background and this aims
to be a workaround to exams which are not in conformance with
the DICOM protocol.
"""
# Divide existing groups into 2 groups:
dict_final = {} # 1
# those containing "3D photos" and undefined
# orientation - these won't be changed (groups_lost).
dict_to_change = {} # 2
# which can be re-grouped according to our heuristic
# split existing groups in these two types of group, based on
# orientation label
# 1st STEP: RE-GROUP
for group_key in dict:
# values used as key of the new dictionary
dicom = dict[group_key].GetList()[0]
orientation = dicom.image.orientation_label
study_id = dicom.acquisition.id_study
# if axial, coronal or sagittal
if orientation in ORIENT_MAP:
group_key_s = (orientation, study_id)
# If this method was called, there is only one slice
# in this group (dicom)
dicom = dict[group_key].GetList()[0]
if group_key_s not in dict_to_change.keys():
group = DicomGroup()
group.AddSlice(dicom)
dict_to_change[group_key_s] = group
else:
group = dict_to_change[group_key_s]
group.AddSlice(dicom)
else:
dict_final[group_key] = dict[group_key]
# group_counter will be used as key to DicomGroups created
# while checking differences
group_counter = 0
for group_key in dict_to_change:
# 2nd STEP: SORT
sorted_list = dict_to_change[group_key].GetHandSortedList()
# 3rd STEP: CHECK DIFFERENCES
axis = ORIENT_MAP[group_key[0]] # based on orientation
for index in xrange(len(sorted_list)-1):
current = sorted_list[index]
next = sorted_list[index+1]
pos_current = current.image.position[axis]
pos_next = current.image.position[axis]
spacing = current.image.spacing
if (pos_next - pos_current) <= (spacing[2] * 2):
if group_counter in dict_final:
dict_final[group_counter].AddSlice(current)
else:
group = DicomGroup()
group.AddSlice(current)
dict_final[group_counter] = group
#Getting the spacing in the Z axis
group.UpdateZSpacing()
else:
group_counter +=1
group = DicomGroup()
group.AddSlice(current)
dict_final[group_counter] = group
#Getting the spacing in the Z axis
group.UpdateZSpacing()
return dict_final
class DicomPatientGrouper:
# read file, check if it is dicom...
# dicom = dicom.Dicom
# grouper = DicomPatientGrouper()
# grouper.AddFile(dicom)
# ... (repeat to all files on folder)
# grouper.Update()
# groups = GetPatientGroups()
def __init__(self):
self.patients_dict = {}
def AddFile(self, dicom):
patient_key = (dicom.patient.name,
dicom.patient.id)
# Does this patient exist?
if patient_key not in self.patients_dict.keys():
patient = PatientGroup()
patient.key = patient_key
patient.AddFile(dicom)
self.patients_dict[patient_key] = patient
# Patient exists... Lets add group to it
else:
patient = self.patients_dict[patient_key]
patient.AddFile(dicom)
def Update(self):
for patient in self.patients_dict.values():
patient.Update()
def GetPatientsGroups(self):
"""
How to use:
patient_list = grouper.GetPatientsGroups()
for patient in patient_list:
group_list = patient.GetGroups()
for group in group_list:
group.GetList()
# :) you've got a list of dicom.Dicom
# of the same series
"""
plist = self.patients_dict.values()
plist = sorted(plist, key = lambda patient:patient.key[0])
return plist