Commit 7055b3d716d7b9892d6c36c263dec561cef0aeaf

Authored by Paulo Henrique Junqueira Amorim
Committed by GitHub
1 parent 8a232033

ADD: Added crop mask tool.

Tool for selecting the region of interest using a box.
invesalius/constants.py
... ... @@ -141,7 +141,24 @@ MODE_NAVIGATOR = 1
141 141 MODE_RADIOLOGY = 2
142 142 MODE_ODONTOLOGY = 3
143 143  
  144 +#Crop box sides code
144 145  
  146 +AXIAL_RIGHT = 1
  147 +AXIAL_LEFT = 2
  148 +AXIAL_UPPER = 3
  149 +AXIAL_BOTTOM = 4
  150 +
  151 +SAGITAL_RIGHT = 5
  152 +SAGITAL_LEFT = 6
  153 +SAGITAL_UPPER = 7
  154 +SAGITAL_BOTTOM = 8
  155 +
  156 +CORONAL_RIGHT = 9
  157 +CORONAL_LEFT = 10
  158 +CORONAL_UPPER = 11
  159 +CORONAL_BOTTOM = 12
  160 +
  161 +CROP_PAN = 13
145 162  
146 163 #Color Table from Slice
147 164 #NumberOfColors, SaturationRange, HueRange, ValueRange
... ... @@ -485,6 +502,8 @@ ID_FLOODFILL_MASK = wx.NewId()
485 502 ID_REMOVE_MASK_PART = wx.NewId()
486 503 ID_SELECT_MASK_PART = wx.NewId()
487 504 ID_FLOODFILL_SEGMENTATION = wx.NewId()
  505 +ID_CROP_MASK = wx.NewId()
  506 +ID_SELECT_MASK_PART = wx.NewId()
488 507  
489 508 #---------------------------------------------------------
490 509 STATE_DEFAULT = 1000
... ... @@ -506,6 +525,8 @@ SLICE_STATE_MASK_FFILL = 3011
506 525 SLICE_STATE_REMOVE_MASK_PARTS = 3012
507 526 SLICE_STATE_SELECT_MASK_PARTS = 3013
508 527 SLICE_STATE_FFILL_SEGMENTATION = 3014
  528 +SLICE_STATE_CROP_MASK = 3015
  529 +SLICE_STATE_SELECT_MASK_PARTS = 3014
509 530  
510 531 VOLUME_STATE_SEED = 2001
511 532 # STATE_LINEAR_MEASURE = 3001
... ... @@ -527,6 +548,8 @@ SLICE_STYLES.append(SLICE_STATE_MASK_FFILL)
527 548 SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS)
528 549 SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS)
529 550 SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION)
  551 +SLICE_STYLES.append(SLICE_STATE_CROP_MASK)
  552 +SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS)
530 553  
531 554 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
532 555 STATE_MEASURE_ANGLE]
... ... @@ -542,6 +565,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
542 565 SLICE_STATE_CROSS: 2,
543 566 SLICE_STATE_SCROLL: 2,
544 567 SLICE_STATE_REORIENT: 2,
  568 + SLICE_STATE_CROP_MASK: 1,
