Commit b26289ddad90fb09c942105f37dcbf34323d8a34

Authored by Thiago Franco de Moraes
2 parents 536f91e7 6cf4c91a

Merge pull request #29 from tfmoraes/mask_boolean_op

Adds boolean operations to mask (union, xor, diff, intersection)
icons/bool_difference.png 0 → 100644

884 Bytes

icons/bool_disjunction.png 0 → 100644

840 Bytes

icons/bool_intersection.png 0 → 100644

820 Bytes

icons/bool_union.png 0 → 100644

843 Bytes

invesalius/constants.py
@@ -475,6 +475,8 @@ ID_SWAP_XY = wx.NewId() @@ -475,6 +475,8 @@ ID_SWAP_XY = wx.NewId()
475 ID_SWAP_XZ = wx.NewId() 475 ID_SWAP_XZ = wx.NewId()
476 ID_SWAP_YZ = wx.NewId() 476 ID_SWAP_YZ = wx.NewId()
477 477
  478 +ID_BOOLEAN_MASK = wx.NewId()
  479 +
478 #--------------------------------------------------------- 480 #---------------------------------------------------------
479 STATE_DEFAULT = 1000 481 STATE_DEFAULT = 1000
480 STATE_WL = 1001 482 STATE_WL = 1001
@@ -565,3 +567,9 @@ PROJECTION_CONTOUR_MIDA=8 @@ -565,3 +567,9 @@ PROJECTION_CONTOUR_MIDA=8
565 #------------ Projections defaults ------------------ 567 #------------ Projections defaults ------------------
566 PROJECTION_BORDER_SIZE=1.0 568 PROJECTION_BORDER_SIZE=1.0
567 PROJECTION_MIP_SIZE=2 569 PROJECTION_MIP_SIZE=2
  570 +
  571 +# ------------- Boolean operations ------------------
  572 +BOOLEAN_UNION = 1
  573 +BOOLEAN_DIFF = 2
  574 +BOOLEAN_AND = 3
  575 +BOOLEAN_XOR = 4
invesalius/control.py
@@ -82,6 +82,8 @@ class Controller(): @@ -82,6 +82,8 @@ class Controller():
82 Publisher.subscribe(self.OnOpenRecentProject, 'Open recent project') 82 Publisher.subscribe(self.OnOpenRecentProject, 'Open recent project')
83 Publisher.subscribe(self.OnShowAnalyzeFile, 'Show analyze dialog') 83 Publisher.subscribe(self.OnShowAnalyzeFile, 'Show analyze dialog')
84 84
  85 + Publisher.subscribe(self.ShowBooleanOpDialog, 'Show boolean dialog')
  86 +
85 87
86 def OnCancelImport(self, pubsub_evt): 88 def OnCancelImport(self, pubsub_evt):
87 #self.cancel_import = True 89 #self.cancel_import = True
@@ -628,6 +630,6 @@ class Controller(): @@ -628,6 +630,6 @@ class Controller():
628 preset_name + '.plist') 630 preset_name + '.plist')
629 plistlib.writePlist(preset, preset_dir) 631 plistlib.writePlist(preset, preset_dir)
630 632
631 -  
632 -  
633 - 633 + def ShowBooleanOpDialog(self, pubsub_evt):
  634 + dlg = dialogs.MaskBooleanDialog(prj.Project().mask_dict)
  635 + dlg.ShowModal()
invesalius/data/slice_.py
@@ -160,6 +160,8 @@ class Slice(object): @@ -160,6 +160,8 @@ class Slice(object):
160 160
161 Publisher.subscribe(self._set_projection_type, 'Set projection type') 161 Publisher.subscribe(self._set_projection_type, 'Set projection type')
162 162
  163 + Publisher.subscribe(self._do_boolean_op, 'Do boolean operation')
  164 +
