Commit 6cf4c91a91de4ed7c5475b9a311a8bb48bd79a81
1 parent
536f91e7
Exists in
master
and in
2 other branches
Adds boolean operation to mask (union, xor, diff, intersection)
Implements boolean operations to masks. The user has a gui to choose the masks and the operation (union, xor, diff, intersection). Then InVesalius creates a new mask with the result of the operation. Doing some boolean operations Added Intesection and XOR There was an error in the bool diff operation Gui improvements changed "xor" to "exclusive disjuction" added the icons Improvements to the boolean operation dialog Added a menu to mask operations (booleans is there)
Showing
9 changed files
with
159 additions
and
8 deletions
Show diff stats
884 Bytes
840 Bytes
820 Bytes
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() | ... | ... |