545 569 STATE_ANNOTATE: 2,
546 570 STATE_DEFAULT: 0,
547 571 STATE_MEASURE_ANGLE: 2,
... ...
invesalius/data/geometry.py 0 → 100644
... ... @@ -0,0 +1,606 @@
  1 +#--------------------------------------------------------------------------
  2 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  3 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  4 +# Homepage: http://www.softwarepublico.gov.br /
  5 +# http://www.cti.gov.br/invesalius
  6 +# Contact: invesalius@cti.gov.br
  7 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  8 +#--------------------------------------------------------------------------
  9 +# Este programa e software livre; voce pode redistribui-lo e/ou
  10 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  11 +# publicada pela Free Software Foundation; de acordo com a versao 2
  12 +# da Licenca.
  13 +#
  14 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  15 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  16 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  17 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  18 +# detalhes.
  19 +#--------------------------------------------------------------------------
  20 +
  21 +import numpy as np
  22 +import math
  23 +import vtk
  24 +from wx.lib.pubsub import pub as Publisher
  25 +
  26 +import utils
  27 +import constants as const
  28 +
  29 +
  30 +class Box(object):
  31 + """
  32 + This class is a data structure for storing the
  33 + coordinates (min and max) of box used in crop-mask.
  34 + """
  35 +
  36 + __metaclass__= utils.Singleton
  37 +
  38 + def __init__(self):
  39 + self.xi = None
  40 + self.xf = None
  41 +
  42 + self.yi = None
  43 + self.yf = None
  44 +
  45 + self.zi = None
  46 + self.zf = None
  47 +
  48 + self.size_x = None
  49 + self.size_y = None
  50 + self.size_z = None
  51 +
  52 + self.sagital = {}
  53 + self.coronal = {}
  54 + self.axial = {}
  55 +
  56 + self.xs = None
  57 + self.ys = None
  58 + self.zs = None
  59 +
  60 + self.first_run = True
  61 +
  62 + def SetX(self, i, f):
  63 + self.xi = i
  64 + self.xf = f
  65 + self.size_x = f
  66 +
  67 + def SetY(self, i, f):
  68 + self.yi = i
  69 + self.yf = f
  70 + self.size_y = f
  71 +
  72 + def SetZ(self, i, f):
  73 + self.zi = i
  74 + self.zf = f
  75 + self.size_z = f
  76 +
  77 + def SetSpacing(self, x, y, z):
  78 + self.xs = x
  79 + self.ys = y
  80 + self.zs = z
  81 +
  82 + self.xi = self.xi * self.xs
  83 + self.xf = self.xf * self.xs
  84 +
  85 + self.yi = self.yi * self.ys
  86 + self.yf = self.yf * self.ys
  87 +
  88 + self.zi = self.zi * self.zs
  89 + self.zf = self.zf * self.zs
  90 +
  91 + self.size_x = self.size_x * self.xs
  92 + self.size_y = self.size_y * self.ys
  93 + self.size_z = self.size_z * self.zs
  94 +
  95 + if self.first_run:
  96 + self.first_run = False
  97 +
  98 + def MakeMatrix(self):
  99 + """
  100 + Update values in a matrix to each orientation.
  101 + """
  102 +
  103 + self.sagital[const.SAGITAL_LEFT] = [[self.xi, self.yi - (self.ys/2), self.zi],\
  104 + [self.xi, self.yi - (self.ys/2), self.zf]]
  105 +
  106 + self.sagital[const.SAGITAL_RIGHT] = [[self.xi, self.yf + (self.ys/2), self.zi],\
  107 + [self.xi, self.yf + (self.ys/2), self.zf]]
  108 +
  109 + self.sagital[const.SAGITAL_BOTTOM] = [[self.xi, self.yi, self.zi - (self.zs/2)],\
  110 + [self.xi, self.yf, self.zi - (self.zs/2)]]
  111 +
  112 + self.sagital[const.SAGITAL_UPPER] = [[self.xi, self.yi, self.zf + (self.zs/2) ],\
  113 + [self.xi, self.yf, self.zf + (self.zs/2) ]]
  114 +
  115 + self.coronal[const.CORONAL_BOTTOM] = [[self.xi, self.yi, self.zi - (self.zs/2)],\
  116 + [self.xf, self.yf, self.zi - (self.zs/2)]]
  117 +
  118 + self.coronal[const.CORONAL_UPPER] = [[self.xi, self.yi, self.zf + (self.zs/2)],\
  119 + [self.xf, self.yf, self.zf + (self.zs/2)]]
  120 +
  121 + self.coronal[const.CORONAL_LEFT] = [[self.xi - (self.xs/2), self.yi, self.zi],\
  122 + [self.xi - (self.xs/2), self.yf, self.zf]]
  123 +
  124 + self.coronal[const.CORONAL_RIGHT] = [[self.xf + (self.xs/2), self.yi, self.zi],\
  125 + [self.xf + (self.xs/2), self.yf, self.zf]]
  126 +
  127 + self.axial[const.AXIAL_BOTTOM] = [[self.xi, self.yi - (self.ys/2), self.zi],\
  128 + [self.xf, self.yi - (self.ys/2), self.zf]]
  129 +
  130 + self.axial[const.AXIAL_UPPER] = [[self.xi, self.yf + (self.ys/2), self.zi],\
  131 + [self.xf, self.yf + (self.ys/2), self.zf]]
  132 +
  133 + self.axial[const.AXIAL_LEFT] = [[self.xi - (self.xs/2), self.yi, self.zi],\
  134 + [self.xi - (self.xs/2), self.yf, self.zf]]
  135 +
  136 + self.axial[const.AXIAL_RIGHT] = [[self.xf + (self.xs/2), self.yi, self.zi],\
  137 + [self.xf + (self.xs/2), self.yf, self.zf]]
  138 +
  139 + Publisher.sendMessage('Update crop limits into gui', self.GetLimits())
  140 +
  141 +
  142 + def GetLimits(self):
  143 + """
  144 + Return the bounding box limits (initial and final) in x, y and z.
  145 + """
  146 +
  147 + limits = [ int(self.xi / self.xs), int(self.xf / self. xs),\
  148 + int(self.yi / self.ys), int(self.yf / self.ys),\
  149 + int(self.zi / self.zs), int(self.zf / self.zs)]
  150 +
  151 + return limits
  152 +
  153 +
  154 + def UpdatePositionBySideBox(self, pc, axis, position):
  155 + """
  156 + Checks the coordinates are in any side of box and update it.
  157 + Is necessary to move limits of box.
  158 + """
  159 +
  160 + if axis == "AXIAL":
  161 + if position == const.AXIAL_UPPER:
  162 + if pc[1] > self.yi and pc[1] > 0 and pc[1] <= self.size_y:
  163 + self.yf = pc[1]
  164 +
  165 + if position == const.AXIAL_BOTTOM:
  166 + if pc[1] < self.yf and pc[1] >= 0:
  167 + self.yi = pc[1]
  168 +
  169 + if position == const.AXIAL_LEFT:
  170 + if pc[0] < self.xf and pc[0] >= 0:
  171 + self.xi = pc[0]
  172 +
  173 +
  174 + if position == const.AXIAL_RIGHT:
  175 + if pc[0] > self.xi and pc[0] <= self.size_x:
  176 + self.xf = pc[0]
  177 +
  178 +
  179 + if axis == "SAGITAL":
  180 + if position == const.SAGITAL_UPPER:
  181 + if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z:
  182 + self.zf = pc[2]
  183 +
  184 + if position == const.SAGITAL_BOTTOM:
  185 + if pc[2] < self.zf and pc[2] >= 0:
  186 + self.zi = pc[2]
  187 +
  188 + if position == const.SAGITAL_LEFT:
  189 + if pc[1] < self.yf and pc[1] >= 0:
  190 + self.yi = pc[1]
  191 +
  192 + if position == const.SAGITAL_RIGHT:
  193 + if pc[1] > self.yi and pc[1] <= self.size_y:
  194 + self.yf = pc[1]
  195 +
  196 +
  197 + if axis == "CORONAL":
  198 + if position == const.CORONAL_UPPER:
  199 + if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z:
  200 + self.zf = pc[2]
  201 +
  202 + if position == const.CORONAL_BOTTOM:
  203 + if pc[2] < self.zf and pc[2] >= 0:
  204 + self.zi = pc[2]
  205 +
  206 + if position == const.CORONAL_LEFT:
  207 + if pc[0] < self.xf and pc[0] >= 0:
  208 + self.xi = pc[0]
  209 +
  210 + if position == const.CORONAL_RIGHT:
  211 + if pc[0] > self.yi and pc[0] <= self.size_y:
  212 + self.xf = pc[0]
  213 +
  214 + self.MakeMatrix()
  215 +
  216 +
  217 + def UpdatePositionByInsideBox(self, pc, axis):
  218 + """
  219 + Checks the coordinates are inside the box and update it.
  220 + Is necessary to move box in pan event.
  221 + """
  222 +
  223 + if axis == "AXIAL":
  224 +
  225 + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0:
  226 + self.yf = self.yf + pc[1]
  227 + self.yi = self.yi + pc[1]
  228 +
  229 + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0:
  230 + self.xf = self.xf + pc[0]
  231 + self.xi = self.xi + pc[0]
  232 +
  233 + if axis == "SAGITAL":
  234 +
  235 + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0:
  236 + self.yf = self.yf + pc[1]
  237 + self.yi = self.yi + pc[1]
  238 +
  239 + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0:
  240 + self.zf = self.zf + pc[2]
  241 + self.zi = self.zi + pc[2]
  242 +
  243 + if axis == "CORONAL":
  244 +
  245 + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0:
  246 + self.xf = self.xf + pc[0]
  247 + self.xi = self.xi + pc[0]
  248 +
  249 + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0:
  250 + self.zf = self.zf + pc[2]
  251 + self.zi = self.zi + pc[2]
  252 +
  253 + self.MakeMatrix()
  254 +
  255 +
  256 +
  257 +class DrawCrop2DRetangle():
  258 + """
  259 + This class is responsible for draw and control user
  260 + interactions with the box. Each side of box is displayed in an
  261 + anatomical orientation (axial, sagital or coronal).
  262 + """
  263 + def __init__(self):
  264 + self.viewer = None
  265 + self.points_in_display = {}
  266 + self.box = None
  267 + self.mouse_pressed = False
  268 + self.canvas = None
  269 + self.status_move = None
  270 + self.crop_pan = None
  271 + self.last_x = 0
  272 + self.last_y = 0
  273 + self.last_z = 0
  274 +
  275 + def MouseMove(self, x, y):
  276 +
  277 + self.MouseInLine(x, y)
  278 +
  279 + x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y)
  280 + slice_spacing = self.viewer.slice_.spacing
  281 + xs, ys, zs = slice_spacing
  282 +
  283 + x_pos_sl = x_pos_sl_ * xs
  284 + y_pos_sl = y_pos_sl_ * ys
  285 +
  286 + x, y, z = self.viewer.get_voxel_coord_by_screen_pos(x, y)
  287 +
  288 + if self.viewer.orientation == "AXIAL":
  289 +
  290 + if self.status_move == const.AXIAL_UPPER or\
  291 + self.status_move == const.AXIAL_BOTTOM:
  292 + Publisher.sendMessage('Set interactor resize NS cursor')
  293 + elif self.status_move == const.AXIAL_LEFT or\
  294 + self.status_move == const.AXIAL_RIGHT:
  295 + Publisher.sendMessage('Set interactor resize WE cursor')
  296 + elif self.crop_pan == const.CROP_PAN:
  297 + Publisher.sendMessage('Set interactor resize NSWE cursor')
  298 + else:
  299 + Publisher.sendMessage('Set interactor default cursor')
  300 +
  301 + if self.viewer.orientation == "SAGITAL":
  302 + if self.status_move == const.SAGITAL_UPPER or\
  303 + self.status_move == const.SAGITAL_BOTTOM:
  304 + Publisher.sendMessage('Set interactor resize NS cursor')
  305 + elif self.status_move == const.SAGITAL_LEFT or\
  306 + self.status_move == const.SAGITAL_RIGHT:
  307 + Publisher.sendMessage('Set interactor resize WE cursor')
  308 + elif self.crop_pan == const.CROP_PAN:
  309 + Publisher.sendMessage('Set interactor resize NSWE cursor')
  310 + else:
  311 + Publisher.sendMessage('Set interactor default cursor')
  312 +
  313 + if self.viewer.orientation == "CORONAL":
  314 + if self.status_move == const.CORONAL_UPPER or\
  315 + self.status_move == const.CORONAL_BOTTOM:
  316 + Publisher.sendMessage('Set interactor resize NS cursor')
  317 + elif self.status_move == const.CORONAL_LEFT or\
  318 + self.status_move == const.CORONAL_RIGHT:
  319 + Publisher.sendMessage('Set interactor resize WE cursor')
  320 + elif self.crop_pan == const.CROP_PAN:
  321 + Publisher.sendMessage('Set interactor resize NSWE cursor')
  322 + else:
  323 + Publisher.sendMessage('Set interactor default cursor')
  324 +
  325 + if self.mouse_pressed and self.status_move:
  326 + self.box.UpdatePositionBySideBox((x * xs, y * ys, z * zs),\
  327 + self.viewer.orientation, self.status_move)
  328 +
  329 + nv_x = x - self.last_x
  330 + nv_y = y - self.last_y
  331 + nv_z = z - self.last_z
  332 +
  333 + if self.mouse_pressed and self.crop_pan:
  334 + self.box.UpdatePositionByInsideBox((nv_x * xs, nv_y * ys, nv_z * zs),\
  335 + self.viewer.orientation)
  336 +
  337 + self.last_x = x
  338 + self.last_y = y
  339 + self.last_z = z
  340 +
  341 + Publisher.sendMessage('Redraw canvas')
  342 +
  343 + def ReleaseLeft(self):
  344 + self.status_move = None
  345 +
  346 + def LeftPressed(self, x, y):
  347 + self.mouse_pressed = True
  348 +
  349 + def MouseInLine(self, x, y):
  350 + x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y)
  351 +
  352 + slice_spacing = self.viewer.slice_.spacing
  353 + xs, ys, zs = slice_spacing
  354 +
  355 + if self.viewer.orientation == "AXIAL":
  356 + x_pos_sl = x_pos_sl_ * xs
  357 + y_pos_sl = y_pos_sl_ * ys
  358 +
  359 + for k, p in self.box.axial.iteritems():
  360 + p0 = p[0]
  361 + p1 = p[1]
  362 +
  363 + dist = self.distance_from_point_line((p0[0], p0[1]),\
  364 + (p1[0], p1[1]),\
  365 + (x_pos_sl, y_pos_sl))
  366 +
  367 + if dist <= 2:
  368 + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "AXIAL"):
  369 + self.status_move = k
  370 + break
  371 +
  372 + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "AXIAL")\
  373 + and self.status_move == None:
  374 + self.crop_pan = const.CROP_PAN
  375 + #break
  376 + else:
  377 + if self.crop_pan:
  378 + self.crop_pan = None
  379 + break
  380 +
  381 + if not (self.mouse_pressed) and k != self.status_move:
  382 + self.status_move = None
  383 +
  384 +
  385 + if self.viewer.orientation == "CORONAL":
  386 + x_pos_sl = x_pos_sl_ * xs
  387 + y_pos_sl = y_pos_sl_ * zs
  388 +
  389 + for k, p in self.box.coronal.iteritems():
  390 + p0 = p[0]
  391 + p1 = p[1]
  392 +
  393 + dist = self.distance_from_point_line((p0[0], p0[2]),\
  394 + (p1[0], p1[2]),\
  395 + (x_pos_sl, y_pos_sl))
  396 + if dist <= 2:
  397 + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "CORONAL"):
  398 + self.status_move = k
  399 + break
  400 +
  401 + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "CORONAL")\
  402 + and self.status_move == None:
  403 + self.crop_pan = const.CROP_PAN
  404 + #break
  405 + else:
  406 + if self.crop_pan:
  407 + self.crop_pan = None
  408 + break
  409 +
  410 + if not (self.mouse_pressed) and k != self.status_move:
  411 + self.status_move = None
  412 +
  413 +
  414 + if self.viewer.orientation == "SAGITAL":
  415 + x_pos_sl = x_pos_sl_ * ys
  416 + y_pos_sl = y_pos_sl_ * zs
  417 +
  418 + for k, p in self.box.sagital.iteritems():
  419 + p0 = p[0]
  420 + p1 = p[1]
  421 +
  422 + dist = self.distance_from_point_line((p0[1], p0[2]),\
  423 + (p1[1], p1[2]),\
  424 + (x_pos_sl, y_pos_sl))
  425 +
  426 + if dist <= 2:
  427 + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "SAGITAL"):
  428 + self.status_move = k
  429 + break
  430 +
  431 + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "SAGITAL")\
  432 + and self.status_move == None:
  433 + self.crop_pan = const.CROP_PAN
  434 + #break
  435 + else:
  436 + if self.crop_pan:
  437 + self.crop_pan = None
  438 + break
  439 +
  440 + if not (self.mouse_pressed) and k != self.status_move:
  441 + self.status_move = None
  442 +
  443 +
  444 +
  445 + def draw_to_canvas(self, gc, canvas):
  446 + """
  447 + Draws to an wx.GraphicsContext.
  448 +
  449 + Parameters:
  450 + gc: is a wx.GraphicsContext
  451 + canvas: the canvas it's being drawn.
  452 + """
  453 + self.canvas = canvas
  454 + self.UpdateValues(canvas)
  455 +
  456 + def point_into_box(self, p1, p2, pc, axis):
  457 +
  458 + if axis == "AXIAL":
  459 + if pc[0] > self.box.xi + 10 and pc[0] < self.box.xf - 10\
  460 + and pc[1] - 10 > self.box.yi and pc[1] < self.box.yf - 10:
  461 + return True
  462 + else:
  463 + return False
  464 +
  465 + if axis == "SAGITAL":
  466 + if pc[0] > self.box.yi + 10 and pc[0] < self.box.yf - 10\
  467 + and pc[1] - 10 > self.box.zi and pc[1] < self.box.zf - 10:
  468 + return True
  469 + else:
  470 + return False
  471 +
  472 + if axis == "CORONAL":
  473 + if pc[0] > self.box.xi + 10 and pc[0] < self.box.xf - 10\
  474 + and pc[1] - 10 > self.box.zi and pc[1] < self.box.zf - 10:
  475 + return True
  476 + else:
  477 + return False
  478 +
  479 +
  480 + def point_between_line(self, p1, p2, pc, axis):
  481 + """
  482 + Checks whether a point is in the line limits
  483 + """
  484 +
  485 + if axis == "AXIAL":
  486 + if p1[0] < pc[0] and p2[0] > pc[0]: #x axis
  487 + return True
  488 + elif p1[1] < pc[1] and p2[1] > pc[1]: #y axis
  489 + return True
  490 + else:
  491 + return False
  492 + elif axis == "SAGITAL":
  493 + if p1[1] < pc[0] and p2[1] > pc[0]: #y axis
  494 + return True
  495 + elif p1[2] < pc[1] and p2[2] > pc[1]: #z axis
  496 + return True
  497 + else:
  498 + return False
  499 + elif axis == "CORONAL":
  500 + if p1[0] < pc[0] and p2[0] > pc[0]: #x axis
  501 + return True
  502 + elif p1[2] < pc[1] and p2[2] > pc[1]: #z axis
  503 + return True
  504 + else:
  505 + return False
  506 +
  507 +
  508 + def distance_from_point_line(self, p1, p2, pc):
  509 + """
  510 + Calculate the distance from point pc to a line formed by p1 and p2.
  511 + """
  512 +
  513 + #TODO: Same function into clut_raycasting
  514 + # Create a function to organize it.
  515 +
  516 + # Create a vector pc-p1 and p2-p1
  517 + A = np.array(pc) - np.array(p1)
  518 + B = np.array(p2) - np.array(p1)
  519 + # Calculate the size from those vectors
  520 + len_A = np.linalg.norm(A)
  521 + len_B = np.linalg.norm(B)
  522 + # calculate the angle theta (in radians) between those vector
  523 + theta = math.acos(np.dot(A, B) / (len_A * len_B))
  524 + # Using the sin from theta, calculate the adjacent leg, which is the
  525 + # distance from the point to the line
  526 + distance = math.sin(theta) * len_A
  527 + return distance
  528 +
  529 +
  530 + def Coord3DtoDisplay(self, x, y, z, canvas):
  531 +
  532 + coord = vtk.vtkCoordinate()
  533 + coord.SetValue(x, y, z)
  534 + cx, cy = coord.GetComputedDisplayValue(canvas.evt_renderer)
  535 +
  536 + return (cx, cy)
  537 +
  538 + def MakeBox(self):
  539 +
  540 + slice_size = self.viewer.slice_.matrix.shape
  541 + zf, yf, xf = slice_size[0] - 1, slice_size[1] - 1, slice_size[2] - 1
  542 +
  543 + slice_spacing = self.viewer.slice_.spacing
  544 + xs, ys, zs = slice_spacing
  545 +
  546 + self.box = box = Box()
  547 +
  548 + if self.box.first_run:
  549 + box.SetX(0, xf)
  550 + box.SetY(0, yf)
  551 + box.SetZ(0, zf)
  552 + box.SetSpacing(xs, ys, zs)
  553 + box.MakeMatrix()
  554 +
  555 +
  556 + def UpdateValues(self, canvas):
  557 +
  558 + box = self.box
  559 + slice_number = self.viewer.slice_data.number
  560 +
  561 + slice_spacing = self.viewer.slice_.spacing
  562 + xs, ys, zs = slice_spacing
  563 +
  564 + if canvas.orientation == "AXIAL":
  565 + for points in box.axial.values():
  566 + pi_x, pi_y, pi_z = points[0]
  567 + pf_x, pf_y, pf_z = points[1]
  568 +
  569 + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas)
  570 + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas)
  571 +
  572 + sn = slice_number * zs
  573 + if sn >= box.zi and sn <= box.zf:
  574 + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255))
  575 +
  576 + elif canvas.orientation == "CORONAL":
  577 + for points in box.coronal.values():
  578 + pi_x, pi_y, pi_z = points[0]
  579 + pf_x, pf_y, pf_z = points[1]
  580 +
  581 + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas)
  582 + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas)
  583 +
  584 + sn = slice_number * ys
  585 +
  586 + if sn >= box.yi and sn <= box.yf:
  587 + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255))
  588 +
  589 + elif canvas.orientation == "SAGITAL":
  590 + for points in box.sagital.values():
  591 +
  592 + pi_x, pi_y, pi_z = points[0]
  593 + pf_x, pf_y, pf_z = points[1]
  594 +
  595 + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas)
  596 + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas)
  597 +
  598 + sn = slice_number * xs
  599 + if sn >= box.xi and sn <= box.xf:
  600 + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255))
  601 +
  602 +
  603 + def SetViewer(self, viewer):
  604 + self.viewer = viewer
  605 + self.MakeBox()
  606 +
