Commit fb59d2bd2572e55ed5b23fe0e41fb653f1bc5ca7
1 parent
bf95d589
Exists in
master
and in
68 other branches
ADD: Dicom preview class - not working yet
Showing
3 changed files
with
527 additions
and
311 deletions
Show diff stats
.gitattributes
@@ -114,6 +114,7 @@ invesalius/gui/data_notebook.py -text | @@ -114,6 +114,7 @@ invesalius/gui/data_notebook.py -text | ||
114 | invesalius/gui/default_tasks.py -text | 114 | invesalius/gui/default_tasks.py -text |
115 | invesalius/gui/default_viewers.py -text | 115 | invesalius/gui/default_viewers.py -text |
116 | invesalius/gui/dialogs.py -text | 116 | invesalius/gui/dialogs.py -text |
117 | +invesalius/gui/dicom_preview_panel.py -text | ||
117 | invesalius/gui/frame.py -text | 118 | invesalius/gui/frame.py -text |
118 | invesalius/gui/import_data_wizard.py -text | 119 | invesalius/gui/import_data_wizard.py -text |
119 | invesalius/gui/import_panel.py -text | 120 | invesalius/gui/import_panel.py -text |
@@ -0,0 +1,305 @@ | @@ -0,0 +1,305 @@ | ||
1 | +#!/usr/bin/env python | ||
2 | +# -*- coding: UTF-8 -*- | ||
3 | + | ||
4 | +import wx | ||
5 | +import vtk | ||
6 | +import vtkgdcm | ||
7 | + | ||
8 | +from vtk.wx.wxVTKRenderWindowInteractor import wxVTKRenderWindowInteractor | ||
9 | +#from reader import dicom_reader | ||
10 | + | ||
11 | +myEVT_SELECT = wx.NewEventType() | ||
12 | +# This event occurs when the user select a preview | ||
13 | +EVT_SELECT = wx.PyEventBinder(myEVT_SELECT, 1) | ||
14 | + | ||
15 | +myEVT_SELECT_SERIE = wx.NewEventType() | ||
16 | +# This event occurs when the user select a preview | ||
17 | +EVT_SELECT_SERIE = wx.PyEventBinder(myEVT_SELECT_SERIE, 1) | ||
18 | + | ||
19 | +class PreviewEvent(wx.PyCommandEvent): | ||
20 | + def __init__(self , evtType, id): | ||
21 | + wx.PyCommandEvent.__init__(self, evtType, id) | ||
22 | + | ||
23 | + def GetSelectID(self): | ||
24 | + return self.SelectedID | ||
25 | + | ||
26 | + def SetSelectedID(self, id): | ||
27 | + self.SelectedID = id | ||
28 | + | ||
29 | + | ||
30 | +class SerieEvent(PreviewEvent): | ||
31 | + def __init__(self , evtType, id): | ||
32 | + super(SerieEvent, self).__init__(evtType, id) | ||
33 | + | ||
34 | + | ||
35 | +class DicomLoader(object): | ||
36 | + """ | ||
37 | + Responsible for load dicom files. A dictionary like behavior | ||
38 | + """ | ||
39 | + def __init__(self): | ||
40 | + self.loaded_dicoms = {} | ||
41 | + | ||
42 | + def __getitem__(self, filename): | ||
43 | + """ | ||
44 | + Especial method to behave like dictionary | ||
45 | + """ | ||
46 | + try: | ||
47 | + return self.loaded_dicoms[filename] | ||
48 | + except KeyError: | ||
49 | + #print "Except" | ||
50 | + self._load_dicom_files(filename) | ||
51 | + return self.loaded_dicoms[filename] | ||
52 | + | ||
53 | + def _load_dicom_files(self, filename, window=150, level=230): | ||
54 | + reader = vtkgdcm.vtkGDCMImageReader() | ||
55 | + reader.SetFileName(filename) | ||
56 | + imagedata = reader.GetOutput() | ||
57 | + | ||
58 | + scale = imagedata.GetScalarRange() | ||
59 | + | ||
60 | + cast = vtk.vtkImageMapToWindowLevelColors() | ||
61 | + cast.SetInput(imagedata) | ||
62 | + cast.SetWindow(float(window)) | ||
63 | + cast.SetLevel(float(level)) | ||
64 | + | ||
65 | + self.loaded_dicoms[filename] = cast.GetOutput() | ||
66 | + | ||
67 | + | ||
68 | +class Preview(wx.Panel): | ||
69 | + """ | ||
70 | + Where the images will be showed. | ||
71 | + """ | ||
72 | + dicom_loader = DicomLoader() | ||
73 | + def __init__(self, parent): | ||
74 | + super(Preview, self).__init__(parent) | ||
75 | + # Will it be white? | ||
76 | + self.SetBackgroundColour((255, 255, 255)) | ||
77 | + self.sizer = wx.BoxSizer(wx.VERTICAL) | ||
78 | + self.SetSizer(self.sizer) | ||
79 | + self._init_ui() | ||
80 | + self._init_vtk() | ||
81 | + self._bind_events() | ||
82 | + | ||
83 | + def _init_ui(self): | ||
84 | + self.title = wx.StaticText(self, -1, "Image", | ||
85 | + style=wx.ALIGN_CENTER) | ||
86 | + | ||
87 | + self.subtitle = wx.StaticText(self, -1, "Image", | ||
88 | + style=wx.ALIGN_CENTER) | ||
89 | + | ||
90 | + self.interactor = wxVTKRenderWindowInteractor(self, -1, size=(70, 70)) | ||
91 | + | ||
92 | + self.sizer.Add(self.title, 0, wx.ALIGN_CENTER_HORIZONTAL) | ||
93 | + self.sizer.Add(self.subtitle, 0, wx.ALIGN_CENTER_HORIZONTAL) | ||
94 | + self.sizer.Add(self.interactor, 0, wx.ALIGN_CENTER_HORIZONTAL\ | ||
95 | + | wx.ALL, 5) | ||
96 | + | ||
97 | + def _init_vtk(self): | ||
98 | + self.actor = vtk.vtkImageActor() | ||
99 | + | ||
100 | + self.render = vtk.vtkRenderer() | ||
101 | + | ||
102 | + self.interactor.SetInteractorStyle(None) | ||
103 | + self.interactor.GetRenderWindow().AddRenderer(self.render) | ||
104 | + | ||
105 | + self.render.AddActor(self.actor) | ||
106 | + | ||
107 | + | ||
108 | + def _bind_events(self): | ||
109 | + self.Bind( wx.EVT_LEFT_DCLICK, self.OnSelect) | ||
110 | + | ||
111 | + def OnSelect(self, evt): | ||
112 | + evt = PreviewEvent(myEVT_SELECT, self.GetId()) | ||
113 | + evt.SetSelectedID(self.ID) | ||
114 | + self.GetEventHandler().ProcessEvent(evt) | ||
115 | + | ||
116 | + def SetTitle(self, title): | ||
117 | + self.title.SetLabel(title) | ||
118 | + | ||
119 | + def SetSubtitle(self, subtitle): | ||
120 | + self.subtitle.SetLabel(subtitle) | ||
121 | + | ||
122 | + def SetImage(self, image_data): | ||
123 | + """ | ||
124 | + Set a Image to preview. | ||
125 | + """ | ||
126 | + filename, window, level, title, subtitle = image_data | ||
127 | + print image_data | ||
128 | + self.SetTitle(title) | ||
129 | + self.SetSubtitle(subtitle) | ||
130 | + #self.ID = image_file[5] # todo: check if this is necessary | ||
131 | + | ||
132 | + # TODO: enhace interface | ||
133 | + imagedata = Preview.dicom_loader[filename] | ||
134 | + self.actor.SetInput(imagedata) | ||
135 | + self.render.ResetCamera() | ||
136 | + | ||
137 | +class DicomPreviewSeries(wx.Panel): | ||
138 | + """A dicom series preview panel""" | ||
139 | + def __init__(self, parent): | ||
140 | + super(DicomPreviewSeries, self).__init__(parent) | ||
141 | + # TODO: 3 pixels between the previews is a good idea? | ||
142 | + # I have to test. | ||
143 | + self.sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
144 | + self.SetSizer(self.sizer) | ||
145 | + self.displayed_position = 0 | ||
146 | + self._init_ui() | ||
147 | + | ||
148 | + def _init_ui(self): | ||
149 | + self.scroll = wx.ScrollBar(self, style=wx.SB_VERTICAL) | ||
150 | + self.grid = wx.GridSizer(rows=3, cols=5, vgap=3, hgap=3) | ||
151 | + self.sizer.Add(self.grid) | ||
152 | + self.sizer.Add(self.scroll, 0, wx.EXPAND) | ||
153 | + self._Add_Panels_Preview() | ||
154 | + self._bind_events() | ||
155 | + | ||
156 | + def _Add_Panels_Preview(self): | ||
157 | + self.previews = [] | ||
158 | + for i in xrange(3): | ||
159 | + for j in xrange(5): | ||
160 | + p = Preview(self) | ||
161 | + p.Hide() | ||
162 | + self.previews.append(p) | ||
163 | + self.grid.Add(p, i, j) | ||
164 | + | ||
165 | + def _bind_events(self): | ||
166 | + # When the user scrolls the window | ||
167 | + self.Bind(wx.EVT_SCROLL, self.OnScroll) | ||
168 | + self.Bind(EVT_SELECT, self.OnSelect) | ||
169 | + | ||
170 | + def OnSelect(self, evt): | ||
171 | + my_evt = SerieEvent(myEVT_SELECT_SERIE, self.GetId()) | ||
172 | + my_evt.SetSelectedID(evt.GetSelectID()) | ||
173 | + self.GetEventHandler().ProcessEvent(my_evt) | ||
174 | + | ||
175 | + def SetDicomDirectory(self, directory): | ||
176 | + print "SetDicomDirectory" | ||
177 | + self.directory = directory | ||
178 | + self.series = dicom_reader.GetSeries(directory)[0] | ||
179 | + print "keys", [key[0] for key in self.series.keys()] | ||
180 | + | ||
181 | + s = self.series | ||
182 | + for k in s.keys(): | ||
183 | + print "------ PESSOA ---------" | ||
184 | + print "%s (%d series)"%(k[0], len(s[k])-1) | ||
185 | + for ns in range(1,len(s[k])): | ||
186 | + print "------ SERIE ---------" | ||
187 | + print "unnamed" | ||
188 | + print "age %s" %(s[k][ns][8]) | ||
189 | + print "date acquired %s %s" %(s[k][ns][0], s[k][ns][4]) | ||
190 | + print "birthdate %s" %(s[k][ns][23]) | ||
191 | + print "institution %s" %(s[k][ns][6]) | ||
192 | + | ||
193 | + # TODO: I need to improve this | ||
194 | + self.files = [(self.series[i][0][0][8], # Filename | ||
195 | + self.series[i][0][0][12], # Window Level | ||
196 | + self.series[i][0][0][13], # Window Width | ||
197 | + "unnamed", #% (n + 1), # Title | ||
198 | + "%d Images" % len(self.series[i][0]), # Subtitle | ||
199 | + i) for n, i in enumerate(self.series)] | ||
200 | + | ||
201 | + def SetDicomSeries(self, files): | ||
202 | + self.files = files | ||
203 | + scroll_range = len(files)/5 | ||
204 | + if scroll_range * 5 < len(files): | ||
205 | + scroll_range +=1 | ||
206 | + self.scroll.SetScrollbar(0, 3, scroll_range, 5) | ||
207 | + self._display_previews() | ||
208 | + | ||
209 | + def _display_previews(self): | ||
210 | + initial = self.displayed_position * 5 | ||
211 | + final = initial + 15 | ||
212 | + for f, p in zip(self.files[initial:final], self.previews): | ||
213 | + print "--------" | ||
214 | + print "f:", f | ||
215 | + print "p: ", p | ||
216 | + p.SetImage(f) | ||
217 | + p.Show() | ||
218 | + | ||
219 | + def OnScroll(self, evt): | ||
220 | + self.displayed_position = evt.GetPosition() | ||
221 | + [i.Hide() for i in self.previews] | ||
222 | + self._display_previews() | ||
223 | + | ||
224 | + | ||
225 | +class DicomPreview(wx.Panel): | ||
226 | + """A dicom preview panel""" | ||
227 | + def __init__(self, parent): | ||
228 | + super(DicomPreview, self).__init__(parent) | ||
229 | + # TODO: 3 pixels between the previews is a good idea? | ||
230 | + # I have to test. | ||
231 | + self.sizer = wx.BoxSizer(wx.HORIZONTAL) | ||
232 | + self.SetSizer(self.sizer) | ||
233 | + self.displayed_position = 0 | ||
234 | + self.nhidden_last_display = 0 | ||
235 | + self._init_ui() | ||
236 | + | ||
237 | + def _init_ui(self): | ||
238 | + self.scroll = wx.ScrollBar(self, style=wx.SB_VERTICAL) | ||
239 | + self.grid = wx.GridSizer(rows=3, cols=5, vgap=3, hgap=3) | ||
240 | + self.sizer.Add(self.grid) | ||
241 | + self.sizer.Add(self.scroll, 0, wx.EXPAND) | ||
242 | + self._Add_Panels_Preview() | ||
243 | + self._bind_events() | ||
244 | + | ||
245 | + def _Add_Panels_Preview(self): | ||
246 | + self.previews = [] | ||
247 | + for i in xrange(3): | ||
248 | + for j in xrange(5): | ||
249 | + p = Preview(self) | ||
250 | + #p.Hide() | ||
251 | + self.previews.append(p) | ||
252 | + self.grid.Add(p, i, j) | ||
253 | + | ||
254 | + def _bind_events(self): | ||
255 | + # When the user scrolls the window | ||
256 | + self.Bind(wx.EVT_SCROLL, self.OnScroll) | ||
257 | + | ||
258 | + def SetDicomDirectory(self, directory): | ||
259 | + self.directory = directory | ||
260 | + self.series = dicom_reader.GetSeries(directory)[0] | ||
261 | + | ||
262 | + def SetDicomSerie(self, serie): | ||
263 | + k = serie | ||
264 | + self.files = [(i[8], | ||
265 | + i[12], | ||
266 | + i[13], | ||
267 | + "Image %d" % (n + 1), # Title | ||
268 | + "%s"% str(i[3][2]), # Spacing | ||
269 | + n)for n, i in enumerate(self.series[k][0])] | ||
270 | + scroll_range = len(self.files)/5 | ||
271 | + if scroll_range * 5 < len(self.files): | ||
272 | + scroll_range +=1 | ||
273 | + self.scroll.SetScrollbar(0, 3, scroll_range, 5) | ||
274 | + self._display_previews() | ||
275 | + | ||
276 | + def _display_previews(self): | ||
277 | + initial = self.displayed_position * 5 | ||
278 | + final = initial + 15 | ||
279 | + | ||
280 | + if len(self.files) < final: | ||
281 | + for i in xrange(final-len(self.files)): | ||
282 | + try: | ||
283 | + self.previews[-i-1].Hide() | ||
284 | + except IndexError: | ||
285 | + #print "doesn't exist!" | ||
286 | + pass | ||
287 | + self.nhidden_last_display = final-len(self.files) | ||
288 | + else: | ||
289 | + if self.nhidden_last_display: | ||
290 | + for i in xrange(self.nhidden_last_display): | ||
291 | + try: | ||
292 | + self.previews[-i-1].Show() | ||
293 | + except IndexError: | ||
294 | + #print "doesn't exist!" | ||
295 | + pass | ||
296 | + self.nhidden_last_display = 0 | ||
297 | + | ||
298 | + for f, p in zip(self.files[initial:final], self.previews): | ||
299 | + p.SetImage(f) | ||
300 | + p.interactor.Render() | ||
301 | + | ||
302 | + def OnScroll(self, evt): | ||
303 | + if self.displayed_position != evt.GetPosition(): | ||
304 | + self.displayed_position = evt.GetPosition() | ||
305 | + self._display_previews() |
invesalius/reader/dicom_grouper.py
@@ -19,340 +19,250 @@ | @@ -19,340 +19,250 @@ | ||
19 | # detalhes. | 19 | # detalhes. |
20 | #--------------------------------------------------------------------- | 20 | #--------------------------------------------------------------------- |
21 | 21 | ||
22 | -import dicom | ||
23 | 22 | ||
24 | -class DicomGroups: | ||
25 | - """ | ||
26 | - It is possible to separate sets of a set | ||
27 | - of files dicom. | ||
28 | - | ||
29 | - To use: | ||
30 | - list_dicoms = [c:\a.dcm, c:\a1.gdcm] | ||
31 | - dicom_splitter = DicomGroups() | ||
32 | - dicom_splitter.SetFileList(list_dicoms) | ||
33 | - dicom_splitter.Update() | ||
34 | - splitted = dicom_splitter.GetOutput() | ||
35 | - """ | ||
36 | - | ||
37 | - def __init__(self): | ||
38 | - self.parser = dicom.Parser() | ||
39 | - #List of DICOM from Directory | ||
40 | - self.filenamelist = [] | ||
41 | - # List of DICOM with Informations | ||
42 | - self.filelist = [] | ||
43 | - #is output | ||
44 | - self.groups_dcm = {} | ||
45 | - #It is the kind of group that was used. | ||
46 | - self.split_type = 0 | ||
47 | - | ||
48 | - def SetFileList(self, filenamelist): | ||
49 | - """ | ||
50 | - Input is a list with the complete address | ||
51 | - of each DICOM. | ||
52 | - """ | ||
53 | - self.filenamelist = filenamelist | ||
54 | - | ||
55 | - def Update(self): | ||
56 | - """ | ||
57 | - Trigger processing group of series | ||
58 | - """ | ||
59 | - | ||
60 | - self.__ParseFiles() | ||
61 | - | ||
62 | - self.__Split1() | ||
63 | - | ||
64 | - if (len(self.GetOutput().keys()) == len(self.filenamelist)): | ||
65 | - self.__Split2() | ||
66 | - self.split_type = 1 | ||
67 | - | ||
68 | - self.__Split3() | ||
69 | - | ||
70 | - self.__UpdateZSpacing() | ||
71 | - | ||
72 | - | ||
73 | - def GetOutput(self): | ||
74 | - """ | ||
75 | - Returns a dictionary with groups | ||
76 | - of DICOM. | ||
77 | - """ | ||
78 | - return self.groups_dcm | 23 | +# --------------------------------------------------------- |
24 | +# PROBLEM 1 | ||
25 | +# There are times when there are lots of groups on dict, but | ||
26 | +# each group contains only one slice (DICOM file). | ||
27 | +# | ||
28 | +# Equipments / manufacturer: | ||
29 | +# TODO | ||
30 | +# | ||
31 | +# Cases: | ||
32 | +# TODO 0031, 0056, 1093 | ||
33 | +# | ||
34 | +# What occurs in these cases: | ||
35 | +# <dicom.image.number> and <dicom.acquisition.series_number> | ||
36 | +# were swapped | ||
79 | 37 | ||
80 | - def GetSplitterType(self): | ||
81 | - """ | ||
82 | - Return Integer with the SplitterType | ||
83 | - 0 option used the name of patient information, | ||
84 | - id of the study, number of series and orientation | ||
85 | - of the image (AXIAL, SAGITAL and CORONAL). | ||
86 | - 1 option was checked is used to splitted the if distance | ||
87 | - of the images (tag image position) is more 2x Thickness. | ||
88 | - """ | ||
89 | - return self.split_type | ||
90 | 38 | ||
91 | - def __ParseFiles(self): | ||
92 | - """ | ||
93 | - It generates a list with information | ||
94 | - concerning dicom files within a directory. | ||
95 | - The Input is a List containing andress of | ||
96 | - the DICOM | ||
97 | - """ | ||
98 | - filenamelist = self.filenamelist | ||
99 | - filelist = self.filelist | ||
100 | - parser = self.parser | ||
101 | - for x in xrange(len(filenamelist)): | 39 | +# ----------------------------------------------------------- |
40 | +# PROBLEM 2 | ||
41 | +# Two slices (DICOM file) inside a group have the same | ||
42 | +# position. | ||
43 | +# | ||
44 | +# Equipments / manufacturer: | ||
45 | +# TODO | ||
46 | +# | ||
47 | +# Cases: | ||
48 | +# TODO 0031, 0056, 1093 | ||
49 | +# | ||
50 | +# What occurs in these cases: | ||
51 | +# <dicom.image.number> and <dicom.acquisition.series_number> | ||
52 | +# were swapped | ||
102 | 53 | ||
103 | - if not parser.SetFileName(filenamelist[x]): | ||
104 | - return None | 54 | +ORIENT_MAP = {"SAGITTAL":0, "AXIAL":1, "CORONAL":2} |
105 | 55 | ||
106 | - information = dicom.Dicom() | ||
107 | - information.SetParser(parser) | ||
108 | 56 | ||
109 | - self.filelist.append(information) | ||
110 | - self.filelist = filelist | ||
111 | - | 57 | +class DicomGroup: |
58 | + def __init__(self): | ||
59 | + # key: | ||
60 | + # (dicom.patient.name, dicom.acquisition.id_study, | ||
61 | + # dicom.acquisition.series_number, | ||
62 | + # dicom.image.orientation_label, index) | ||
63 | + self.key = () | ||
64 | + self.slices_dict = {} # slice_position: Dicom.dicom | ||
65 | + # IDEA (13/10): Represent internally as dictionary, | ||
66 | + # externally as list | ||
67 | + self.nslices = 0 | ||
68 | + | ||
69 | + def AddSlice(self, dicom): | ||
70 | + pos = dicom.image.position | ||
71 | + if pos not in self.slices_dict.keys(): | ||
72 | + self.slices_dict[pos] = dicom | ||
73 | + self.nslices += 1 | ||
74 | + return True | ||
75 | + else: | ||
76 | + return False | ||
77 | + | ||
78 | + def GetList(self): | ||
79 | + # Should be called when user selects this group | ||
80 | + # This list will be used to create the vtkImageData | ||
81 | + # (interpolated) | ||
82 | + return self.slices_dict.values() | ||
83 | + | ||
84 | + def GetSortedList(self): | ||
85 | + # This will be used to fix problem 1, after merging | ||
86 | + # single DicomGroups of same study_id and orientation | ||
87 | + list_ = self.slices_dict_values() | ||
88 | + axis = ORIENT_MAP[self.key[3]] | ||
89 | + list_ = sorted(list_, key = lambda dicom:dicom.image.position[axis]) | ||
90 | + return list_ | ||
91 | + | ||
92 | + | ||
93 | +class PatientGroup: | ||
94 | + def __init__(self): | ||
95 | + # key: | ||
96 | + # (dicom.patient.name, dicom.patient.id) | ||
97 | + self.key = () | ||
98 | + self.groups_dict = {} # group_key: DicomGroup | ||
99 | + self.slices = 0 | ||
100 | + | ||
101 | + def AddFile(self, dicom, index=0): | ||
102 | + # Given general DICOM information, we group slices according | ||
103 | + # to main series information (group_key) | ||
104 | + | ||
105 | + # WARN: This was defined after years of experience | ||
106 | + # (2003-2009), so THINK TWICE before changing group_key | ||
107 | + | ||
108 | + # Problem 2 is being fixed by the way this method is | ||
109 | + # implemented, dinamically during new dicom's addition | ||
110 | + group_key = (dicom.patient.name, | ||
111 | + dicom.acquisition.id_study, | ||
112 | + dicom.acquisition.series_number, | ||
113 | + dicom.image.orientation_label, | ||
114 | + index) # This will be used to deal with Problem 2 | ||
112 | 115 | ||
113 | - def __GetInformations(self, ind): | ||
114 | - """ | ||
115 | - Return a list referring to a specific DICOM | ||
116 | - in dictionary. In some cases it is necessary | ||
117 | - to pass only the index | ||
118 | - """ | ||
119 | - filelist = self.filelist | ||
120 | - return filelist[ind] | ||
121 | - | 116 | + # Does this group exist? Best case ;) |
117 | + if group_key not in self.groups_dict.keys(): | ||
118 | + group = DicomGroup() | ||
119 | + group.AddSlice(dicom) | ||
120 | + self.groups_dict[group_key] = group | ||
121 | + # Group exists... Lets try to add slice | ||
122 | + else: | ||
123 | + group = self.groups_dicom[group_key] | ||
124 | + slice_added = group.AddSlice(dicom) | ||
125 | + if not slice_added: | ||
126 | + # If we're here, then Problem 2 occured | ||
127 | + # TODO: Optimize recursion | ||
128 | + self.AddFile(file_path, index+1) | ||
122 | 129 | ||
123 | - def __Split1(self): | ||
124 | - """ | ||
125 | - Bring together the series under the name of | ||
126 | - the patient, id of the study, serial number | ||
127 | - and label orientation of the image. | ||
128 | - """ | ||
129 | - groups_dcm = self.groups_dcm | ||
130 | - | ||
131 | - for x in xrange(len(self.filelist)): | ||
132 | - information = self.__GetInformations(x) | 130 | + def Update(self): |
131 | + # Ideally, AddFile would be sufficient for splitting DICOM | ||
132 | + # files into groups (series). However, this does not work for | ||
133 | + # acquisitions / equipments and manufacturers. | ||
133 | 134 | ||
134 | - key = (information.patient.name, information.acquisition.id_study,\ | ||
135 | - information.acquisition.serie_number, information.image.orientation_label) | 135 | + # Although DICOM is a protocol, each one uses its fields in a |
136 | + # different manner | ||
136 | 137 | ||
137 | - if (key in groups_dcm.keys()): | ||
138 | - groups_dcm[key].append(information) | ||
139 | - else: | ||
140 | - groups_dcm[key] = [information] | 138 | + # Check if Problem 1 occurs (n groups with 1 slice each) |
139 | + is_there_problem_1 = False | ||
140 | + if (self.ndicom == len(self.groups_dicom)) and\ | ||
141 | + (self.ndicom > 1): | ||
142 | + is_there_problem_1 = True | ||
141 | 143 | ||
142 | - self.groups_dcm = groups_dcm | 144 | + # Fix Problem 1 |
145 | + if is_there_problem_1: | ||
146 | + self.groups_dict = self.FixProblem1(self.groups_dict) | ||
143 | 147 | ||
148 | + def GetGroups(self): | ||
149 | + return self.groups_dict.values() | ||
144 | 150 | ||
145 | - def __Split2(self): | 151 | + def FixProblem1(self, dict): |
146 | """ | 152 | """ |
147 | - Separate according to the difference of the current | ||
148 | - share with the next. | ||
149 | - If the else them is higher than the z axis | ||
150 | - multiplied by two. | 153 | + Merge multiple DICOM groups in case Problem 1 (description |
154 | + above) occurs. | ||
155 | + | ||
156 | + WARN: We've implemented an heuristic to try to solve | ||
157 | + the problem. There is no scientific background and this aims | ||
158 | + to be a workaround to exams which are not in conformance with | ||
159 | + the DICOM protocol. | ||
151 | """ | 160 | """ |
152 | - self.groups_dcm = {} | ||
153 | - cont_series = 0 | ||
154 | - groups_dcm = self.groups_dcm | ||
155 | - | ||
156 | - #Through in the series (index of the dictionary) | ||
157 | - for x in xrange(len(self.filelist)): | ||
158 | - #Slices through in the serie | ||
159 | - information = self.__GetInformations(x) | ||
160 | - key = (information.patient.name, cont_series) | ||
161 | - | ||
162 | - #If there exists only one slice. | ||
163 | - if (len(self.filelist) > 1): | ||
164 | - #If the number of slices in the series is | ||
165 | - #less than the all number of slices. | ||
166 | - #It is necessary to whether and the | ||
167 | - #last slice. | ||
168 | - if ((x < len(self.filelist) and (x != len(self.filelist) - 1))): | ||
169 | - #position of next slice | ||
170 | - information = self.__GetInformations(x + 1) | ||
171 | - image_position_prox = information.image.position | ||
172 | - print image_position_prox | ||
173 | - else: | ||
174 | - #slice up the position. | ||
175 | - information = self.__GetInformations(x - 1) | ||
176 | - image_position_prox = information.image.position | ||
177 | - #According to the orientation of the image subtraction | ||
178 | - #will be between a specific position in the vector of positions. | ||
179 | - image_orientation_label = information.image.orientation_label | ||
180 | - image_position = information.image.position | ||
181 | - if(image_orientation_label == "SAGITTAL"): | ||
182 | - dif_image_position = image_position_prox[0] - image_position[0] | ||
183 | - | ||
184 | - elif (image_orientation_label == "AXIAL"): | ||
185 | - dif_image_position = image_position_prox[1] - image_position[1] | ||
186 | - else: | ||
187 | - dif_image_position = image_position_prox[2] - image_position[2] | ||
188 | - | ||
189 | - #If the difference in the positions is less than the | ||
190 | - #spacing z-axis (thickness) multiplied by two. | ||
191 | - #Otherwise this key create and add value | ||
192 | - spacing = information.image.spacing | ||
193 | - if ((dif_image_position) <= spacing[2] * 2): | ||
194 | - #If there is already such a key in the dictionary, | ||
195 | - #only adds value. Otherwise this key create in the | ||
196 | - #dictionary and add value | ||
197 | - if (key in groups_dcm.keys()): | ||
198 | - groups_dcm[key].append(information) | ||
199 | - else: | ||
200 | - groups_dcm[key] = [information] | 161 | + # Divide existing groups into 2 groups: |
162 | + dict_final = {} # 1 | ||
163 | + # those containing "3D photos" and undefined | ||
164 | + # orientation - these won't be changed (groups_lost). | ||
165 | + | ||
166 | + dict_to_change = {} # 2 | ||
167 | + # which can be re-grouped according to our heuristic | ||
168 | + | ||
169 | + # split existing groups in these two types of group, based on | ||
170 | + # orientation label | ||
171 | + | ||
172 | + # 1st STEP: RE-GROUP | ||
173 | + for group_key in dict: | ||
174 | + # values used as key of the new dictionary | ||
175 | + orientation = dict[group_key].image.orientation_label | ||
176 | + study_id = dict[group_key].acquisition.id_study | ||
177 | + # if axial, coronal or sagittal | ||
178 | + if orientation in ORIENT_MAP: | ||
179 | + group_key_s = (orientation, study_id) | ||
180 | + # If this method was called, there is only one slice | ||
181 | + # in this group (dicom) | ||
182 | + dicom = dict[group_key].GetList()[0] | ||
183 | + if group_key_s not in dict_to_change.keys(): | ||
184 | + group = DicomGroup() | ||
185 | + group.AddSlice(dicom) | ||
186 | + dict_to_change[group_key_s] = group | ||
201 | else: | 187 | else: |
202 | - cont_series = cont_series + 1 | ||
203 | - groups_dcm[key] = [information] | ||
204 | - | 188 | + group = dict_to_change[group_key_s] |
189 | + group.AddSlice(dicom) | ||
205 | else: | 190 | else: |
206 | - | ||
207 | - if (cont_series in groups_dcm.keys()): | ||
208 | - groups_dcm[key].append(information) | ||
209 | - else: | ||
210 | - groups_dcm[key] = [information] | ||
211 | - | ||
212 | - cont_series = cont_series + 1 | ||
213 | - | ||
214 | - self.groups_dcm = groups_dcm | ||
215 | - | ||
216 | - | ||
217 | - def __Split3(self): | ||
218 | - """ | ||
219 | - Separate the slice with the positions | ||
220 | - repeated. | ||
221 | - """ | ||
222 | - groups_dcm = self.groups_dcm | ||
223 | - groups_dcm_ = {} | ||
224 | - size_groups = len(groups_dcm.keys()) | ||
225 | - tmp1 = {} | ||
226 | - tmp_list = [] | ||
227 | - | ||
228 | - #goes according to the serial number | ||
229 | - #already separated. | ||
230 | - for n in xrange(size_groups): | ||
231 | - | ||
232 | - #Key of dictionary | ||
233 | - key = groups_dcm.keys()[n] | ||
234 | - | ||
235 | - #Number of slices in the series | ||
236 | - size_list = len(groups_dcm[key]) | ||
237 | - | ||
238 | - for y in xrange(size_list): | ||
239 | - | ||
240 | - #Slices walks in the series | ||
241 | - information = groups_dcm[key][y] | ||
242 | - | ||
243 | - #Generate new key to dictionary | ||
244 | - image_pos = information.image.position | ||
245 | - key_ = (image_pos[0], image_pos[1], image_pos[2]) | ||
246 | - | ||
247 | - #Add informations in the list | ||
248 | - list = [information] | ||
249 | - | ||
250 | - #If list Null, create dictionary | ||
251 | - #and add list with information | ||
252 | - #after add in a temporary list | ||
253 | - if (tmp_list == []): | ||
254 | - tmp = {} | ||
255 | - tmp[key_] = information | ||
256 | - tmp_list.append(tmp) | ||
257 | - | 191 | + dict_final[group_key] = dict[group_key] |
192 | + | ||
193 | + # group_counter will be used as key to DicomGroups created | ||
194 | + # while checking differences | ||
195 | + group_counter = 0 | ||
196 | + for group_key in dict_to_change: | ||
197 | + # 2nd STEP: SORT | ||
198 | + sorted_list = dict_to_change[group_key].GetSortedList() | ||
199 | + | ||
200 | + # 3rd STEP: CHECK DIFFERENCES | ||
201 | + axis = ORIENT_MAP[group_key[0]] # based on orientation | ||
202 | + for index in xrange(len(sorted_list)-1): | ||
203 | + current = sorted_list[index] | ||
204 | + next = sorted_list[index+1] | ||
205 | + | ||
206 | + pos_current = current.image.position[axis] | ||
207 | + pos_next = current.image.position[axis] | ||
208 | + spacing = current.image.spacing | ||
209 | + | ||
210 | + if (pos_next - pos_current) <= (spacing[2] * 2): | ||
211 | + if group_counter in dict_final: | ||
212 | + dict_final[group_counter].AddSlice(current) | ||
213 | + else: | ||
214 | + group = DicomGroup() | ||
215 | + group.AddSlice(current) | ||
216 | + dict_final[group_counter] = group | ||
258 | else: | 217 | else: |
259 | - b = len(tmp_list) | ||
260 | - a = 0 | ||
261 | - #flag is to control when be necessary | ||
262 | - #to create another position in the list. | ||
263 | - flag = 0 | ||
264 | - | ||
265 | - while a < b: | ||
266 | - #if there is to share the same | ||
267 | - #position create new key in the | ||
268 | - #dictionary | 218 | + group_counter +=1 |
219 | + group = DicomGroup() | ||
220 | + group.AddSlice(current) | ||
221 | + dict_final[group_counter] = group | ||
269 | 222 | ||
270 | - if not (key_ in (tmp_list[a]).keys()): | ||
271 | - (tmp_list[a])[key_] = information | ||
272 | - flag = 1 | ||
273 | - a = a + 1 | 223 | + return dict_final |
274 | 224 | ||
275 | - if (flag == 0): | ||
276 | - tmp = {} | ||
277 | - tmp[key_] = information | ||
278 | 225 | ||
279 | - tmp_list.append(tmp) | 226 | +class DicomPatientGrouper: |
227 | + # read file, check if it is dicom... | ||
228 | + # dicom = dicom.Dicom | ||
229 | + # grouper = DicomPatientGrouper() | ||
230 | + # grouper.AddFile(dicom) | ||
231 | + # ... (repeat to all files on folder) | ||
232 | + # grouper.Update() | ||
233 | + # groups = GetPatientGroups() | ||
280 | 234 | ||
281 | - | ||
282 | - #for each item on the list is created | ||
283 | - #a new position in the dictionary. | ||
284 | - size_tmp_list = len(tmp_list) | 235 | + def __init__(self): |
236 | + self.patients_dict = {} | ||
237 | + | ||
238 | + def AddFile(self, dicom): | ||
239 | + patient_key = (dicom.patient.name, | ||
240 | + dicom.patient.id) | ||
241 | + | ||
242 | + # Does this patient exist? | ||
243 | + if patient_key not in self.patients_dict.keys(): | ||
244 | + patient = PatientGroup() | ||
245 | + patient.AddSlice(dicom) | ||
246 | + self.patients_dict[patient_key] = patient | ||
247 | + # Patient exists... Lets add group to it | ||
248 | + else: | ||
249 | + patient = self.patients_dict[patient_key] | ||
250 | + patient.AddFile(dicom) | ||
285 | 251 | ||
286 | - for x in xrange(size_tmp_list): | ||
287 | - | ||
288 | - tmp1 = tmp_list[x] | ||
289 | - for m in xrange(len(tmp1.keys())): | ||
290 | - | ||
291 | - key = tmp1.keys()[m] | ||
292 | - information = tmp1[key] | ||
293 | - | ||
294 | - #new_key = (x,information.patient.name, information.image.orientation_label, | ||
295 | - # information.acquisition.serie_number) | ||
296 | - | ||
297 | - new_key = (information.patient.name, None, x, | ||
298 | - information.image.orientation_label) | ||
299 | - | ||
300 | - if (new_key in groups_dcm_.keys()): | ||
301 | - groups_dcm_[new_key].append(information) | ||
302 | - else: | ||
303 | - groups_dcm_[new_key] = [information] | ||
304 | - | ||
305 | - #the number of previously existing number is | ||
306 | - #greater or equal then the group keeps up, | ||
307 | - #but maintains the same group of positions. | ||
308 | - if len(self.groups_dcm.keys()) < len(groups_dcm_.keys()): | ||
309 | - self.groups_dcm = groups_dcm_ | ||
310 | - | ||
311 | - for j in xrange(len(self.groups_dcm.keys())): | ||
312 | - key = self.groups_dcm.keys()[j] | ||
313 | - self.groups_dcm[key].sort(key=lambda x: x.image.number) | ||
314 | - | ||
315 | - | ||
316 | - def __UpdateZSpacing(self): | 252 | + def Update(self): |
253 | + for patient in self.patients_dict.values(): | ||
254 | + patient.Update() | ||
255 | + | ||
256 | + def GetPatientsGroups(self): | ||
317 | """ | 257 | """ |
318 | - Calculate Z spacing from slices | 258 | + How to use: |
259 | + patient_list = grouper.GetPatientsGroups() | ||
260 | + for patient in patient_list: | ||
261 | + group_list = patient.GetGroups() | ||
262 | + for group in group_list: | ||
263 | + group.GetList() | ||
264 | + # :) you've got a list of dicom.Dicom | ||
265 | + # of the same series | ||
319 | """ | 266 | """ |
267 | + return self.patients_dict.values() | ||
320 | 268 | ||
321 | - for x in xrange(len(self.groups_dcm.keys())): | ||
322 | - | ||
323 | - key = self.groups_dcm.keys()[x] | ||
324 | - information = self.groups_dcm[key][0] | ||
325 | - if (len(self.groups_dcm[key]) > 1): | ||
326 | - #Catch a slice of middle and the next to find the spacing. | ||
327 | - center = len(self.groups_dcm[key])/2 | ||
328 | - if (center == 1): | ||
329 | - center = 0 | ||
330 | - | ||
331 | - information = self.groups_dcm[key][center] | ||
332 | - current_position = information.image.position | ||
333 | - | ||
334 | - information = self.groups_dcm[key][center + 1] | ||
335 | - next_position = information.image.position | ||
336 | - | ||
337 | - try: | ||
338 | - information = self.groups_dcm[self.groups_dcm.keys()[x]][3] | ||
339 | - image_orientation_label = information.image.orientation_label | ||
340 | - | ||
341 | - except(IndexError): | ||
342 | - image_orientation_label = None | ||
343 | - | ||
344 | - if(image_orientation_label == "SAGITTAL"): | ||
345 | - spacing = current_position[0] - next_position[0] | ||
346 | - elif(image_orientation_label == "CORONAL"): | ||
347 | - spacing = current_position[1] - next_position[1] | ||
348 | - else: | ||
349 | - spacing = current_position[2] - next_position[2] | ||
350 | - | ||
351 | - spacing = abs(spacing) | ||
352 | - | ||
353 | - else: | ||
354 | - spacing = None | ||
355 | - | ||
356 | - for information in self.groups_dcm[key]: | ||
357 | - if information.image.spacing: | ||
358 | - information.image.spacing[2] = spacing |