Commit 3e4ed11ddd539f45e2800e17665c4f4bff817166
Committed by
GitHub
1 parent
ccf02f83
Exists in
master
Create border closed (#149)
* Created a function to pad image * taking padding in consideration when converting to vtk * Better normals computation * Added option to close border holes
Showing
6 changed files
with
146 additions
and
72 deletions
Show diff stats
invesalius/data/converters.py
... | ... | @@ -21,7 +21,7 @@ import numpy |
21 | 21 | import vtk |
22 | 22 | from vtk.util import numpy_support |
23 | 23 | |
24 | -def to_vtk(n_array, spacing, slice_number, orientation): | |
24 | +def to_vtk(n_array, spacing, slice_number, orientation, origin=(0, 0, 0), padding=(0, 0, 0)): | |
25 | 25 | |
26 | 26 | if orientation == "SAGITTAL": |
27 | 27 | orientation = "SAGITAL" |
... | ... | @@ -32,20 +32,22 @@ def to_vtk(n_array, spacing, slice_number, orientation): |
32 | 32 | dy, dx = n_array.shape |
33 | 33 | dz = 1 |
34 | 34 | |
35 | + px, py, pz = padding | |
36 | + | |
35 | 37 | v_image = numpy_support.numpy_to_vtk(n_array.flat) |
36 | 38 | |
37 | 39 | if orientation == 'AXIAL': |
38 | - extent = (0, dx -1, 0, dy -1, slice_number, slice_number + dz - 1) | |
40 | + extent = (0 - px , dx -1 - px, 0 - py, dy - 1 - py, slice_number - pz, slice_number + dz - 1 - pz) | |
39 | 41 | elif orientation == 'SAGITAL': |
40 | 42 | dx, dy, dz = dz, dx, dy |
41 | - extent = (slice_number, slice_number + dx - 1, 0, dy - 1, 0, dz - 1) | |
43 | + extent = (slice_number - px, slice_number + dx - 1 - px, 0 - py, dy - 1 - py, 0 - pz, dz - 1 - pz) | |
42 | 44 | elif orientation == 'CORONAL': |
43 | 45 | dx, dy, dz = dx, dz, dy |
44 | - extent = (0, dx - 1, slice_number, slice_number + dy - 1, 0, dz - 1) | |
46 | + extent = (0 - px, dx - 1 - px, slice_number - py, slice_number + dy - 1 - py, 0 - pz, dz - 1 - pz) | |
45 | 47 | |
46 | 48 | # Generating the vtkImageData |
47 | 49 | image = vtk.vtkImageData() |
48 | - image.SetOrigin(0, 0, 0) | |
50 | + image.SetOrigin(origin) | |
49 | 51 | image.SetSpacing(spacing) |
50 | 52 | image.SetDimensions(dx, dy, dz) |
51 | 53 | # SetNumberOfScalarComponents and SetScalrType were replaced by | ... | ... |
invesalius/data/imagedata_utils.py
... | ... | @@ -751,3 +751,22 @@ def imgnormalize(data, srange=(0, 255)): |
751 | 751 | datan = datan.astype(numpy.int16) |
752 | 752 | |
753 | 753 | return datan |
754 | + | |
755 | + | |
756 | +def pad_image(image, pad_value, pad_bottom, pad_top): | |
757 | + dz, dy, dx = image.shape | |
758 | + z_iadd = 0 | |
759 | + z_eadd = 0 | |
760 | + if pad_bottom: | |
761 | + z_iadd = 1 | |
762 | + dz += 1 | |
763 | + if pad_top: | |
764 | + z_eadd = 1 | |
765 | + dz += 1 | |
766 | + new_shape = dz, dy + 2, dx + 2 | |
767 | + | |
768 | + paded_image = numpy.empty(shape=new_shape, dtype=image.dtype) | |
769 | + paded_image[:] = pad_value | |
770 | + paded_image[z_iadd: z_iadd + image.shape[0], 1:-1, 1:-1] = image | |
771 | + | |
772 | + return paded_image | ... | ... |
invesalius/data/surface.py
... | ... | @@ -538,6 +538,8 @@ class SurfaceManager(): |
538 | 538 | fill_holes = surface_parameters['options']['fill'] |
539 | 539 | keep_largest = surface_parameters['options']['keep_largest'] |
540 | 540 | |
541 | + fill_border_holes = surface_parameters['options'].get('fill_border_holes', True) | |
542 | + | |
541 | 543 | mode = 'CONTOUR' # 'GRAYSCALE' |
542 | 544 | min_value, max_value = mask.threshold_range |
543 | 545 | colour = mask.colour[:3] |
... | ... | @@ -598,7 +600,7 @@ class SurfaceManager(): |
598 | 600 | smooth_relaxation_factor, |
599 | 601 | smooth_iterations, language, flip_image, |
600 | 602 | algorithm != 'Default', algorithm, |
601 | - imagedata_resolution), | |
603 | + imagedata_resolution, fill_border_holes), | |
602 | 604 | callback=lambda x: filenames.append(x)) |
603 | 605 | |
604 | 606 | while len(filenames) != n_pieces: |
... | ... | @@ -659,7 +661,7 @@ class SurfaceManager(): |
659 | 661 | smooth_relaxation_factor, |
660 | 662 | smooth_iterations, language, flip_image, |
661 | 663 | algorithm != 'Default', algorithm, |
662 | - imagedata_resolution), | |
664 | + imagedata_resolution, fill_border_holes), | |
663 | 665 | callback=lambda x: filenames.append(x), |
664 | 666 | error_callback=functools.partial(self._on_callback_error, |
665 | 667 | dialog=sp)) |
... | ... | @@ -673,7 +675,7 @@ class SurfaceManager(): |
673 | 675 | smooth_relaxation_factor, |
674 | 676 | smooth_iterations, language, flip_image, |
675 | 677 | algorithm != 'Default', algorithm, |
676 | - imagedata_resolution), | |
678 | + imagedata_resolution, fill_border_holes), | |
677 | 679 | callback=lambda x: filenames.append(x)) |
678 | 680 | |
679 | 681 | while len(filenames) != n_pieces: | ... | ... |
invesalius/data/surface_process.py
... | ... | @@ -14,7 +14,7 @@ import vtk |
14 | 14 | import invesalius.i18n as i18n |
15 | 15 | import invesalius.data.converters as converters |
16 | 16 | from invesalius.data import cy_mesh |
17 | -# import invesalius.data.imagedata_utils as iu | |
17 | +import invesalius.data.imagedata_utils as iu | |
18 | 18 | |
19 | 19 | import weakref |
20 | 20 | from scipy import ndimage |
... | ... | @@ -45,15 +45,25 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, |
45 | 45 | mask_dtype, roi, spacing, mode, min_value, max_value, |
46 | 46 | decimate_reduction, smooth_relaxation_factor, |
47 | 47 | smooth_iterations, language, flip_image, |
48 | - from_binary, algorithm, imagedata_resolution): | |
48 | + from_binary, algorithm, imagedata_resolution, fill_border_holes): | |
49 | + | |
50 | + pad_bottom = (roi.start == 0) | |
51 | + pad_top = (roi.stop >= shape[0]) | |
52 | + | |
53 | + if fill_border_holes: | |
54 | + padding = (1, 1, pad_bottom) | |
55 | + else: | |
56 | + padding = (0, 0, 0) | |
57 | + | |
49 | 58 | if from_binary: |
50 | 59 | mask = numpy.memmap(mask_filename, mode='r', |
51 | 60 | dtype=mask_dtype, |
52 | 61 | shape=mask_shape) |
53 | - a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, | |
54 | - 1:, 1:]) | |
55 | - image = converters.to_vtk(a_mask, spacing, roi.start, | |
56 | - "AXIAL") | |
62 | + if fill_border_holes: | |
63 | + a_mask = iu.pad_image(mask[roi.start + 1: roi.stop + 1, 1:, 1:], 0, pad_bottom, pad_top) | |
64 | + else: | |
65 | + a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) | |
66 | + image = converters.to_vtk(a_mask, spacing, roi.start, "AXIAL", padding=padding) | |
57 | 67 | del a_mask |
58 | 68 | else: |
59 | 69 | image = numpy.memmap(filename, mode='r', dtype=dtype, |
... | ... | @@ -61,16 +71,21 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, |
61 | 71 | mask = numpy.memmap(mask_filename, mode='r', |
62 | 72 | dtype=mask_dtype, |
63 | 73 | shape=mask_shape) |
64 | - a_image = numpy.array(image[roi]) | |
74 | + if fill_border_holes: | |
75 | + a_image = iu.pad_image(image[roi], numpy.iinfo(image.dtype).min, pad_bottom, pad_top) | |
76 | + else: | |
77 | + a_image = numpy.array(image[roi]) | |
78 | + # if z_iadd: | |
79 | + # a_image[0, 1:-1, 1:-1] = image[0] | |
80 | + # if z_eadd: | |
81 | + # a_image[-1, 1:-1, 1:-1] = image[-1] | |
65 | 82 | |
66 | 83 | if algorithm == u'InVesalius 3.b2': |
67 | - a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, | |
68 | - 1:, 1:]) | |
84 | + a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) | |
69 | 85 | a_image[a_mask == 1] = a_image.min() - 1 |
70 | 86 | a_image[a_mask == 254] = (min_value + max_value) / 2.0 |
71 | 87 | |
72 | - image = converters.to_vtk(a_image, spacing, roi.start, | |
73 | - "AXIAL") | |
88 | + image = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) | |
74 | 89 | |
75 | 90 | gauss = vtk.vtkImageGaussianSmooth() |
76 | 91 | gauss.SetInputData(image) |
... | ... | @@ -83,8 +98,11 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, |
83 | 98 | del gauss |
84 | 99 | del a_mask |
85 | 100 | else: |
86 | - image = converters.to_vtk(a_image, spacing, roi.start, | |
87 | - "AXIAL") | |
101 | + # if z_iadd: | |
102 | + # origin = -spacing[0], -spacing[1], -spacing[2] | |
103 | + # else: | |
104 | + # origin = 0, -spacing[1], -spacing[2] | |
105 | + image = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) | |
88 | 106 | del a_image |
89 | 107 | |
90 | 108 | if imagedata_resolution: |
... | ... | @@ -97,6 +115,11 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, |
97 | 115 | flip.ReleaseDataFlagOn() |
98 | 116 | flip.Update() |
99 | 117 | |
118 | + # writer = vtk.vtkXMLImageDataWriter() | |
119 | + # writer.SetFileName('/tmp/camboja.vti') | |
120 | + # writer.SetInputData(flip.GetOutput()) | |
121 | + # writer.Write() | |
122 | + | |
100 | 123 | del image |
101 | 124 | image = flip.GetOutput() |
102 | 125 | del flip |
... | ... | @@ -224,34 +247,34 @@ def join_process_surface(filenames, algorithm, smooth_iterations, smooth_relaxat |
224 | 247 | |
225 | 248 | # polydata.SetSource(None) |
226 | 249 | # polydata.DebugOn() |
227 | - else: | |
228 | - #smoother = vtk.vtkWindowedSincPolyDataFilter() | |
229 | - send_message('Smoothing ...') | |
230 | - smoother = vtk.vtkSmoothPolyDataFilter() | |
231 | - smoother_ref = weakref.ref(smoother) | |
232 | - # smoother_ref().AddObserver("ProgressEvent", lambda obj,evt: | |
233 | - # UpdateProgress(smoother_ref(), _("Creating 3D surface..."))) | |
234 | - smoother.SetInputData(polydata) | |
235 | - smoother.SetNumberOfIterations(smooth_iterations) | |
236 | - smoother.SetRelaxationFactor(smooth_relaxation_factor) | |
237 | - smoother.SetFeatureAngle(80) | |
238 | - #smoother.SetEdgeAngle(90.0) | |
239 | - #smoother.SetPassBand(0.1) | |
240 | - smoother.BoundarySmoothingOn() | |
241 | - smoother.FeatureEdgeSmoothingOn() | |
242 | - #smoother.NormalizeCoordinatesOn() | |
243 | - #smoother.NonManifoldSmoothingOn() | |
244 | - # smoother.ReleaseDataFlagOn() | |
245 | - # smoother.GetOutput().ReleaseDataFlagOn() | |
246 | - smoother.Update() | |
247 | - del polydata | |
248 | - polydata = smoother.GetOutput() | |
249 | - #polydata.Register(None) | |
250 | - # polydata.SetSource(None) | |
251 | - del smoother | |
252 | - | |
253 | - | |
254 | - if decimate_reduction: | |
250 | + # else: | |
251 | + # #smoother = vtk.vtkWindowedSincPolyDataFilter() | |
252 | + # send_message('Smoothing ...') | |
253 | + # smoother = vtk.vtkSmoothPolyDataFilter() | |
254 | + # smoother_ref = weakref.ref(smoother) | |
255 | + # # smoother_ref().AddObserver("ProgressEvent", lambda obj,evt: | |
256 | + # # UpdateProgress(smoother_ref(), _("Creating 3D surface..."))) | |
257 | + # smoother.SetInputData(polydata) | |
258 | + # smoother.SetNumberOfIterations(smooth_iterations) | |
259 | + # smoother.SetRelaxationFactor(smooth_relaxation_factor) | |
260 | + # smoother.SetFeatureAngle(80) | |
261 | + # #smoother.SetEdgeAngle(90.0) | |
262 | + # #smoother.SetPassBand(0.1) | |
263 | + # smoother.BoundarySmoothingOn() | |
264 | + # smoother.FeatureEdgeSmoothingOn() | |
265 | + # #smoother.NormalizeCoordinatesOn() | |
266 | + # #smoother.NonManifoldSmoothingOn() | |
267 | + # # smoother.ReleaseDataFlagOn() | |
268 | + # # smoother.GetOutput().ReleaseDataFlagOn() | |
269 | + # smoother.Update() | |
270 | + # del polydata | |
271 | + # polydata = smoother.GetOutput() | |
272 | + # #polydata.Register(None) | |
273 | + # # polydata.SetSource(None) | |
274 | + # del smoother | |
275 | + | |
276 | + | |
277 | + if not decimate_reduction: | |
255 | 278 | print("Decimating", decimate_reduction) |
256 | 279 | send_message('Decimating ...') |
257 | 280 | decimation = vtk.vtkQuadricDecimation() |
... | ... | @@ -320,9 +343,11 @@ def join_process_surface(filenames, algorithm, smooth_iterations, smooth_relaxat |
320 | 343 | # normals_ref().AddObserver("ProgressEvent", lambda obj,evt: |
321 | 344 | # UpdateProgress(normals_ref(), _("Creating 3D surface..."))) |
322 | 345 | normals.SetInputData(polydata) |
323 | - # normals.SetFeatureAngle(80) | |
324 | - # normals.SplittingOff() | |
325 | - # normals.AutoOrientNormalsOn() | |
346 | + normals.SetFeatureAngle(80) | |
347 | + normals.SplittingOn() | |
348 | + normals.AutoOrientNormalsOn() | |
349 | + normals.NonManifoldTraversalOn() | |
350 | + normals.ComputeCellNormalsOn() | |
326 | 351 | # normals.GetOutput().ReleaseDataFlagOn() |
327 | 352 | normals.Update() |
328 | 353 | del polydata |
... | ... | @@ -332,22 +357,22 @@ def join_process_surface(filenames, algorithm, smooth_iterations, smooth_relaxat |
332 | 357 | del normals |
333 | 358 | |
334 | 359 | |
335 | - # Improve performance | |
336 | - stripper = vtk.vtkStripper() | |
337 | - # stripper.ReleaseDataFlagOn() | |
338 | - # stripper_ref = weakref.ref(stripper) | |
339 | - # stripper_ref().AddObserver("ProgressEvent", lambda obj,evt: | |
340 | - # UpdateProgress(stripper_ref(), _("Creating 3D surface..."))) | |
341 | - stripper.SetInputData(polydata) | |
342 | - stripper.PassThroughCellIdsOn() | |
343 | - stripper.PassThroughPointIdsOn() | |
344 | - # stripper.GetOutput().ReleaseDataFlagOn() | |
345 | - stripper.Update() | |
346 | - del polydata | |
347 | - polydata = stripper.GetOutput() | |
348 | - #polydata.Register(None) | |
349 | - # polydata.SetSource(None) | |
350 | - del stripper | |
360 | + # # Improve performance | |
361 | + # stripper = vtk.vtkStripper() | |
362 | + # # stripper.ReleaseDataFlagOn() | |
363 | + # # stripper_ref = weakref.ref(stripper) | |
364 | + # # stripper_ref().AddObserver("ProgressEvent", lambda obj,evt: | |
365 | + # # UpdateProgress(stripper_ref(), _("Creating 3D surface..."))) | |
366 | + # stripper.SetInputData(polydata) | |
367 | + # stripper.PassThroughCellIdsOn() | |
368 | + # stripper.PassThroughPointIdsOn() | |
369 | + # # stripper.GetOutput().ReleaseDataFlagOn() | |
370 | + # stripper.Update() | |
371 | + # del polydata | |
372 | + # polydata = stripper.GetOutput() | |
373 | + # #polydata.Register(None) | |
374 | + # # polydata.SetSource(None) | |
375 | + # del stripper | |
351 | 376 | |
352 | 377 | send_message('Calculating area and volume ...') |
353 | 378 | measured_polydata = vtk.vtkMassProperties() | ... | ... |
invesalius/gui/dialogs.py
... | ... | @@ -1662,7 +1662,10 @@ class SurfaceCreationOptionsPanel(wx.Panel): |
1662 | 1662 | (combo_quality, 0, flag_button, 0)]) |
1663 | 1663 | |
1664 | 1664 | |
1665 | - # LINES 4 and 5: Checkboxes | |
1665 | + # LINES 4, 5 and 6: Checkboxes | |
1666 | + check_box_border_holes = wx.CheckBox(self, -1, _("Fill border holes")) | |
1667 | + check_box_border_holes.SetValue(False) | |
1668 | + self.check_box_border_holes = check_box_border_holes | |
1666 | 1669 | check_box_holes = wx.CheckBox(self, -1, _("Fill holes")) |
1667 | 1670 | check_box_holes.SetValue(False) |
1668 | 1671 | self.check_box_holes = check_box_holes |
... | ... | @@ -1673,6 +1676,7 @@ class SurfaceCreationOptionsPanel(wx.Panel): |
1673 | 1676 | # Merge all sizers and checkboxes |
1674 | 1677 | sizer = wx.BoxSizer(wx.VERTICAL) |
1675 | 1678 | sizer.Add(fixed_sizer, 0, wx.TOP|wx.RIGHT|wx.LEFT|wx.GROW|wx.EXPAND, 5) |
1679 | + sizer.Add(check_box_border_holes, 0, wx.RIGHT|wx.LEFT, 5) | |
1676 | 1680 | sizer.Add(check_box_holes, 0, wx.RIGHT|wx.LEFT, 5) |
1677 | 1681 | sizer.Add(check_box_largest, 0, wx.RIGHT|wx.LEFT, 5) |
1678 | 1682 | |
... | ... | @@ -1687,11 +1691,13 @@ class SurfaceCreationOptionsPanel(wx.Panel): |
1687 | 1691 | mask_index = self.combo_mask.GetSelection() |
1688 | 1692 | surface_name = self.text.GetValue() |
1689 | 1693 | quality = const.SURFACE_QUALITY_LIST[self.combo_quality.GetSelection()] |
1694 | + fill_border_holes = self.check_box_border_holes.GetValue() | |
1690 | 1695 | fill_holes = self.check_box_holes.GetValue() |
1691 | 1696 | keep_largest = self.check_box_largest.GetValue() |
1692 | 1697 | return {"index": mask_index, |
1693 | 1698 | "name": surface_name, |
1694 | 1699 | "quality": quality, |
1700 | + "fill_border_holes": fill_border_holes, | |
1695 | 1701 | "fill": fill_holes, |
1696 | 1702 | "keep_largest": keep_largest, |
1697 | 1703 | "overwrite": False} | ... | ... |
invesalius/gui/preferences.py
... | ... | @@ -15,7 +15,7 @@ except ImportError: # if it's not there locally, try the wxPython lib. |
15 | 15 | class Preferences(wx.Dialog): |
16 | 16 | |
17 | 17 | def __init__( self, parent, id = ID, title = _("Preferences"), size=wx.DefaultSize,\ |
18 | - pos=wx.DefaultPosition, style=wx.DEFAULT_DIALOG_STYLE): | |
18 | + pos=wx.DefaultPosition, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER): | |
19 | 19 | |
20 | 20 | try: |
21 | 21 | pre = wx.PreDialog() |
... | ... | @@ -36,14 +36,16 @@ class Preferences(wx.Dialog): |
36 | 36 | else: |
37 | 37 | self.book = fnb.FlatNotebook(self, wx.ID_ANY, agwStyle=bookStyle) |
38 | 38 | |
39 | - sizer.Add(self.book, 80, wx.EXPAND|wx.ALL) | |
39 | + sizer.Add(self.book, 1, wx.EXPAND|wx.ALL) | |
40 | 40 | |
41 | 41 | self.pnl_viewer2d = Viewer2D(self) |
42 | 42 | self.pnl_viewer3d = Viewer3D(self) |
43 | + # self.pnl_surface = SurfaceCreation(self) | |
43 | 44 | self.pnl_language = Language(self) |
44 | 45 | |
45 | 46 | self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) |
46 | 47 | self.book.AddPage(self.pnl_viewer3d, _("3D Visualization")) |
48 | + # self.book.AddPage(self.pnl_surface, _("Surface creation")) | |
47 | 49 | self.book.AddPage(self.pnl_language, _("Language")) |
48 | 50 | |
49 | 51 | line = wx.StaticLine(self, -1, size=(20,-1), style=wx.LI_HORIZONTAL) |
... | ... | @@ -59,7 +61,7 @@ class Preferences(wx.Dialog): |
59 | 61 | |
60 | 62 | btnsizer.Realize() |
61 | 63 | |
62 | - sizer.Add(btnsizer, 10, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP|wx.BOTTOM, 5) | |
64 | + sizer.Add(btnsizer, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP|wx.BOTTOM, 5) | |
63 | 65 | |
64 | 66 | self.SetSizer(sizer) |
65 | 67 | sizer.Fit(self) |
... | ... | @@ -214,3 +216,21 @@ class Language(wx.Panel): |
214 | 216 | locales = self.lg.GetLocalesKey() |
215 | 217 | selection = locales.index(language) |
216 | 218 | self.cmb_lang.SetSelection(int(selection)) |
219 | + | |
220 | + | |
221 | + | |
222 | +class SurfaceCreation(wx.Panel): | |
223 | + def __init__(self, parent): | |
224 | + wx.Panel.__init__(self, parent) | |
225 | + self.rb_fill_border = wx.RadioBox(self, -1, "Fill border holes", choices=[_('Yes'), _('No')], style=wx.RA_SPECIFY_COLS | wx.NO_BORDER) | |
226 | + | |
227 | + sizer = wx.BoxSizer(wx.VERTICAL) | |
228 | + sizer.Add(self.rb_fill_border) | |
229 | + | |
230 | + self.SetSizerAndFit(sizer) | |
231 | + | |
232 | + def GetSelection(self): | |
233 | + return {} | |
234 | + | |
235 | + def LoadSelection(self, values): | |
236 | + pass | ... | ... |