Commit b26289ddad90fb09c942105f37dcbf34323d8a34
Exists in
master
and in
40 other branches
Merge pull request #29 from tfmoraes/mask_boolean_op
Adds boolean operations to mask (union, xor, diff, intersection)
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() | ... | ... |