163 Publisher.subscribe(self.OnExportMask,'Export mask to file') 165 Publisher.subscribe(self.OnExportMask,'Export mask to file')
164 166
165 Publisher.subscribe(self.OnCloseProject, 'Close project data') 167 Publisher.subscribe(self.OnCloseProject, 'Close project data')
@@ -1080,12 +1082,17 @@ class Slice(object): @@ -1080,12 +1082,17 @@ class Slice(object):
1080 else: 1082 else:
1081 node.value += shiftWW * factor 1083 node.value += shiftWW * factor
1082 1084
1083 - def do_threshold_to_a_slice(self, slice_matrix, mask): 1085 + def do_threshold_to_a_slice(self, slice_matrix, mask, threshold=None):
1084 """ 1086 """
1085 Based on the current threshold bounds generates a threshold mask to 1087 Based on the current threshold bounds generates a threshold mask to
1086 given slice_matrix. 1088 given slice_matrix.
1087 """ 1089 """
1088 - thresh_min, thresh_max = self.current_mask.threshold_range 1090 + if threshold:
  1091 + thresh_min, thresh_max = threshold
  1092 + else:
  1093 + thresh_min, thresh_max = self.current_mask.threshold_range
  1094 +
  1095 + print ">>>> THreshold", thresh_min, thresh_max
