From 7055b3d716d7b9892d6c36c263dec561cef0aeaf Mon Sep 17 00:00:00 2001 From: Paulo Henrique Junqueira Amorim Date: Thu, 15 Sep 2016 09:51:02 -0300 Subject: [PATCH] ADD: Added crop mask tool. --- invesalius/constants.py | 24 ++++++++++++++++++++++++ invesalius/data/geometry.py | 606 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ invesalius/data/styles.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- invesalius/data/viewer_slice.py | 20 +++++++++++++++++--- invesalius/gui/dialogs.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- invesalius/gui/frame.py | 16 ++++++++++++++++ 6 files changed, 893 insertions(+), 6 deletions(-) create mode 100644 invesalius/data/geometry.py diff --git a/invesalius/constants.py b/invesalius/constants.py index 9f0dbeb..527b5e8 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -141,7 +141,24 @@ MODE_NAVIGATOR = 1 MODE_RADIOLOGY = 2 MODE_ODONTOLOGY = 3 +#Crop box sides code +AXIAL_RIGHT = 1 +AXIAL_LEFT = 2 +AXIAL_UPPER = 3 +AXIAL_BOTTOM = 4 + +SAGITAL_RIGHT = 5 +SAGITAL_LEFT = 6 +SAGITAL_UPPER = 7 +SAGITAL_BOTTOM = 8 + +CORONAL_RIGHT = 9 +CORONAL_LEFT = 10 +CORONAL_UPPER = 11 +CORONAL_BOTTOM = 12 + +CROP_PAN = 13 #Color Table from Slice #NumberOfColors, SaturationRange, HueRange, ValueRange @@ -485,6 +502,8 @@ ID_FLOODFILL_MASK = wx.NewId() ID_REMOVE_MASK_PART = wx.NewId() ID_SELECT_MASK_PART = wx.NewId() ID_FLOODFILL_SEGMENTATION = wx.NewId() +ID_CROP_MASK = wx.NewId() +ID_SELECT_MASK_PART = wx.NewId() #--------------------------------------------------------- STATE_DEFAULT = 1000 @@ -506,6 +525,8 @@ SLICE_STATE_MASK_FFILL = 3011 SLICE_STATE_REMOVE_MASK_PARTS = 3012 SLICE_STATE_SELECT_MASK_PARTS = 3013 SLICE_STATE_FFILL_SEGMENTATION = 3014 +SLICE_STATE_CROP_MASK = 3015 +SLICE_STATE_SELECT_MASK_PARTS = 3014 VOLUME_STATE_SEED = 2001 # STATE_LINEAR_MEASURE = 3001 @@ -527,6 +548,8 @@ SLICE_STYLES.append(SLICE_STATE_MASK_FFILL) SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS) SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS) SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION) +SLICE_STYLES.append(SLICE_STATE_CROP_MASK) +SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -542,6 +565,7 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, SLICE_STATE_CROSS: 2, SLICE_STATE_SCROLL: 2, SLICE_STATE_REORIENT: 2, + SLICE_STATE_CROP_MASK: 1, STATE_ANNOTATE: 2, STATE_DEFAULT: 0, STATE_MEASURE_ANGLE: 2, diff --git a/invesalius/data/geometry.py b/invesalius/data/geometry.py new file mode 100644 index 0000000..4e30704 --- /dev/null +++ b/invesalius/data/geometry.py @@ -0,0 +1,606 @@ +#-------------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br / +# http://www.cti.gov.br/invesalius +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +#-------------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +#-------------------------------------------------------------------------- + +import numpy as np +import math +import vtk +from wx.lib.pubsub import pub as Publisher + +import utils +import constants as const + + +class Box(object): + """ + This class is a data structure for storing the + coordinates (min and max) of box used in crop-mask. + """ + + __metaclass__= utils.Singleton + + def __init__(self): + self.xi = None + self.xf = None + + self.yi = None + self.yf = None + + self.zi = None + self.zf = None + + self.size_x = None + self.size_y = None + self.size_z = None + + self.sagital = {} + self.coronal = {} + self.axial = {} + + self.xs = None + self.ys = None + self.zs = None + + self.first_run = True + + def SetX(self, i, f): + self.xi = i + self.xf = f + self.size_x = f + + def SetY(self, i, f): + self.yi = i + self.yf = f + self.size_y = f + + def SetZ(self, i, f): + self.zi = i + self.zf = f + self.size_z = f + + def SetSpacing(self, x, y, z): + self.xs = x + self.ys = y + self.zs = z + + self.xi = self.xi * self.xs + self.xf = self.xf * self.xs + + self.yi = self.yi * self.ys + self.yf = self.yf * self.ys + + self.zi = self.zi * self.zs + self.zf = self.zf * self.zs + + self.size_x = self.size_x * self.xs + self.size_y = self.size_y * self.ys + self.size_z = self.size_z * self.zs + + if self.first_run: + self.first_run = False + + def MakeMatrix(self): + """ + Update values in a matrix to each orientation. + """ + + self.sagital[const.SAGITAL_LEFT] = [[self.xi, self.yi - (self.ys/2), self.zi],\ + [self.xi, self.yi - (self.ys/2), self.zf]] + + self.sagital[const.SAGITAL_RIGHT] = [[self.xi, self.yf + (self.ys/2), self.zi],\ + [self.xi, self.yf + (self.ys/2), self.zf]] + + self.sagital[const.SAGITAL_BOTTOM] = [[self.xi, self.yi, self.zi - (self.zs/2)],\ + [self.xi, self.yf, self.zi - (self.zs/2)]] + + self.sagital[const.SAGITAL_UPPER] = [[self.xi, self.yi, self.zf + (self.zs/2) ],\ + [self.xi, self.yf, self.zf + (self.zs/2) ]] + + self.coronal[const.CORONAL_BOTTOM] = [[self.xi, self.yi, self.zi - (self.zs/2)],\ + [self.xf, self.yf, self.zi - (self.zs/2)]] + + self.coronal[const.CORONAL_UPPER] = [[self.xi, self.yi, self.zf + (self.zs/2)],\ + [self.xf, self.yf, self.zf + (self.zs/2)]] + + self.coronal[const.CORONAL_LEFT] = [[self.xi - (self.xs/2), self.yi, self.zi],\ + [self.xi - (self.xs/2), self.yf, self.zf]] + + self.coronal[const.CORONAL_RIGHT] = [[self.xf + (self.xs/2), self.yi, self.zi],\ + [self.xf + (self.xs/2), self.yf, self.zf]] + + self.axial[const.AXIAL_BOTTOM] = [[self.xi, self.yi - (self.ys/2), self.zi],\ + [self.xf, self.yi - (self.ys/2), self.zf]] + + self.axial[const.AXIAL_UPPER] = [[self.xi, self.yf + (self.ys/2), self.zi],\ + [self.xf, self.yf + (self.ys/2), self.zf]] + + self.axial[const.AXIAL_LEFT] = [[self.xi - (self.xs/2), self.yi, self.zi],\ + [self.xi - (self.xs/2), self.yf, self.zf]] + + self.axial[const.AXIAL_RIGHT] = [[self.xf + (self.xs/2), self.yi, self.zi],\ + [self.xf + (self.xs/2), self.yf, self.zf]] + + Publisher.sendMessage('Update crop limits into gui', self.GetLimits()) + + + def GetLimits(self): + """ + Return the bounding box limits (initial and final) in x, y and z. + """ + + limits = [ int(self.xi / self.xs), int(self.xf / self. xs),\ + int(self.yi / self.ys), int(self.yf / self.ys),\ + int(self.zi / self.zs), int(self.zf / self.zs)] + + return limits + + + def UpdatePositionBySideBox(self, pc, axis, position): + """ + Checks the coordinates are in any side of box and update it. + Is necessary to move limits of box. + """ + + if axis == "AXIAL": + if position == const.AXIAL_UPPER: + if pc[1] > self.yi and pc[1] > 0 and pc[1] <= self.size_y: + self.yf = pc[1] + + if position == const.AXIAL_BOTTOM: + if pc[1] < self.yf and pc[1] >= 0: + self.yi = pc[1] + + if position == const.AXIAL_LEFT: + if pc[0] < self.xf and pc[0] >= 0: + self.xi = pc[0] + + + if position == const.AXIAL_RIGHT: + if pc[0] > self.xi and pc[0] <= self.size_x: + self.xf = pc[0] + + + if axis == "SAGITAL": + if position == const.SAGITAL_UPPER: + if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z: + self.zf = pc[2] + + if position == const.SAGITAL_BOTTOM: + if pc[2] < self.zf and pc[2] >= 0: + self.zi = pc[2] + + if position == const.SAGITAL_LEFT: + if pc[1] < self.yf and pc[1] >= 0: + self.yi = pc[1] + + if position == const.SAGITAL_RIGHT: + if pc[1] > self.yi and pc[1] <= self.size_y: + self.yf = pc[1] + + + if axis == "CORONAL": + if position == const.CORONAL_UPPER: + if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z: + self.zf = pc[2] + + if position == const.CORONAL_BOTTOM: + if pc[2] < self.zf and pc[2] >= 0: + self.zi = pc[2] + + if position == const.CORONAL_LEFT: + if pc[0] < self.xf and pc[0] >= 0: + self.xi = pc[0] + + if position == const.CORONAL_RIGHT: + if pc[0] > self.yi and pc[0] <= self.size_y: + self.xf = pc[0] + + self.MakeMatrix() + + + def UpdatePositionByInsideBox(self, pc, axis): + """ + Checks the coordinates are inside the box and update it. + Is necessary to move box in pan event. + """ + + if axis == "AXIAL": + + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0: + self.yf = self.yf + pc[1] + self.yi = self.yi + pc[1] + + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0: + self.xf = self.xf + pc[0] + self.xi = self.xi + pc[0] + + if axis == "SAGITAL": + + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0: + self.yf = self.yf + pc[1] + self.yi = self.yi + pc[1] + + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0: + self.zf = self.zf + pc[2] + self.zi = self.zi + pc[2] + + if axis == "CORONAL": + + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0: + self.xf = self.xf + pc[0] + self.xi = self.xi + pc[0] + + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0: + self.zf = self.zf + pc[2] + self.zi = self.zi + pc[2] + + self.MakeMatrix() + + + +class DrawCrop2DRetangle(): + """ + This class is responsible for draw and control user + interactions with the box. Each side of box is displayed in an + anatomical orientation (axial, sagital or coronal). + """ + def __init__(self): + self.viewer = None + self.points_in_display = {} + self.box = None + self.mouse_pressed = False + self.canvas = None + self.status_move = None + self.crop_pan = None + self.last_x = 0 + self.last_y = 0 + self.last_z = 0 + + def MouseMove(self, x, y): + + self.MouseInLine(x, y) + + x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y) + slice_spacing = self.viewer.slice_.spacing + xs, ys, zs = slice_spacing + + x_pos_sl = x_pos_sl_ * xs + y_pos_sl = y_pos_sl_ * ys + + x, y, z = self.viewer.get_voxel_coord_by_screen_pos(x, y) + + if self.viewer.orientation == "AXIAL": + + if self.status_move == const.AXIAL_UPPER or\ + self.status_move == const.AXIAL_BOTTOM: + Publisher.sendMessage('Set interactor resize NS cursor') + elif self.status_move == const.AXIAL_LEFT or\ + self.status_move == const.AXIAL_RIGHT: + Publisher.sendMessage('Set interactor resize WE cursor') + elif self.crop_pan == const.CROP_PAN: + Publisher.sendMessage('Set interactor resize NSWE cursor') + else: + Publisher.sendMessage('Set interactor default cursor') + + if self.viewer.orientation == "SAGITAL": + if self.status_move == const.SAGITAL_UPPER or\ + self.status_move == const.SAGITAL_BOTTOM: + Publisher.sendMessage('Set interactor resize NS cursor') + elif self.status_move == const.SAGITAL_LEFT or\ + self.status_move == const.SAGITAL_RIGHT: + Publisher.sendMessage('Set interactor resize WE cursor') + elif self.crop_pan == const.CROP_PAN: + Publisher.sendMessage('Set interactor resize NSWE cursor') + else: + Publisher.sendMessage('Set interactor default cursor') + + if self.viewer.orientation == "CORONAL": + if self.status_move == const.CORONAL_UPPER or\ + self.status_move == const.CORONAL_BOTTOM: + Publisher.sendMessage('Set interactor resize NS cursor') + elif self.status_move == const.CORONAL_LEFT or\ + self.status_move == const.CORONAL_RIGHT: + Publisher.sendMessage('Set interactor resize WE cursor') + elif self.crop_pan == const.CROP_PAN: + Publisher.sendMessage('Set interactor resize NSWE cursor') + else: + Publisher.sendMessage('Set interactor default cursor') + + if self.mouse_pressed and self.status_move: + self.box.UpdatePositionBySideBox((x * xs, y * ys, z * zs),\ + self.viewer.orientation, self.status_move) + + nv_x = x - self.last_x + nv_y = y - self.last_y + nv_z = z - self.last_z + + if self.mouse_pressed and self.crop_pan: + self.box.UpdatePositionByInsideBox((nv_x * xs, nv_y * ys, nv_z * zs),\ + self.viewer.orientation) + + self.last_x = x + self.last_y = y + self.last_z = z + + Publisher.sendMessage('Redraw canvas') + + def ReleaseLeft(self): + self.status_move = None + + def LeftPressed(self, x, y): + self.mouse_pressed = True + + def MouseInLine(self, x, y): + x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y) + + slice_spacing = self.viewer.slice_.spacing + xs, ys, zs = slice_spacing + + if self.viewer.orientation == "AXIAL": + x_pos_sl = x_pos_sl_ * xs + y_pos_sl = y_pos_sl_ * ys + + for k, p in self.box.axial.iteritems(): + p0 = p[0] + p1 = p[1] + + dist = self.distance_from_point_line((p0[0], p0[1]),\ + (p1[0], p1[1]),\ + (x_pos_sl, y_pos_sl)) + + if dist <= 2: + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "AXIAL"): + self.status_move = k + break + + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "AXIAL")\ + and self.status_move == None: + self.crop_pan = const.CROP_PAN + #break + else: + if self.crop_pan: + self.crop_pan = None + break + + if not (self.mouse_pressed) and k != self.status_move: + self.status_move = None + + + if self.viewer.orientation == "CORONAL": + x_pos_sl = x_pos_sl_ * xs + y_pos_sl = y_pos_sl_ * zs + + for k, p in self.box.coronal.iteritems(): + p0 = p[0] + p1 = p[1] + + dist = self.distance_from_point_line((p0[0], p0[2]),\ + (p1[0], p1[2]),\ + (x_pos_sl, y_pos_sl)) + if dist <= 2: + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "CORONAL"): + self.status_move = k + break + + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "CORONAL")\ + and self.status_move == None: + self.crop_pan = const.CROP_PAN + #break + else: + if self.crop_pan: + self.crop_pan = None + break + + if not (self.mouse_pressed) and k != self.status_move: + self.status_move = None + + + if self.viewer.orientation == "SAGITAL": + x_pos_sl = x_pos_sl_ * ys + y_pos_sl = y_pos_sl_ * zs + + for k, p in self.box.sagital.iteritems(): + p0 = p[0] + p1 = p[1] + + dist = self.distance_from_point_line((p0[1], p0[2]),\ + (p1[1], p1[2]),\ + (x_pos_sl, y_pos_sl)) + + if dist <= 2: + if self.point_between_line(p0, p1, (x_pos_sl, y_pos_sl), "SAGITAL"): + self.status_move = k + break + + if self.point_into_box(p0, p1, (x_pos_sl, y_pos_sl), "SAGITAL")\ + and self.status_move == None: + self.crop_pan = const.CROP_PAN + #break + else: + if self.crop_pan: + self.crop_pan = None + break + + if not (self.mouse_pressed) and k != self.status_move: + self.status_move = None + + + + def draw_to_canvas(self, gc, canvas): + """ + Draws to an wx.GraphicsContext. + + Parameters: + gc: is a wx.GraphicsContext + canvas: the canvas it's being drawn. + """ + self.canvas = canvas + self.UpdateValues(canvas) + + def point_into_box(self, p1, p2, pc, axis): + + if axis == "AXIAL": + if pc[0] > self.box.xi + 10 and pc[0] < self.box.xf - 10\ + and pc[1] - 10 > self.box.yi and pc[1] < self.box.yf - 10: + return True + else: + return False + + if axis == "SAGITAL": + if pc[0] > self.box.yi + 10 and pc[0] < self.box.yf - 10\ + and pc[1] - 10 > self.box.zi and pc[1] < self.box.zf - 10: + return True + else: + return False + + if axis == "CORONAL": + if pc[0] > self.box.xi + 10 and pc[0] < self.box.xf - 10\ + and pc[1] - 10 > self.box.zi and pc[1] < self.box.zf - 10: + return True + else: + return False + + + def point_between_line(self, p1, p2, pc, axis): + """ + Checks whether a point is in the line limits + """ + + if axis == "AXIAL": + if p1[0] < pc[0] and p2[0] > pc[0]: #x axis + return True + elif p1[1] < pc[1] and p2[1] > pc[1]: #y axis + return True + else: + return False + elif axis == "SAGITAL": + if p1[1] < pc[0] and p2[1] > pc[0]: #y axis + return True + elif p1[2] < pc[1] and p2[2] > pc[1]: #z axis + return True + else: + return False + elif axis == "CORONAL": + if p1[0] < pc[0] and p2[0] > pc[0]: #x axis + return True + elif p1[2] < pc[1] and p2[2] > pc[1]: #z axis + return True + else: + return False + + + def distance_from_point_line(self, p1, p2, pc): + """ + Calculate the distance from point pc to a line formed by p1 and p2. + """ + + #TODO: Same function into clut_raycasting + # Create a function to organize it. + + # Create a vector pc-p1 and p2-p1 + A = np.array(pc) - np.array(p1) + B = np.array(p2) - np.array(p1) + # Calculate the size from those vectors + len_A = np.linalg.norm(A) + len_B = np.linalg.norm(B) + # calculate the angle theta (in radians) between those vector + theta = math.acos(np.dot(A, B) / (len_A * len_B)) + # Using the sin from theta, calculate the adjacent leg, which is the + # distance from the point to the line + distance = math.sin(theta) * len_A + return distance + + + def Coord3DtoDisplay(self, x, y, z, canvas): + + coord = vtk.vtkCoordinate() + coord.SetValue(x, y, z) + cx, cy = coord.GetComputedDisplayValue(canvas.evt_renderer) + + return (cx, cy) + + def MakeBox(self): + + slice_size = self.viewer.slice_.matrix.shape + zf, yf, xf = slice_size[0] - 1, slice_size[1] - 1, slice_size[2] - 1 + + slice_spacing = self.viewer.slice_.spacing + xs, ys, zs = slice_spacing + + self.box = box = Box() + + if self.box.first_run: + box.SetX(0, xf) + box.SetY(0, yf) + box.SetZ(0, zf) + box.SetSpacing(xs, ys, zs) + box.MakeMatrix() + + + def UpdateValues(self, canvas): + + box = self.box + slice_number = self.viewer.slice_data.number + + slice_spacing = self.viewer.slice_.spacing + xs, ys, zs = slice_spacing + + if canvas.orientation == "AXIAL": + for points in box.axial.values(): + pi_x, pi_y, pi_z = points[0] + pf_x, pf_y, pf_z = points[1] + + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas) + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas) + + sn = slice_number * zs + if sn >= box.zi and sn <= box.zf: + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255)) + + elif canvas.orientation == "CORONAL": + for points in box.coronal.values(): + pi_x, pi_y, pi_z = points[0] + pf_x, pf_y, pf_z = points[1] + + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas) + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas) + + sn = slice_number * ys + + if sn >= box.yi and sn <= box.yf: + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255)) + + elif canvas.orientation == "SAGITAL": + for points in box.sagital.values(): + + pi_x, pi_y, pi_z = points[0] + pf_x, pf_y, pf_z = points[1] + + s_cxi, s_cyi = self.Coord3DtoDisplay(pi_x, pi_y, pi_z, canvas) + s_cxf, s_cyf = self.Coord3DtoDisplay(pf_x, pf_y, pf_z ,canvas) + + sn = slice_number * xs + if sn >= box.xi and sn <= box.xf: + canvas.draw_line((s_cxi, s_cyi),(s_cxf, s_cyf), colour=(255,255,255,255)) + + + def SetViewer(self, viewer): + self.viewer = viewer + self.MakeBox() + diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index d266964..5a36b27 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -21,6 +21,7 @@ import os import multiprocessing import tempfile import time +import math from concurrent import futures @@ -51,6 +52,7 @@ import watershed_process import utils import transformations +import geometry as geom ORIENTATIONS = { "AXIAL": const.AXIAL, @@ -1131,7 +1133,6 @@ class WaterShedInteractorStyle(DefaultInteractorStyle): if (self.left_pressed): cursor = slice_data.cursor position = self.viewer.get_slice_pixel_coord_by_world_pos(*coord) - print ">>>", position radius = cursor.radius if position < 0: @@ -1758,7 +1759,111 @@ class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle): self._progr_title = _(u"Remove part") self._progr_msg = _(u"Removing part ...") +class CropMaskConfig(object): + __metaclass__= utils.Singleton + def __init__(self): + self.dlg_visible = False + +class CropMaskInteractorStyle(DefaultInteractorStyle): + + def __init__(self, viewer): + DefaultInteractorStyle.__init__(self, viewer) + + self.viewer = viewer + self.orientation = self.viewer.orientation + self.picker = vtk.vtkWorldPointPicker() + self.slice_actor = viewer.slice_data.actor + self.slice_data = viewer.slice_data + self.draw_retangle = None + + self.config = CropMaskConfig() + + def __evts__(self): + self.AddObserver("MouseMoveEvent", self.OnMove) + self.AddObserver("LeftButtonPressEvent", self.OnLeftPressed) + self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton) + + Publisher.subscribe(self.CropMask, "Crop mask") + + def OnMove(self, obj, evt): + iren = self.viewer.interactor + x, y = iren.GetEventPosition() + self.draw_retangle.MouseMove(x,y) + + def OnLeftPressed(self, obj, evt): + self.draw_retangle.mouse_pressed = True + iren = self.viewer.interactor + x, y = iren.GetEventPosition() + self.draw_retangle.LeftPressed(x,y) + + def OnReleaseLeftButton(self, obj, evt): + self.draw_retangle.mouse_pressed = False + self.draw_retangle.ReleaseLeft() + + def SetUp(self): + + self.draw_retangle = geom.DrawCrop2DRetangle() + self.draw_retangle.SetViewer(self.viewer) + + self.viewer.canvas.draw_list.append(self.draw_retangle) + self.viewer.UpdateCanvas() + + if not(self.config.dlg_visible): + self.config.dlg_visible = True + + dlg = dialogs.CropOptionsDialog(self.config) + dlg.UpdateValues(self.draw_retangle.box.GetLimits()) + dlg.Show() + + self.__evts__() + #self.draw_lines() + #Publisher.sendMessage('Hide current mask') + #Publisher.sendMessage('Reload actual slice') + + def CleanUp(self): + + for draw in self.viewer.canvas.draw_list: + self.viewer.canvas.draw_list.remove(draw) + + Publisher.sendMessage('Redraw canvas') + + def CropMask(self, pubsub_evt): + if self.viewer.orientation == "AXIAL": + + xi, xf, yi, yf, zi, zf = self.draw_retangle.box.GetLimits() + + xi += 1 + xf += 1 + + yi += 1 + yf += 1 + + zi += 1 + zf += 1 + self.viewer.slice_.do_threshold_to_all_slices() + cp_mask = self.viewer.slice_.current_mask.matrix.copy() + + tmp_mask = self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1].copy() + + self.viewer.slice_.current_mask.matrix[:] = 1 + + self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1] = tmp_mask + + self.viewer.slice_.current_mask.save_history(0, 'VOLUME', self.viewer.slice_.current_mask.matrix.copy(), cp_mask) + + self.viewer.slice_.buffer_slices['AXIAL'].discard_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_mask() + + self.viewer.slice_.buffer_slices['AXIAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() + self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() + + self.viewer.slice_.current_mask.was_edited = True + Publisher.sendMessage('Reload actual slice') + + class SelectPartConfig(object): __metaclass__= utils.Singleton def __init__(self): @@ -2054,7 +2159,9 @@ def get_style(style): const.SLICE_STATE_REMOVE_MASK_PARTS: RemoveMaskPartsInteractorStyle, const.SLICE_STATE_SELECT_MASK_PARTS: SelectMaskPartsInteractorStyle, const.SLICE_STATE_FFILL_SEGMENTATION: FloodFillSegmentInteractorStyle, + const.SLICE_STATE_CROP_MASK:CropMaskInteractorStyle, } return STYLES[style] + diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index f61a9bd..7c6fea7 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -148,7 +148,7 @@ class ContourMIPConfig(wx.Panel): class CanvasRendererCTX: - def __init__(self, evt_renderer, canvas_renderer): + def __init__(self, evt_renderer, canvas_renderer, orientation=None): """ A Canvas to render over a vtktRenderer. @@ -167,6 +167,7 @@ class CanvasRendererCTX: self.evt_renderer = evt_renderer self._size = self.canvas_renderer.GetSize() self.draw_list = [] + self.orientation = orientation self.gc = None self.last_cam_modif_time = -1 self.modified = True @@ -641,7 +642,7 @@ class Viewer(wx.Panel): self.style.CleanUp() del self.style - + style = styles.get_style(state)(self) setup = getattr(style, 'SetUp', None) @@ -1188,6 +1189,10 @@ class Viewer(wx.Panel): Publisher.subscribe(self.OnExportPicture,'Export picture to file') Publisher.subscribe(self.SetDefaultCursor, 'Set interactor default cursor') + Publisher.subscribe(self.SetSizeNSCursor, 'Set interactor resize NS cursor') + Publisher.subscribe(self.SetSizeWECursor, 'Set interactor resize WE cursor') + Publisher.subscribe(self.SetSizeNWSECursor, 'Set interactor resize NSWE cursor') + Publisher.subscribe(self.AddActors, 'Add actors ' + str(ORIENTATIONS[self.orientation])) Publisher.subscribe(self.RemoveActors, 'Remove actors ' + str(ORIENTATIONS[self.orientation])) Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') @@ -1216,6 +1221,15 @@ class Viewer(wx.Panel): def SetDefaultCursor(self, pusub_evt): self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + def SetSizeNSCursor(self, pusub_evt): + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) + + def SetSizeWECursor(self, pusub_evt): + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZEWE)) + + def SetSizeNWSECursor(self, pubsub_evt): + self.interactor.SetCursor(wx.StockCursor(wx.CURSOR_SIZENWSE)) + def OnExportPicture(self, pubsub_evt): Publisher.sendMessage('Begin busy cursor') view_prop_list = [] @@ -1384,7 +1398,7 @@ class Viewer(wx.Panel): self.cam = self.slice_data.renderer.GetActiveCamera() self.__build_cross_lines() - self.canvas = CanvasRendererCTX(self.slice_data.renderer, self.slice_data.canvas_renderer) + self.canvas = CanvasRendererCTX(self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation) # Set the slice number to the last slice to ensure the camera if far # enough to show all slices. diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 565381e..eabb42d 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -1696,7 +1696,7 @@ class ImportBitmapParameters(wx.Dialog): else: size=wx.Size(380,210) - pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Create project from bitmap"),size=wx.Size(380,220),\ + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Create project from bitmap"),size=size,\ style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP) self.interval = 0 @@ -2032,7 +2032,6 @@ class SelectPartsOptionsDialog(wx.Dialog): evt.Skip() self.Destroy() - class FFillSegmentationOptionsDialog(wx.Dialog): def __init__(self, config): pre = wx.PreDialog() @@ -2205,3 +2204,124 @@ class FFillSegmentationOptionsDialog(wx.Dialog): Publisher.sendMessage('Disable style', const.SLICE_STATE_MASK_FFILL) evt.Skip() self.Destroy() + +class CropOptionsDialog(wx.Dialog): + + def __init__(self, config): + + self.config = config + + pre = wx.PreDialog() + + if sys.platform == 'win32': + size=wx.Size(240,180) + else: + size=wx.Size(205,180) + + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Crop mask"),\ + size=size, style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + + self.PostCreate(pre) + + self._init_gui() + #self.config = config + + def UpdateValues(self, pubsub_evt): + + if type(pubsub_evt) == list: + data = pubsub_evt + else: + data = pubsub_evt.data + + xi, xf, yi, yf, zi, zf = data + + self.tx_axial_i.SetValue(str(zi)) + self.tx_axial_f.SetValue(str(zf)) + + self.tx_sagital_i.SetValue(str(xi)) + self.tx_sagital_f.SetValue(str(xf)) + + self.tx_coronal_i.SetValue(str(yi)) + self.tx_coronal_f.SetValue(str(yf)) + + def _init_gui(self): + + + p = wx.Panel(self, -1, style = wx.TAB_TRAVERSAL + | wx.CLIP_CHILDREN + | wx.FULL_REPAINT_ON_RESIZE) + + gbs_principal = self.gbs = wx.GridBagSizer(4,1) + + gbs = self.gbs = wx.GridBagSizer(3, 4) + + flag_labels = wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL + + stx_axial = wx.StaticText(p, -1, _(u"Axial:")) + self.tx_axial_i = tx_axial_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + stx_axial_t = wx.StaticText(p, -1, _(u" - ")) + self.tx_axial_f = tx_axial_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + + gbs.Add(stx_axial, (0,0), flag=flag_labels) + gbs.Add(tx_axial_i, (0,1)) + gbs.Add(stx_axial_t, (0,2), flag=flag_labels) + gbs.Add(tx_axial_f, (0,3)) + + stx_sagital = wx.StaticText(p, -1, _(u"Sagital:")) + self.tx_sagital_i = tx_sagital_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + stx_sagital_t = wx.StaticText(p, -1, _(u" - ")) + self.tx_sagital_f = tx_sagital_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + + gbs.Add(stx_sagital, (1,0), flag=flag_labels) + gbs.Add(tx_sagital_i, (1,1)) + gbs.Add(stx_sagital_t, (1,2), flag=flag_labels) + gbs.Add(tx_sagital_f, (1,3)) + + stx_coronal = wx.StaticText(p, -1, _(u"Coronal:")) + self.tx_coronal_i = tx_coronal_i = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + stx_coronal_t = wx.StaticText(p, -1, _(u" - ")) + self.tx_coronal_f = tx_coronal_f = wx.TextCtrl(p, -1, "", size=wx.Size(50,-1)) + + gbs.Add(stx_coronal, (2,0), flag=flag_labels) + gbs.Add(tx_coronal_i, (2,1)) + gbs.Add(stx_coronal_t, (2,2), flag=flag_labels) + gbs.Add(tx_coronal_f, (2,3)) + + gbs_button = wx.GridBagSizer(2, 4) + + btn_ok = self.btn_ok= wx.Button(p, wx.ID_OK) + btn_ok.SetDefault() + + btn_cancel = wx.Button(p, wx.ID_CANCEL) + + gbs_button.Add(btn_cancel, (0,0)) + gbs_button.Add(btn_ok, (0,1)) + + gbs_principal.AddSizer(gbs, (0,0), flag = wx.ALL|wx.EXPAND) + gbs_principal.AddStretchSpacer((1,0)) + gbs_principal.AddStretchSpacer((2,0)) + gbs_principal.AddSizer(gbs_button, (3,0), flag = wx.ALIGN_RIGHT) + + box = wx.BoxSizer() + box.AddSizer(gbs_principal, 1, wx.ALL|wx.EXPAND, 10) + + p.SetSizer(box) + + Publisher.subscribe(self.UpdateValues, 'Update crop limits into gui') + + btn_ok.Bind(wx.EVT_BUTTON, self.OnOk) + btn_cancel.Bind(wx.EVT_BUTTON, self.OnClose) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + + def OnOk(self, evt): + self.config.dlg_visible = False + Publisher.sendMessage('Crop mask') + Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) + evt.Skip() + + def OnClose(self, evt): + self.config.dlg_visible = False + Publisher.sendMessage('Disable style', const.SLICE_STATE_CROP_MASK) + evt.Skip() + self.Destroy() diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 6508c9a..1232037 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -463,6 +463,12 @@ class Frame(wx.Frame): self.OnInterpolatedSlices(True) else: self.OnInterpolatedSlices(False) + elif id == const.ID_CROP_MASK: + self.OnCropMask() + + def OnInterpolatedSlices(self, status): + Publisher.sendMessage('Set interpolated slices', status) + def OnSize(self, evt): """ @@ -601,6 +607,9 @@ class Frame(wx.Frame): def OnInterpolatedSlices(self, status): Publisher.sendMessage('Set interpolated slices', status) + + def OnCropMask(self): + Publisher.sendMessage('Enable style', const.SLICE_STATE_CROP_MASK) # ------------------------------------------------------------------ # ------------------------------------------------------------------ @@ -737,6 +746,7 @@ class MenuBar(wx.MenuBar): self.clean_mask_menu = mask_menu.Append(const.ID_CLEAN_MASK, _(u"Clean Mask\tCtrl+Shift+A")) self.clean_mask_menu.Enable(False) + mask_menu.AppendSeparator() self.fill_hole_mask_menu = mask_menu.Append(const.ID_FLOODFILL_MASK, _(u"Fill holes manually")) self.fill_hole_mask_menu.Enable(False) @@ -745,6 +755,11 @@ class MenuBar(wx.MenuBar): self.select_mask_part_menu = mask_menu.Append(const.ID_SELECT_MASK_PART, _(u"Select parts")) self.select_mask_part_menu.Enable(False) + + mask_menu.AppendSeparator() + + self.crop_mask_menu = mask_menu.Append(const.ID_CROP_MASK, _("Crop")) + self.crop_mask_menu.Enable(False) tools_menu.AppendMenu(-1, _(u"Mask"), mask_menu) @@ -892,6 +907,7 @@ class MenuBar(wx.MenuBar): def OnShowMask(self, pubsub_evt): index, value = pubsub_evt.data self.clean_mask_menu.Enable(value) + self.crop_mask_menu.Enable(value) # ------------------------------------------------------------------ -- libgit2 0.21.2