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,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() |