1089 m = (((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255) 1096 m = (((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255)
1090 m[mask == 1] = 1 1097 m[mask == 1] = 1
1091 m[mask == 2] = 2 1098 m[mask == 2] = 2
@@ -1106,7 +1113,7 @@ class Slice(object): @@ -1106,7 +1113,7 @@ class Slice(object):
1106 for n in xrange(1, mask.matrix.shape[0]): 1113 for n in xrange(1, mask.matrix.shape[0]):
1107 if mask.matrix[n, 0, 0] == 0: 1114 if mask.matrix[n, 0, 0] == 0:
1108 m = mask.matrix[n, 1:, 1:] 1115 m = mask.matrix[n, 1:, 1:]
1109 - mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m) 1116 + mask.matrix[n, 1:, 1:] = self.do_threshold_to_a_slice(self.matrix[n-1], m, mask.threshold_range)
1110 1117
1111 mask.matrix.flush() 1118 mask.matrix.flush()
1112 1119
@@ -1209,6 +1216,51 @@ class Slice(object): @@ -1209,6 +1216,51 @@ class Slice(object):
1209 1216
1210 return blend_imagedata.GetOutput() 1217 return blend_imagedata.GetOutput()
1211 1218
  1219 + def _do_boolean_op(self, pubsub_evt):
  1220 + op, m1, m2 = pubsub_evt.data
  1221 + self.do_boolean_op(op, m1, m2)
  1222 +
  1223 + def do_boolean_op(self, op, m1, m2):
  1224 + name_ops = {const.BOOLEAN_UNION: _(u"Union"),
  1225 + const.BOOLEAN_DIFF: _(u"Diff"),
  1226 + const.BOOLEAN_AND: _(u"Intersection"),
  1227 + const.BOOLEAN_XOR: _(u"XOR")}
  1228 +
  1229 +
  1230 + name = u"%s_%s_%s" % (name_ops[op], m1.name, m2.name)
  1231 + proj = Project()
  1232 + mask_dict = proj.mask_dict
  1233 + names_list = [mask_dict[i].name for i in mask_dict.keys()]
  1234 + new_name = utils.next_copy_name(name, names_list)
  1235 +
  1236 + future_mask = Mask()
  1237 + future_mask.create_mask(self.matrix.shape)
  1238 + future_mask.name = new_name
  1239 +
  1240 + future_mask.matrix[:] = 1
  1241 + m = future_mask.matrix[1:, 1:, 1:]
  1242 +
  1243 + self.do_threshold_to_all_slices(m1)
  1244 + m1 = m1.matrix[1:, 1:, 1:]
  1245 +
  1246 + self.do_threshold_to_all_slices(m2)
  1247 + m2 = m2.matrix[1:, 1:, 1:]
  1248 +
  1249 + if op == const.BOOLEAN_UNION:
  1250 + m[:] = ((m1 > 2) + (m2 > 2)) * 255
  1251 +
  1252 + elif op == const.BOOLEAN_DIFF:
  1253 + m[:] = ((m1 > 2) - ((m1 > 2) & (m2 > 2))) * 255
  1254 +
  1255 + elif op == const.BOOLEAN_AND:
  1256 + m[:] = ((m1 > 2) & (m2 > 2)) * 255
  1257 +
  1258 + elif op == const.BOOLEAN_XOR:
  1259 + m[:] = numpy.logical_xor((m1 > 2), (m2 > 2)) * 255
  1260 +
  1261 + future_mask.was_edited = True
  1262 + self._add_mask_into_proj(future_mask)
  1263 +
1212 def apply_slice_buffer_to_mask(self, orientation): 1264 def apply_slice_buffer_to_mask(self, orientation):
1213 """ 1265 """
1214 Apply the modifications (edition) in mask buffer to mask. 1266 Apply the modifications (edition) in mask buffer to mask.
invesalius/gui/dialogs.py
@@ -23,6 +23,7 @@ import random @@ -23,6 +23,7 @@ import random
23 import sys 23 import sys
24 24
25 import wx 25 import wx
  26 +import wx.combo
26 from wx.lib import masked 27 from wx.lib import masked
27 from wx.lib.agw import floatspin 28 from wx.lib.agw import floatspin
28 from wx.lib.wordwrap import wordwrap 29 from wx.lib.wordwrap import wordwrap
@@ -1469,3 +1470,82 @@ class WatershedOptionsDialog(wx.Dialog): @@ -1469,3 +1470,82 @@ class WatershedOptionsDialog(wx.Dialog):
1469 self.SetSizer(sizer) 1470 self.SetSizer(sizer)
1470 sizer.Fit(self) 1471 sizer.Fit(self)
1471 self.Layout() 1472 self.Layout()
  1473 +
  1474 +
  1475 +class MaskBooleanDialog(wx.Dialog):
  1476 + def __init__(self, masks):
  1477 + pre = wx.PreDialog()
  1478 + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Booleans operations"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT)
  1479 + self.PostCreate(pre)
  1480 +
  1481 + self._init_gui(masks)
  1482 +
  1483 + def _init_gui(self, masks):
  1484 + mask_choices = [(masks[i].name, masks[i]) for i in sorted(masks)]
  1485 + self.mask1 = wx.ComboBox(self, -1, mask_choices[0][0], choices=[])
  1486 + self.mask2 = wx.ComboBox(self, -1, mask_choices[0][0], choices=[])
  1487 +
  1488 + for n, m in mask_choices:
  1489 + self.mask1.Append(n, m)
  1490 + self.mask2.Append(n, m)
  1491 +
  1492 + self.mask1.SetSelection(0)
  1493 +
  1494 + if len(mask_choices) > 1:
  1495 + self.mask2.SetSelection(1)
  1496 + else:
  1497 + self.mask2.SetSelection(0)
  1498 +
  1499 + icon_folder = '../icons/'
  1500 + op_choices = ((_(u"Union"), const.BOOLEAN_UNION, 'bool_union.png'),
  1501 + (_(u"Difference"), const.BOOLEAN_DIFF, 'bool_difference.png'),
  1502 + (_(u"Intersection"), const.BOOLEAN_AND, 'bool_intersection.png'),
  1503 + (_(u"Exclusive disjunction"), const.BOOLEAN_XOR, 'bool_disjunction.png'))
  1504 + self.op_boolean = wx.combo.BitmapComboBox(self, -1, op_choices[0][0], choices=[])
  1505 +
  1506 + for n, i, f in op_choices:
  1507 + bmp = wx.Bitmap(os.path.join(icon_folder, f), wx.BITMAP_TYPE_PNG)
  1508 + self.op_boolean.Append(n, bmp, i)
  1509 +
  1510 + self.op_boolean.SetSelection(0)
  1511 +
  1512 + btn_ok = wx.Button(self, wx.ID_OK)
  1513 + btn_ok.SetDefault()
  1514 +
  1515 + btn_cancel = wx.Button(self, wx.ID_CANCEL)
  1516 +
  1517 + btnsizer = wx.StdDialogButtonSizer()
  1518 + btnsizer.AddButton(btn_ok)
  1519 + btnsizer.AddButton(btn_cancel)
  1520 + btnsizer.Realize()
  1521 +
  1522 + gsizer = wx.FlexGridSizer(rows=3, cols=2, hgap=5, vgap=5)
  1523 +
  1524 + gsizer.Add(wx.StaticText(self, -1, _(u"Mask 1")))
  1525 + gsizer.Add(self.mask1, 1, wx.EXPAND)
  1526 + gsizer.Add(wx.StaticText(self, -1, _(u"Operation")))
  1527 + gsizer.Add(self.op_boolean, 1, wx.EXPAND)
  1528 + gsizer.Add(wx.StaticText(self, -1, _(u"Mask 2")))
  1529 + gsizer.Add(self.mask2, 1, wx.EXPAND)
  1530 +
  1531 + sizer = wx.BoxSizer(wx.VERTICAL)
  1532 + sizer.Add(gsizer, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5)
  1533 + sizer.Add(btnsizer, 0, wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5)
  1534 +
  1535 + self.SetSizer(sizer)
  1536 + sizer.Fit(self)
  1537 +
  1538 + self.Centre()
  1539 +
  1540 + btn_ok.Bind(wx.EVT_BUTTON, self.OnOk)
  1541 +
  1542 + def OnOk(self, evt):
  1543 + op = self.op_boolean.GetClientData(self.op_boolean.GetSelection())
  1544 + m1 = self.mask1.GetClientData(self.mask1.GetSelection())
  1545 + m2 = self.mask2.GetClientData(self.mask2.GetSelection())
  1546 +
  1547 + Publisher.sendMessage('Do boolean operation', (op, m1, m2))
  1548 + Publisher.sendMessage('Reload actual slice')
  1549 +
  1550 + self.Close()
  1551 + self.Destroy()
invesalius/gui/frame.py
@@ -397,6 +397,9 @@ class Frame(wx.Frame): @@ -397,6 +397,9 @@ class Frame(wx.Frame):
397 elif id == wx.ID_REDO: 397 elif id == wx.ID_REDO:
398 self.OnRedo() 398 self.OnRedo()
399 399
  400 + elif id == const.ID_BOOLEAN_MASK:
  401 + self.OnMaskBoolean()
  402 +
400 def OnSize(self, evt): 403 def OnSize(self, evt):
401 """ 404 """
402 Refresh GUI when frame is resized. 405 Refresh GUI when frame is resized.
@@ -490,10 +493,11 @@ class Frame(wx.Frame): @@ -490,10 +493,11 @@ class Frame(wx.Frame):
490 Publisher.sendMessage('Redo edition') 493 Publisher.sendMessage('Redo edition')
491 494
492 495
  496 + def OnMaskBoolean(self):
  497 + print "Mask boolean"
  498 + Publisher.sendMessage('Show boolean dialog')
493 499
494 500
495 -  
496 -  
497 # ------------------------------------------------------------------ 501 # ------------------------------------------------------------------
498 # ------------------------------------------------------------------ 502 # ------------------------------------------------------------------
499 # ------------------------------------------------------------------ 503 # ------------------------------------------------------------------
@@ -605,6 +609,11 @@ class MenuBar(wx.MenuBar): @@ -605,6 +609,11 @@ class MenuBar(wx.MenuBar):
605 #app(const.ID_EDIT_LIST, "Show Undo List...") 609 #app(const.ID_EDIT_LIST, "Show Undo List...")
606 ################################################################# 610 #################################################################
607 611
  612 + # Mask Menu
  613 + mask_menu = wx.Menu()
  614 + mask_menu.Append(const.ID_BOOLEAN_MASK, _(u"Boolean operations"))
  615 + file_edit.AppendMenu(-1, _(u"Mask"), mask_menu)
  616 +
608 617
609 # VIEW 618 # VIEW
610 #view_tool_menu = wx.Menu() 619 #view_tool_menu = wx.Menu()