... ...
invesalius/data/styles.py
... ... @@ -21,6 +21,7 @@ import os
21 21 import multiprocessing
22 22 import tempfile
23 23 import time
  24 +import math
24 25  
25 26 from concurrent import futures
26 27  
... ... @@ -51,6 +52,7 @@ import watershed_process
51 52  
52 53 import utils
53 54 import transformations
  55 +import geometry as geom
54 56  
55 57 ORIENTATIONS = {
56 58 "AXIAL": const.AXIAL,
... ... @@ -1131,7 +1133,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle):
1131 1133 if (self.left_pressed):
1132 1134 cursor = slice_data.cursor
1133 1135 position = self.viewer.get_slice_pixel_coord_by_world_pos(*coord)
1134   - print ">>>", position
1135 1136 radius = cursor.radius
1136 1137  
1137 1138 if position < 0:
... ... @@ -1758,7 +1759,111 @@ class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle):
1758 1759 self._progr_title = _(u"Remove part")
1759 1760 self._progr_msg = _(u"Removing part ...")
1760 1761  
  1762 +class CropMaskConfig(object):
  1763 + __metaclass__= utils.Singleton
  1764 + def __init__(self):
  1765 + self.dlg_visible = False
  1766 +
  1767 +class CropMaskInteractorStyle(DefaultInteractorStyle):
  1768 +
  1769 + def __init__(self, viewer):
  1770 + DefaultInteractorStyle.__init__(self, viewer)
  1771 +
  1772 + self.viewer = viewer
  1773 + self.orientation = self.viewer.orientation
  1774 + self.picker = vtk.vtkWorldPointPicker()
  1775 + self.slice_actor = viewer.slice_data.actor
  1776 + self.slice_data = viewer.slice_data
  1777 + self.draw_retangle = None
  1778 +
  1779 + self.config = CropMaskConfig()
  1780 +
  1781 + def __evts__(self):
  1782 + self.AddObserver("MouseMoveEvent", self.OnMove)
  1783 + self.AddObserver("LeftButtonPressEvent", self.OnLeftPressed)
  1784 + self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton)
  1785 +
  1786 + Publisher.subscribe(self.CropMask, "Crop mask")
  1787 +
  1788 + def OnMove(self, obj, evt):
  1789 + iren = self.viewer.interactor
  1790 + x, y = iren.GetEventPosition()
  1791 + self.draw_retangle.MouseMove(x,y)
  1792 +
  1793 + def OnLeftPressed(self, obj, evt):
  1794 + self.draw_retangle.mouse_pressed = True
  1795 + iren = self.viewer.interactor
  1796 + x, y = iren.GetEventPosition()
  1797 + self.draw_retangle.LeftPressed(x,y)
  1798 +
  1799 + def OnReleaseLeftButton(self, obj, evt):
  1800 + self.draw_retangle.mouse_pressed = False
  1801 + self.draw_retangle.ReleaseLeft()
  1802 +
  1803 + def SetUp(self):
  1804 +
  1805 + self.draw_retangle = geom.DrawCrop2DRetangle()
  1806 + self.draw_retangle.SetViewer(self.viewer)
  1807 +
  1808 + self.viewer.canvas.draw_list.append(self.draw_retangle)
  1809 + self.viewer.UpdateCanvas()
  1810 +
  1811 + if not(self.config.dlg_visible):
  1812 + self.config.dlg_visible = True
  1813 +
  1814 + dlg = dialogs.CropOptionsDialog(self.config)
  1815 + dlg.UpdateValues(self.draw_retangle.box.GetLimits())
  1816 + dlg.Show()
  1817 +
  1818 + self.__evts__()
  1819 + #self.draw_lines()
  1820 + #Publisher.sendMessage('Hide current mask')
  1821 + #Publisher.sendMessage('Reload actual slice')
  1822 +
  1823 + def CleanUp(self):
  1824 +
  1825 + for draw in self.viewer.canvas.draw_list:
  1826 + self.viewer.canvas.draw_list.remove(draw)
  1827 +
  1828 + Publisher.sendMessage('Redraw canvas')
  1829 +
  1830 + def CropMask(self, pubsub_evt):
  1831 + if self.viewer.orientation == "AXIAL":
  1832 +
  1833 + xi, xf, yi, yf, zi, zf = self.draw_retangle.box.GetLimits()
  1834 +
  1835 + xi += 1
  1836 + xf += 1
  1837 +
  1838 + yi += 1
  1839 + yf += 1
  1840 +
  1841 + zi += 1
  1842 + zf += 1
