Commit 3174c58fe17f8eb04ad9817ad20cce8b8cde4896
1 parent
63ad0eb3
Exists in
master
and in
68 other branches
ADD: Preview DICOM in import panel (still working)
Showing
4 changed files
with
151 additions
and
246 deletions
Show diff stats
invesalius/control.py
... | ... | @@ -39,9 +39,12 @@ class Controller(): |
39 | 39 | |
40 | 40 | # retrieve DICOM files splited into groups |
41 | 41 | patient_series = dcm.GetDicomGroups(path) |
42 | - ps.Publisher().sendMessage("Load import panel", patient_series) | |
43 | - first_patient = patient_series[0] | |
44 | - #ps.Publisher().sendMessage("Load dicom preview", first_patient) | |
42 | + if patient_series: | |
43 | + ps.Publisher().sendMessage("Load import panel", patient_series) | |
44 | + first_patient = patient_series[0] | |
45 | + ps.Publisher().sendMessage("Load dicom preview", first_patient) | |
46 | + else: | |
47 | + print "No DICOM files on directory" | |
45 | 48 | |
46 | 49 | def OnImportMedicalImages(self, pubsub_evt): |
47 | 50 | directory = pubsub_evt.data |
... | ... | @@ -144,103 +147,6 @@ class Controller(): |
144 | 147 | |
145 | 148 | return imagedata, dicom |
146 | 149 | |
147 | - def ImportDirectory(self, pubsub_evt=None, dir_=None): | |
148 | - """ | |
149 | - Import medical images (if any) and generate vtkImageData, saving data | |
150 | - inside Project instance. | |
151 | - """ | |
152 | - | |
153 | - if not dir_: | |
154 | - dir_ = pubsub_evt.data | |
155 | - | |
156 | - # Select medical images from directory and generate vtkImageData | |
157 | - output = dcm.LoadImages(dir_) | |
158 | - proj = prj.Project() | |
159 | - proj.name = "Untitled" | |
160 | - | |
161 | - if output: | |
162 | - # In this case, there were DICOM files on the folder | |
163 | - imagedata, dicom = output | |
164 | - | |
165 | - # Set orientation | |
166 | - orientation = dicom.image.orientation_label | |
167 | - if (orientation == "CORONAL"): | |
168 | - orientation = const.CORONAL | |
169 | - elif(orientation == "SAGITTAL"): | |
170 | - orientation = const.SAGITAL | |
171 | - else: | |
172 | - orientation = const.AXIAL | |
173 | - | |
174 | - # Retrieve window, level, modalit | |
175 | - window = float(dicom.image.window) | |
176 | - level = float(dicom.image.level) | |
177 | - acquisition_modality = dicom.acquisition.modality | |
178 | - | |
179 | - # If there was gantry tilt, fix it: | |
180 | - tilt_value = dicom.acquisition.tilt | |
181 | - if (tilt_value): | |
182 | - # Tell user gantry tilt and fix, according to answer | |
183 | - message = "Fix gantry tilt applying the degrees bellow" | |
184 | - value = -1*tilt_value | |
185 | - tilt_value = dialog.ShowNumberDialog(message, value) | |
186 | - imagedata = utils.FixGantryTilt(imagedata, tilt_value) | |
187 | - else: | |
188 | - "No DICOM files were found. Trying to read with ITK..." | |
189 | - imagedata = analyze.ReadDirectory(dir_) | |
190 | - if imagedata: | |
191 | - acquisition_modality = "MRI" | |
192 | - | |
193 | - #TODO: Verify if all Analyse is AXIAL orientation | |
194 | - orientation = const.AXIAL | |
195 | - | |
196 | - proj.SetAcquisitionModality(acquisition_modality) | |
197 | - proj.imagedata = imagedata | |
198 | - proj.original_orientation = orientation | |
199 | - threshold_range = proj.imagedata.GetScalarRange() | |
200 | - proj.window = window = threshold_range[1] - threshold_range[0] | |
201 | - proj.level = level = (0.5 * (threshold_range[1] + threshold_range[0])) | |
202 | - | |
203 | - ps.Publisher().sendMessage('Update window level value',\ | |
204 | - (proj.window, proj.level)) | |
205 | - | |
206 | - if not imagedata: | |
207 | - print "Sorry, but there are no medical images supported on this dir." | |
208 | - else: | |
209 | - # Create new project | |
210 | - proj.SetAcquisitionModality(acquisition_modality) | |
211 | - proj.imagedata = imagedata | |
212 | - proj.original_orientation = orientation | |
213 | - proj.window = window | |
214 | - proj.level = level | |
215 | - | |
216 | - threshold_range = proj.imagedata.GetScalarRange() | |
217 | - | |
218 | - const.WINDOW_LEVEL['Default'] = (window, level) | |
219 | - const.WINDOW_LEVEL['Manual'] = (window, level) | |
220 | - | |
221 | - const.THRESHOLD_OUTVALUE = threshold_range[0] | |
222 | - const.THRESHOLD_INVALUE = threshold_range[1] | |
223 | - | |
224 | - # Based on imagedata, load data to GUI | |
225 | - ps.Publisher().sendMessage('Load slice to viewer', (imagedata)) | |
226 | - | |
227 | - # TODO: where to insert!!! | |
228 | - self.LoadImagedataInfo() | |
229 | - | |
230 | - #Initial Window and Level | |
231 | - ps.Publisher().sendMessage('Bright and contrast adjustment image',\ | |
232 | - (proj.window, proj.level)) | |
233 | - | |
234 | - ps.Publisher().sendMessage('Update window level value',\ | |
235 | - (proj.window, proj.level)) | |
236 | - | |
237 | - # Call frame so it shows slice and volume related panels | |
238 | - ps.Publisher().sendMessage('Show content panel') | |
239 | - | |
240 | - ps.Publisher().sendMessage('Update AUI') | |
241 | - | |
242 | - ps.Publisher().sendMessage('Load slice plane') | |
243 | - | |
244 | 150 | def LoadImagedataInfo(self): |
245 | 151 | proj = prj.Project() |
246 | 152 | ... | ... |
invesalius/gui/dicom_preview_panel.py
1 | 1 | #!/usr/bin/env python |
2 | 2 | # -*- coding: UTF-8 -*- |
3 | 3 | |
4 | +#TODO: To create a beautiful API | |
4 | 5 | import wx |
5 | 6 | import vtk |
6 | 7 | import vtkgdcm |
7 | 8 | |
8 | 9 | from vtk.wx.wxVTKRenderWindowInteractor import wxVTKRenderWindowInteractor |
9 | -#from reader import dicom_reader | |
10 | +from reader import dicom_reader | |
10 | 11 | |
11 | 12 | myEVT_SELECT = wx.NewEventType() |
12 | 13 | # This event occurs when the user select a preview |
... | ... | @@ -31,66 +32,10 @@ class SerieEvent(PreviewEvent): |
31 | 32 | def __init__(self , evtType, id): |
32 | 33 | super(SerieEvent, self).__init__(evtType, id) |
33 | 34 | |
34 | -class DicomImageData(object): | |
35 | - def __init__(self): | |
36 | - pass | |
37 | - | |
38 | - def SetInput(self, dicom): | |
39 | - reader = vtkgdcm.vtkGDCMImageReader() | |
40 | - reader.SetFileName(dicom.image.file) | |
41 | - imagedata = reader.GetOutput() | |
42 | - | |
43 | - scale = imagedata.GetScalarRange() | |
44 | - | |
45 | - cast = vtk.vtkImageMapToWindowLevelColors() | |
46 | - cast.SetInput(imagedata) | |
47 | - cast.SetWindow(float(dicom.image.window)) | |
48 | - cast.SetLevel(float(dicom.image.level)) | |
49 | - | |
50 | - self.imagedata = cast.GetOutput() | |
51 | - | |
52 | - def GetOutput(self): | |
53 | - return self.imagedata | |
54 | - | |
55 | - | |
56 | -class DicomLoader(object): | |
57 | - """ | |
58 | - Responsible for load dicom files. A dictionary like behavior | |
59 | - """ | |
60 | - def __init__(self): | |
61 | - self.loaded_dicoms = {} | |
62 | - | |
63 | - def __getitem__(self, filename): | |
64 | - """ | |
65 | - Especial method to behave like dictionary | |
66 | - """ | |
67 | - try: | |
68 | - return self.loaded_dicoms[filename] | |
69 | - except KeyError: | |
70 | - #print "Except" | |
71 | - self._load_dicom_files(filename) | |
72 | - return self.loaded_dicoms[filename] | |
73 | - | |
74 | - def _load_dicom_files(self, filename, window=150, level=230): | |
75 | - reader = vtkgdcm.vtkGDCMImageReader() | |
76 | - reader.SetFileName(filename) | |
77 | - imagedata = reader.GetOutput() | |
78 | - | |
79 | - scale = imagedata.GetScalarRange() | |
80 | - | |
81 | - cast = vtk.vtkImageMapToWindowLevelColors() | |
82 | - cast.SetInput(imagedata) | |
83 | - cast.SetWindow(float(window)) | |
84 | - cast.SetLevel(float(level)) | |
85 | - | |
86 | - self.loaded_dicoms[filename] = cast.GetOutput() | |
87 | - | |
88 | - | |
89 | 35 | class Preview(wx.Panel): |
90 | 36 | """ |
91 | 37 | Where the images will be showed. |
92 | 38 | """ |
93 | - dicom_loader = DicomLoader() | |
94 | 39 | def __init__(self, parent): |
95 | 40 | super(Preview, self).__init__(parent) |
96 | 41 | # Will it be white? |
... | ... | @@ -140,29 +85,40 @@ class Preview(wx.Panel): |
140 | 85 | def SetSubtitle(self, subtitle): |
141 | 86 | self.subtitle.SetLabel(subtitle) |
142 | 87 | |
143 | - def SetGroup(self, group): | |
144 | - self.SetTitle(group.title) | |
145 | - self.SetSubtitle("%d images"%(group.nslices)) | |
146 | - d = DicomImageData() | |
147 | - d.SetInput(group.dicom) | |
148 | - imagedata = d.GetOutput() | |
149 | - self.actor.SetInput(imagedata) | |
150 | - self.render.ResetCamera() | |
151 | - | |
152 | - def SetImage(self, image_data): | |
88 | + def SetImage(self, image_file): | |
153 | 89 | """ |
154 | 90 | Set a Image to preview. |
155 | 91 | """ |
156 | - filename, window, level, title, subtitle = image_data | |
157 | - print image_data | |
158 | - self.SetTitle(title) | |
159 | - self.SetSubtitle(subtitle) | |
160 | - #self.ID = image_file[5] # todo: check if this is necessary | |
92 | + self.SetTitle(image_file[3]) | |
93 | + self.SetSubtitle(image_file[4]) | |
94 | + self.ID = image_file[5] | |
95 | + | |
96 | + image_reader = vtkgdcm.vtkGDCMImageReader() | |
97 | + image_reader.SetFileName(image_file[0]) | |
98 | + image = image_reader.GetOutput() | |
99 | + | |
100 | + scale = image.GetScalarRange() | |
101 | + | |
102 | + cast = vtk.vtkImageMapToWindowLevelColors() | |
103 | + #cast.SetShift(abs(scale[0])) | |
104 | + #cast.SetScale(255.0/(scale[1] - scale[0])) | |
105 | + #cast.ClampOverflowOn() | |
106 | + cast.SetInput(image) | |
107 | + #cast.SetOutputScalarTypeToUnsignedChar() | |
108 | + try: | |
109 | + window = float(image_file[1]) | |
110 | + level = float(image_file[2]) | |
111 | + except TypeError: | |
112 | + #TODO: These values are good? | |
113 | + level = 230 | |
114 | + window = 150 | |
161 | 115 | |
162 | - # TODO: enhace interface | |
163 | - imagedata = Preview.dicom_loader[filename] | |
164 | - self.actor.SetInput(imagedata) | |
116 | + cast.SetWindow(window) | |
117 | + cast.SetLevel(level) | |
118 | + self.actor.SetInput(cast.GetOutput()) | |
165 | 119 | self.render.ResetCamera() |
120 | + #self.interactor.Render() | |
121 | + | |
166 | 122 | |
167 | 123 | class DicomPreviewSeries(wx.Panel): |
168 | 124 | """A dicom series preview panel""" |
... | ... | @@ -173,6 +129,7 @@ class DicomPreviewSeries(wx.Panel): |
173 | 129 | self.sizer = wx.BoxSizer(wx.HORIZONTAL) |
174 | 130 | self.SetSizer(self.sizer) |
175 | 131 | self.displayed_position = 0 |
132 | + self.files = [] | |
176 | 133 | self._init_ui() |
177 | 134 | |
178 | 135 | def _init_ui(self): |
... | ... | @@ -202,54 +159,59 @@ class DicomPreviewSeries(wx.Panel): |
202 | 159 | my_evt.SetSelectedID(evt.GetSelectID()) |
203 | 160 | self.GetEventHandler().ProcessEvent(my_evt) |
204 | 161 | |
205 | - def SetDicomDirectory(self, directory): | |
206 | - print "SetDicomDirectory" | |
162 | + def SetPatientGroups(self, patient): | |
163 | + group_list = patient.GetGroups() | |
164 | + n = 0 | |
165 | + for group in group_list: | |
166 | + info = (group.dicom.image.file, | |
167 | + float(group.dicom.image.window), | |
168 | + float(group.dicom.image.level), | |
169 | + group.title, | |
170 | + "%d Images" %(group.nslices), | |
171 | + n) | |
172 | + self.files.append(info) | |
173 | + n+=1 | |
174 | + | |
175 | + scroll_range = len(self.files)/5 | |
176 | + if scroll_range * 5 < len(self.files): | |
177 | + scroll_range +=1 | |
178 | + self.scroll.SetScrollbar(0, 3, scroll_range, 5) | |
179 | + | |
180 | + self._Display_Previews() | |
181 | + | |
182 | + | |
183 | + def SetDicomDirectoryOld(self, directory): | |
184 | + import time | |
185 | + a = time.time() | |
207 | 186 | self.directory = directory |
208 | 187 | self.series = dicom_reader.GetSeries(directory)[0] |
209 | - print "keys", [key[0] for key in self.series.keys()] | |
210 | - | |
211 | - s = self.series | |
212 | - for k in s.keys(): | |
213 | - print "------ PESSOA ---------" | |
214 | - print "%s (%d series)"%(k[0], len(s[k])-1) | |
215 | - for ns in range(1,len(s[k])): | |
216 | - print "------ SERIE ---------" | |
217 | - print "unnamed" | |
218 | - print "age %s" %(s[k][ns][8]) | |
219 | - print "date acquired %s %s" %(s[k][ns][0], s[k][ns][4]) | |
220 | - print "birthdate %s" %(s[k][ns][23]) | |
221 | - print "institution %s" %(s[k][ns][6]) | |
222 | - | |
188 | + b = time.time() | |
223 | 189 | # TODO: I need to improve this |
224 | 190 | self.files = [(self.series[i][0][0][8], # Filename |
225 | 191 | self.series[i][0][0][12], # Window Level |
226 | 192 | self.series[i][0][0][13], # Window Width |
227 | - "unnamed", #% (n + 1), # Title | |
193 | + "Serie %d" % (n + 1), # Title | |
228 | 194 | "%d Images" % len(self.series[i][0]), # Subtitle |
229 | - i) for n, i in enumerate(self.series)] | |
195 | + n) for n, i in enumerate(self.series)] | |
230 | 196 | |
231 | - def SetDicomSeries(self, patient): | |
232 | - #self.files = files | |
233 | - ngroups = patient.ngroups | |
234 | - self.groups = patient.GetGroups() | |
235 | - | |
236 | - scroll_range = ngroups/5 | |
237 | - if scroll_range * 5 < ngroups: | |
197 | + scroll_range = len(self.files)/5 | |
198 | + if scroll_range * 5 < len(self.files): | |
238 | 199 | scroll_range +=1 |
239 | 200 | self.scroll.SetScrollbar(0, 3, scroll_range, 5) |
240 | - self._display_previews() | |
241 | 201 | |
242 | - def _display_previews(self): | |
243 | - begin = self.displayed_position * 5 | |
244 | - end = begin + 15 | |
245 | - for group, preview in zip(self.groups[begin:end], self.previews): | |
246 | - preview.SetGroup(group) | |
247 | - preview.Show() | |
202 | + self._Display_Previews() | |
203 | + | |
204 | + def _Display_Previews(self): | |
205 | + initial = self.displayed_position * 5 | |
206 | + final = initial + 15 | |
207 | + for f, p in zip(self.files[initial:final], self.previews): | |
208 | + p.SetImage(f) | |
209 | + p.Show() | |
248 | 210 | |
249 | 211 | def OnScroll(self, evt): |
250 | 212 | self.displayed_position = evt.GetPosition() |
251 | 213 | [i.Hide() for i in self.previews] |
252 | - self._display_previews() | |
214 | + self._Display_Previews() | |
253 | 215 | |
254 | 216 | |
255 | 217 | class DicomPreview(wx.Panel): |
... | ... | @@ -261,7 +223,7 @@ class DicomPreview(wx.Panel): |
261 | 223 | self.sizer = wx.BoxSizer(wx.HORIZONTAL) |
262 | 224 | self.SetSizer(self.sizer) |
263 | 225 | self.displayed_position = 0 |
264 | - self.nhidden_last_display = 0 | |
226 | + self.files = [] | |
265 | 227 | self._init_ui() |
266 | 228 | |
267 | 229 | def _init_ui(self): |
... | ... | @@ -277,7 +239,7 @@ class DicomPreview(wx.Panel): |
277 | 239 | for i in xrange(3): |
278 | 240 | for j in xrange(5): |
279 | 241 | p = Preview(self) |
280 | - #p.Hide() | |
242 | + p.Hide() | |
281 | 243 | self.previews.append(p) |
282 | 244 | self.grid.Add(p, i, j) |
283 | 245 | |
... | ... | @@ -289,47 +251,55 @@ class DicomPreview(wx.Panel): |
289 | 251 | self.directory = directory |
290 | 252 | self.series = dicom_reader.GetSeries(directory)[0] |
291 | 253 | |
292 | - def SetDicomSerie(self, serie): | |
293 | - k = serie | |
254 | + def SetPatientGroups(self, patient): | |
255 | + self.group_list = patient.GetGroups() | |
256 | + | |
257 | + def SetDicomSerie(self, pos): | |
258 | + group = self.group_list[pos] | |
259 | + #dicom_files = group.GetList() | |
260 | + dicom_files = group.GetHandSortedList() | |
261 | + n = 0 | |
262 | + for dicom in dicom_files: | |
263 | + info = (dicom.image.file, | |
264 | + dicom.image.window, | |
265 | + dicom.image.level, | |
266 | + "Image %d" % (dicom.image.number), | |
267 | + "%.2f" % (dicom.image.position[2]), | |
268 | + n) | |
269 | + self.files.append(info) | |
270 | + n+=1 | |
271 | + | |
272 | + scroll_range = len(self.files)/5 | |
273 | + if scroll_range * 5 < len(self.files): | |
274 | + scroll_range +=1 | |
275 | + self.scroll.SetScrollbar(0, 3, scroll_range, 5) | |
276 | + self._Display_Previews() | |
277 | + | |
278 | + def SetDicomSerieOld(self, serie): | |
279 | + k = self.series.keys()[serie] | |
294 | 280 | self.files = [(i[8], |
295 | 281 | i[12], |
296 | 282 | i[13], |
297 | - "Image %d" % (n + 1), # Title | |
298 | - "%s"% str(i[3][2]), # Spacing | |
299 | - n)for n, i in enumerate(self.series[k][0])] | |
283 | + "Serie %d" % (n + 1), # Title | |
284 | + "%d Images" % n, # Subtitle | |
285 | + n)for n, i in enumerate(self.series[k][0])] | |
300 | 286 | scroll_range = len(self.files)/5 |
301 | 287 | if scroll_range * 5 < len(self.files): |
302 | 288 | scroll_range +=1 |
303 | 289 | self.scroll.SetScrollbar(0, 3, scroll_range, 5) |
304 | - self._display_previews() | |
290 | + self._Display_Previews() | |
305 | 291 | |
306 | - def _display_previews(self): | |
292 | + def _Display_Previews(self): | |
307 | 293 | initial = self.displayed_position * 5 |
308 | 294 | final = initial + 15 |
309 | - | |
310 | - if len(self.files) < final: | |
311 | - for i in xrange(final-len(self.files)): | |
312 | - try: | |
313 | - self.previews[-i-1].Hide() | |
314 | - except IndexError: | |
315 | - #print "doesn't exist!" | |
316 | - pass | |
317 | - self.nhidden_last_display = final-len(self.files) | |
318 | - else: | |
319 | - if self.nhidden_last_display: | |
320 | - for i in xrange(self.nhidden_last_display): | |
321 | - try: | |
322 | - self.previews[-i-1].Show() | |
323 | - except IndexError: | |
324 | - #print "doesn't exist!" | |
325 | - pass | |
326 | - self.nhidden_last_display = 0 | |
327 | - | |
328 | 295 | for f, p in zip(self.files[initial:final], self.previews): |
329 | 296 | p.SetImage(f) |
330 | - p.interactor.Render() | |
297 | + p.Show() | |
331 | 298 | |
332 | 299 | def OnScroll(self, evt): |
333 | - if self.displayed_position != evt.GetPosition(): | |
334 | - self.displayed_position = evt.GetPosition() | |
335 | - self._display_previews() | |
300 | + self.displayed_position = evt.GetPosition() | |
301 | + [i.Hide() for i in self.previews] | |
302 | + self._Display_Previews() | |
303 | + self.Update() | |
304 | + self.Update() | |
305 | + | ... | ... |
invesalius/gui/import_panel.py
... | ... | @@ -153,7 +153,7 @@ class TextPanel(wx.Panel): |
153 | 153 | class ImagePanel(wx.Panel): |
154 | 154 | def __init__(self, parent): |
155 | 155 | wx.Panel.__init__(self, parent, -1) |
156 | - self.SetBackgroundColour((0,255,0)) | |
156 | + #self.SetBackgroundColour((0,255,0)) | |
157 | 157 | |
158 | 158 | splitter = spl.MultiSplitterWindow(self, style=wx.SP_LIVE_UPDATE) |
159 | 159 | splitter.SetOrientation(wx.HORIZONTAL) |
... | ... | @@ -172,19 +172,48 @@ class ImagePanel(wx.Panel): |
172 | 172 | class SeriesPanel(wx.Panel): |
173 | 173 | def __init__(self, parent): |
174 | 174 | wx.Panel.__init__(self, parent, -1) |
175 | - self.SetBackgroundColour((0,0,0)) | |
175 | + #self.SetBackgroundColour((0,0,0)) | |
176 | + | |
176 | 177 | self.serie_preview = dpp.DicomPreviewSeries(self) |
178 | + self.dicom_preview = dpp.DicomPreview(self) | |
179 | + self.dicom_preview.Show(0) | |
180 | + | |
181 | + self.sizer = wx.BoxSizer(wx.VERTICAL) | |
182 | + self.sizer.Add(self.serie_preview, 1, wx.EXPAND | wx.ALL, 5) | |
183 | + self.sizer.Add(self.dicom_preview, 1, wx.EXPAND | wx.ALL, 5) | |
184 | + self.sizer.Fit(self) | |
185 | + | |
186 | + self.SetSizer(self.sizer) | |
187 | + self.SetAutoLayout(True) | |
188 | + self.Show() | |
189 | + | |
177 | 190 | |
178 | 191 | self.__bind_evt() |
192 | + self._bind_gui_evt() | |
179 | 193 | |
180 | 194 | def __bind_evt(self): |
181 | 195 | ps.Publisher().subscribe(self.ShowDicomSeries, "Load dicom preview") |
182 | 196 | |
197 | + def _bind_gui_evt(self): | |
198 | + self.Bind(dpp.EVT_SELECT_SERIE, self.OnSelectSerie) | |
199 | + | |
200 | + def OnSelectSerie(self, evt): | |
201 | + serie = evt.GetSelectID() | |
202 | + self.dicom_preview.SetDicomSerie(serie) | |
203 | + | |
204 | + self.dicom_preview.Show(1) | |
205 | + #self.sizer.Detach(self.serie_preview) | |
206 | + self.serie_preview.Show(0) | |
207 | + self.sizer.Layout() | |
208 | + #self.Show() | |
209 | + self.Update() | |
210 | + | |
211 | + | |
183 | 212 | def ShowDicomSeries(self, pubsub_evt): |
184 | 213 | print "---- ShowDicomSeries ----" |
185 | - list_dicom = pubsub_evt.data | |
186 | - print list_dicom | |
187 | - self.serie_preview.SetDicomSeries(list_dicom) | |
214 | + first_patient = pubsub_evt.data | |
215 | + self.serie_preview.SetPatientGroups(first_patient) | |
216 | + self.dicom_preview.SetPatientGroups(first_patient) | |
188 | 217 | |
189 | 218 | |
190 | 219 | ... | ... |
invesalius/invesalius.py