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 475 ID_SWAP_XZ = wx.NewId()
476 476 ID_SWAP_YZ = wx.NewId()
477 477  
  478 +ID_BOOLEAN_MASK = wx.NewId()
  479 +
478 480 #---------------------------------------------------------
479 481 STATE_DEFAULT = 1000
480 482 STATE_WL = 1001
... ... @@ -565,3 +567,9 @@ PROJECTION_CONTOUR_MIDA=8
565 567 #------------ Projections defaults ------------------
566 568 PROJECTION_BORDER_SIZE=1.0
567 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 82 Publisher.subscribe(self.OnOpenRecentProject, 'Open recent project')
83 83 Publisher.subscribe(self.OnShowAnalyzeFile, 'Show analyze dialog')
84 84  
  85 + Publisher.subscribe(self.ShowBooleanOpDialog, 'Show boolean dialog')
  86 +
85 87  
86 88 def OnCancelImport(self, pubsub_evt):
87 89 #self.cancel_import = True
... ... @@ -628,6 +630,6 @@ class Controller():
628 630 preset_name + '.plist')
629 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 160  
161 161 Publisher.subscribe(self._set_projection_type, 'Set projection type')
162 162  
  163 + Publisher.subscribe(self._do_boolean_op, 'Do boolean operation')
  164 +
163 165 Publisher.subscribe(self.OnExportMask,'Export mask to file')
164 166  
165 167 Publisher.subscribe(self.OnCloseProject, 'Close project data')
... ... @@ -1080,12 +1082,17 @@ class Slice(object):
1080 1082 else:
1081 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 1087 Based on the current threshold bounds generates a threshold mask to
1086 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 1096 m = (((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255)
1090 1097 m[mask == 1] = 1
1091 1098 m[mask == 2] = 2
... ... @@ -1106,7 +1113,7 @@ class Slice(object):
1106 1113 for n in xrange(1, mask.matrix.shape[0]):
1107 1114 if mask.matrix[n, 0, 0] == 0:
1108 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 1118 mask.matrix.flush()
1112 1119  
... ... @@ -1209,6 +1216,51 @@ class Slice(object):
1209 1216  
1210 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 1264 def apply_slice_buffer_to_mask(self, orientation):
1213 1265 """
1214 1266 Apply the modifications (edition) in mask buffer to mask.
... ...
invesalius/gui/dialogs.py
... ... @@ -23,6 +23,7 @@ import random
23 23 import sys
24 24  
25 25 import wx
  26 +import wx.combo
26 27 from wx.lib import masked
27 28 from wx.lib.agw import floatspin
28 29 from wx.lib.wordwrap import wordwrap
... ... @@ -1469,3 +1470,82 @@ class WatershedOptionsDialog(wx.Dialog):
1469 1470 self.SetSizer(sizer)
1470 1471 sizer.Fit(self)
1471 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 397 elif id == wx.ID_REDO:
398 398 self.OnRedo()
399 399  
  400 + elif id == const.ID_BOOLEAN_MASK:
  401 + self.OnMaskBoolean()
  402 +
400 403 def OnSize(self, evt):
401 404 """
402 405 Refresh GUI when frame is resized.
... ... @@ -490,10 +493,11 @@ class Frame(wx.Frame):
490 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 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 618 # VIEW
610 619 #view_tool_menu = wx.Menu()
... ...