1761 1843  
  1844 + self.viewer.slice_.do_threshold_to_all_slices()
  1845 + cp_mask = self.viewer.slice_.current_mask.matrix.copy()
  1846 +
  1847 + tmp_mask = self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1].copy()
  1848 +
  1849 + self.viewer.slice_.current_mask.matrix[:] = 1
  1850 +
  1851 + self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1] = tmp_mask
  1852 +
  1853 + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask)
  1854 +
  1855 + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask()
  1856 + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask()
  1857 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask()
  1858 +
  1859 + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask()
  1860 + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask()
  1861 + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask()
  1862 +
  1863 + self.viewer.slice_.current_mask.was_edited = True
  1864 + Publisher.sendMessage('Reload actual slice')
  1865 +
  1866 +
1762 1867 class SelectPartConfig(object):
1763 1868 __metaclass__= utils.Singleton
1764 1869 def __init__(self):
... ... @@ -2054,7 +2159,9 @@ def get_style(style):
2054 2159 const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle,
2055 2160 const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle,
2056 2161 const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle,
  2162 + const.SLICE_STATE_CROP_MASK:CropMaskInteractorStyle,
2057 2163 }
2058 2164 return STYLES[style]
2059 2165  
2060 2166  
  2167 +
... ...
invesalius/data/viewer_slice.py
... ... @@ -148,7 +148,7 @@ class ContourMIPConfig(wx.Panel):
148 148  
149 149  
150 150 class CanvasRendererCTX:
151   - def __init__(self, evt_renderer, canvas_renderer):
  151 + def __init__(self, evt_renderer, canvas_renderer, orientation=None):
152 152 """
153 153 A Canvas to render over a vtktRenderer.
154 154  
... ... @@ -167,6 +167,7 @@ class CanvasRendererCTX:
167 167 self.evt_renderer = evt_renderer
168 168 self._size = self.canvas_renderer.GetSize()
169 169 self.draw_list = []
  170 + self.orientation = orientation
170 171 self.gc = None
171 172 self.last_cam_modif_time = -1
172 173 self.modified = True
... ... @@ -641,7 +642,7 @@ class Viewer(wx.Panel):
641 642 self.style.CleanUp()
642 643  
643 644 del self.style
644   -
  645 +
645 646 style = styles.get_style(state)(self)
646 647  
647 648 setup = getattr(style, 'SetUp', None)
... ... @@ -1188,6 +1189,10 @@ class Viewer(wx.Panel):
1188 1189 Publisher.subscribe(self.OnExportPicture,'Export picture to file')
1189 1190 Publisher.subscribe(self.SetDefaultCursor, 'Set interactor default cursor')
1190 1191  
  1192 + Publisher.subscribe(self.SetSizeNSCursor, 'Set interactor resize NS cursor')
  1193 + Publisher.subscribe(self.SetSizeWECursor, 'Set interactor resize WE cursor')
  1194 + Publisher.subscribe(self.SetSizeNWSECursor, 'Set interactor resize NSWE cursor')
  1195 +
1191 1196 Publisher.subscribe(self.AddActors, 'Add actors ' + str(ORIENTATIONS[self.orientation]))
1192 1197 Publisher.subscribe(self.RemoveActors, 'Remove actors ' + str(ORIENTATIONS[self.orientation]))
1193 1198 Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes')
... ... @@ -1216,6 +1221,15 @@ class Viewer(wx.Panel):
1216 1221 def SetDefaultCursor(self, pusub_evt):
1217 1222 self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
1218 1223  
  1224 + def SetSizeNSCursor(self, pusub_evt):
  1225 + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS))
  1226 +
  1227 + def SetSizeWECursor(self, pusub_evt):
  1228 + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZEWE))
  1229 +
  1230 + def SetSizeNWSECursor(self, pubsub_evt):
  1231 + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZENWSE))
  1232 +
1219 1233 def OnExportPicture(self, pubsub_evt):
1220 1234 Publisher.sendMessage('Begin busy cursor')
1221 1235 view_prop_list = []
... ... @@ -1384,7 +1398,7 @@ class Viewer(wx.Panel):
1384 1398 self.cam = self.slice_data.renderer.GetActiveCamera()
1385 1399 self.__build_cross_lines()
1386 1400  
1387   - self.canvas = CanvasRendererCTX(self.slice_data.renderer, self.slice_data.canvas_renderer)
  1401 + self.canvas = CanvasRendererCTX(self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation)
1388 1402  
1389 1403 # Set the slice number to the last slice to ensure the camera if far
1390 1404 # enough to show all slices.
... ...
invesalius/gui/dialogs.py
... ... @@ -1696,7 +1696,7 @@ class ImportBitmapParameters(wx.Dialog):
1696 1696 else:
1697 1697 size=wx.Size(380,210)
1698 1698  
1699   - pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Create project from bitmap"),size=wx.Size(380,220),\
  1699 + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Create project from bitmap"),size=size,\
1700 1700 style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP)
1701 1701  
1702 1702 self.interval = 0
... ... @@ -2032,7 +2032,6 @@ class SelectPartsOptionsDialog(wx.Dialog):
2032 2032 evt.Skip()
2033 2033 self.Destroy()
2034 2034  
2035   -
2036 2035 class FFillSegmentationOptionsDialog(wx.Dialog):
2037 2036 def __init__(self, config):
2038 2037 pre = wx.PreDialog()
... ... @@ -2205,3 +2204,124 @@ class FFillSegmentationOptionsDialog(wx.Dialog):
2205 2204 Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL)
2206 2205 evt.Skip()
2207 2206 self.Destroy()
  2207 +
  2208 +class CropOptionsDialog(wx.Dialog):
  2209 +
  2210 + def __init__(self, config):
  2211 +
  2212 + self.config = config
  2213 +
  2214 + pre = wx.PreDialog()
  2215 +
  2216 + if sys.platform == 'win32':
  2217 + size=wx.Size(240,180)
  2218 + else:
  2219 + size=wx.Size(205,180)
  2220 +
  2221 + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Crop mask"),\
  2222 + size=size, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT)
  2223 +
  2224 + self.PostCreate(pre)
  2225 +
  2226 + self._init_gui()
  2227 + #self.config = config
  2228 +
  2229 + def UpdateValues(self, pubsub_evt):
  2230 +
  2231 + if type(pubsub_evt) == list:
  2232 + data = pubsub_evt
  2233 + else:
  2234 + data = pubsub_evt.data
  2235 +
  2236 + xi, xf, yi, yf, zi, zf = data
  2237 +
  2238 + self.tx_axial_i.SetValue(str(zi))
  2239 + self.tx_axial_f.SetValue(str(zf))
  2240 +
  2241 + self.tx_sagital_i.SetValue(str(xi))
  2242 + self.tx_sagital_f.SetValue(str(xf))
  2243 +
  2244 + self.tx_coronal_i.SetValue(str(yi))
  2245 + self.tx_coronal_f.SetValue(str(yf))
  2246 +
  2247 + def _init_gui(self):
  2248 +
  2249 +
  2250 + p = wx.Panel(self, -1, style = wx.TAB_TRAVERSAL
  2251 + | wx.CLIP_CHILDREN
  2252 + | wx.FULL_REPAINT_ON_RESIZE)
  2253 +
  2254 + gbs_principal = self.gbs = wx.GridBagSizer(4,1)
  2255 +
  2256 + gbs = self.gbs = wx.GridBagSizer(3, 4)
  2257 +
  2258 + flag_labels = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL
  2259 +
  2260 + stx_axial = wx.StaticText(p, -1, _(u"Axial:"))
  2261 + self.tx_axial_i = tx_axial_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2262 + stx_axial_t = wx.StaticText(p, -1, _(u" - "))
  2263 + self.tx_axial_f = tx_axial_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2264 +
  2265 + gbs.Add(stx_axial, (0,0), flag=flag_labels)
  2266 + gbs.Add(tx_axial_i, (0,1))
  2267 + gbs.Add(stx_axial_t, (0,2), flag=flag_labels)
  2268 + gbs.Add(tx_axial_f, (0,3))
  2269 +
  2270 + stx_sagital = wx.StaticText(p, -1, _(u"Sagital:"))
  2271 + self.tx_sagital_i = tx_sagital_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2272 + stx_sagital_t = wx.StaticText(p, -1, _(u" - "))
  2273 + self.tx_sagital_f = tx_sagital_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2274 +
  2275 + gbs.Add(stx_sagital, (1,0), flag=flag_labels)
  2276 + gbs.Add(tx_sagital_i, (1,1))
  2277 + gbs.Add(stx_sagital_t, (1,2), flag=flag_labels)
  2278 + gbs.Add(tx_sagital_f, (1,3))
  2279 +
  2280 + stx_coronal = wx.StaticText(p, -1, _(u"Coronal:"))
  2281 + self.tx_coronal_i = tx_coronal_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2282 + stx_coronal_t = wx.StaticText(p, -1, _(u" - "))
  2283 + self.tx_coronal_f = tx_coronal_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1))
  2284 +
  2285 + gbs.Add(stx_coronal, (2,0), flag=flag_labels)
  2286 + gbs.Add(tx_coronal_i, (2,1))
  2287 + gbs.Add(stx_coronal_t, (2,2), flag=flag_labels)
  2288 + gbs.Add(tx_coronal_f, (2,3))
  2289 +
  2290 + gbs_button = wx.GridBagSizer(2, 4)
  2291 +
  2292 + btn_ok = self.btn_ok= wx.Button(p, wx.ID_OK)
  2293 + btn_ok.SetDefault()
  2294 +
  2295 + btn_cancel = wx.Button(p, wx.ID_CANCEL)
  2296 +
  2297 + gbs_button.Add(btn_cancel, (0,0))
  2298 + gbs_button.Add(btn_ok, (0,1))
  2299 +
  2300 + gbs_principal.AddSizer(gbs, (0,0), flag = wx.ALL|wx.EXPAND)
  2301 + gbs_principal.AddStretchSpacer((1,0))
  2302 + gbs_principal.AddStretchSpacer((2,0))
  2303 + gbs_principal.AddSizer(gbs_button, (3,0), flag = wx.ALIGN_RIGHT)
  2304 +
  2305 + box = wx.BoxSizer()
  2306 + box.AddSizer(gbs_principal, 1, wx.ALL|wx.EXPAND, 10)
  2307 +
  2308 + p.SetSizer(box)
  2309 +
  2310 + Publisher.subscribe(self.UpdateValues, 'Update crop limits into gui')
  2311 +
  2312 + btn_ok.Bind(wx.EVT_BUTTON, self.OnOk)
  2313 + btn_cancel.Bind(wx.EVT_BUTTON, self.OnClose)
  2314 + self.Bind(wx.EVT_CLOSE, self.OnClose)
  2315 +
  2316 +
  2317 + def OnOk(self, evt):
  2318 + self.config.dlg_visible = False
  2319 + Publisher.sendMessage('Crop mask')
  2320 + Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK)
  2321 + evt.Skip()
  2322 +
  2323 + def OnClose(self, evt):
  2324 + self.config.dlg_visible = False
  2325 + Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK)
  2326 + evt.Skip()
  2327 + self.Destroy()
... ...
invesalius/gui/frame.py
... ... @@ -463,6 +463,12 @@ class Frame(wx.Frame):
463 463 self.OnInterpolatedSlices(True)
464 464 else:
465 465 self.OnInterpolatedSlices(False)
  466 + elif id == const.ID_CROP_MASK:
  467 + self.OnCropMask()
  468 +
  469 + def OnInterpolatedSlices(self, status):
  470 + Publisher.sendMessage('Set interpolated slices', status)
  471 +
466 472  
467 473 def OnSize(self, evt):
468 474 """
... ... @@ -601,6 +607,9 @@ class Frame(wx.Frame):
601 607  
602 608 def OnInterpolatedSlices(self, status):
603 609 Publisher.sendMessage('Set interpolated slices', status)
  610 +
  611 + def OnCropMask(self):
  612 + Publisher.sendMessage('Enable style', const.SLICE_STATE_CROP_MASK)
604 613  
605 614 # ------------------------------------------------------------------
606 615 # ------------------------------------------------------------------
... ... @@ -737,6 +746,7 @@ class MenuBar(wx.MenuBar):
737 746 self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A"))
738 747 self.clean_mask_menu.Enable(False)
739 748  
  749 + mask_menu.AppendSeparator()
740 750 self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually"))
741 751 self.fill_hole_mask_menu.Enable(False)
742 752  
... ... @@ -745,6 +755,11 @@ class MenuBar(wx.MenuBar):
745 755  
746 756 self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts"))
747 757 self.select_mask_part_menu.Enable(False)
  758 +
  759 + mask_menu.AppendSeparator()
  760 +
  761 + self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop"))
  762 + self.crop_mask_menu.Enable(False)
748 763  
749 764 tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu)
750 765  
... ... @@ -892,6 +907,7 @@ class MenuBar(wx.MenuBar):
892 907 def OnShowMask(self, pubsub_evt):
893 908 index, value = pubsub_evt.data
894 909 self.clean_mask_menu.Enable(value)
  910 + self.crop_mask_menu.Enable(value)
895 911  
896 912  
897 913 # ------------------------------------------------------------------
... ...