diff --git a/invesalius/constants.py b/invesalius/constants.py index 0879227..6d40516 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -105,6 +105,8 @@ SAGITAL_STR="SAGITAL" # Measure type LINEAR = 6 ANGULAR = 7 +DENSITY_ELLIPSE = 8 +DENSITY_POLYGON = 9 # Colour representing each orientation ORIENTATION_COLOUR = {'AXIAL': (1,0,0), # Red @@ -551,6 +553,8 @@ ID_WATERSHED_SEGMENTATION = wx.NewId() ID_THRESHOLD_SEGMENTATION = wx.NewId() ID_FLOODFILL_SEGMENTATION = wx.NewId() ID_CROP_MASK = wx.NewId() +ID_DENSITY_MEASURE = wx.NewId() +ID_MASK_DENSITY_MEASURE = wx.NewId() ID_CREATE_SURFACE = wx.NewId() ID_CREATE_MASK = wx.NewId() @@ -566,6 +570,9 @@ STATE_PAN = 1005 STATE_ANNOTATE = 1006 STATE_MEASURE_DISTANCE = 1007 STATE_MEASURE_ANGLE = 1008 +STATE_MEASURE_DENSITY = 1009 +STATE_MEASURE_DENSITY_ELLIPSE = 1010 +STATE_MEASURE_DENSITY_POLYGON = 1011 SLICE_STATE_CROSS = 3006 SLICE_STATE_SCROLL = 3007 @@ -584,7 +591,11 @@ VOLUME_STATE_SEED = 2001 TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM, STATE_ZOOM_SL, STATE_PAN, STATE_MEASURE_DISTANCE, - STATE_MEASURE_ANGLE] #, STATE_ANNOTATE] + STATE_MEASURE_ANGLE, STATE_MEASURE_DENSITY_ELLIPSE, + STATE_MEASURE_DENSITY_POLYGON, + ] #, STATE_ANNOTATE] + + TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL, SLICE_STATE_REORIENT] @@ -599,6 +610,9 @@ 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(STATE_MEASURE_DENSITY) +SLICE_STYLES.append(STATE_MEASURE_DENSITY_ELLIPSE) +SLICE_STYLES.append(STATE_MEASURE_DENSITY_POLYGON) VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, STATE_MEASURE_ANGLE] @@ -619,6 +633,9 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, STATE_DEFAULT: 0, STATE_MEASURE_ANGLE: 2, STATE_MEASURE_DISTANCE: 2, + STATE_MEASURE_DENSITY_ELLIPSE: 2, + STATE_MEASURE_DENSITY_POLYGON: 2, + STATE_MEASURE_DENSITY: 2, STATE_WL: 2, STATE_SPIN: 2, STATE_ZOOM: 2, diff --git a/invesalius/data/measures.py b/invesalius/data/measures.py index 0567116..9b19363 100644 --- a/invesalius/data/measures.py +++ b/invesalius/data/measures.py @@ -15,8 +15,15 @@ import invesalius.constants as const import invesalius.project as prj import invesalius.session as ses import invesalius.utils as utils + +from invesalius import math_utils +from invesalius.gui.widgets.canvas_renderer import TextBox, CircleHandler, Ellipse, Polygon, CanvasHandlerBase +from scipy.misc import imsave + TYPE = {const.LINEAR: _(u"Linear"), const.ANGULAR: _(u"Angular"), + const.DENSITY_ELLIPSE: _(u"Density Ellipse"), + const.DENSITY_POLYGON: _(u"Density Polygon"), } LOCATION = {const.SURFACE: _(u"3D"), @@ -47,6 +54,9 @@ else: MEASURE_TEXT_COLOUR = (0, 0, 0) MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255) + +DEBUG_DENSITY = False + class MeasureData(with_metaclass(utils.Singleton)): """ Responsible to keep measures data. @@ -117,38 +127,63 @@ class MeasurementManager(object): Publisher.subscribe(self._rm_incomplete_measurements, "Remove incomplete measurements") Publisher.subscribe(self._change_measure_point_pos, 'Change measurement point position') + Publisher.subscribe(self._add_density_measure, "Add density measurement") Publisher.subscribe(self.OnCloseProject, 'Close project data') def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)): for i in measurement_dict: m = measurement_dict[i] - if m.location == const.AXIAL: - radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE + if isinstance(m, DensityMeasurement): + if m.type == const.DENSITY_ELLIPSE: + mr = CircleDensityMeasure(map_id_locations[m.location], + m.slice_number, + m.colour) + mr.set_center(m.points[0]) + mr.set_point1(m.points[1]) + mr.set_point2(m.points[2]) + elif m.type == const.DENSITY_POLYGON: + mr = PolygonDensityMeasure(map_id_locations[m.location], + m.slice_number, + m.colour) + for p in m.points: + mr.insert_point(p) + mr.complete_polygon() + + mr.set_density_values(m.min, m.max, m.mean, m.std, m.area) + print(m.min, m.max, m.mean, m.std) + mr._need_calc = False + self.measures.append((m, mr)) + mr.set_measurement(m) - elif m.location == const.CORONAL: - radius = min(spacing[0], spacing[1]) * const.PROP_MEASURE + else: + if m.location == const.AXIAL: + radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE - elif m.location == const.SAGITAL: - radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE + elif m.location == const.CORONAL: + radius = min(spacing[0], spacing[1]) * const.PROP_MEASURE - else: - radius = min(spacing) * const.PROP_MEASURE + elif m.location == const.SAGITAL: + radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE - representation = CirclePointRepresentation(m.colour, radius) - if m.type == const.LINEAR: - mr = LinearMeasure(m.colour, representation) - else: - mr = AngularMeasure(m.colour, representation) - self.current = (m, mr) - self.measures.append(self.current) - for point in m.points: - x, y, z = point - actors = mr.AddPoint(x, y, z) + else: + radius = min(spacing) * const.PROP_MEASURE + + representation = CirclePointRepresentation(m.colour, radius) + if m.type == const.LINEAR: + mr = LinearMeasure(m.colour, representation) + else: + mr = AngularMeasure(m.colour, representation) + self.current = (m, mr) + self.measures.append(self.current) + for point in m.points: + x, y, z = point + actors = mr.AddPoint(x, y, z) if m.location == const.SURFACE: Publisher.sendMessage(("Add actors " + str(m.location)), actors=actors) + self.current = None if not m.visible: @@ -315,6 +350,39 @@ class MeasurementManager(object): # self.measures.pop() self.current = None + def _add_density_measure(self, density_measure): + m = DensityMeasurement() + m.index = len(self.measures) + m.location = density_measure.location + m.slice_number = density_measure.slice_number + m.colour = density_measure.colour + m.value = density_measure._mean + m.area = density_measure._area + m.mean = density_measure._mean + m.min = density_measure._min + m.max = density_measure._max + m.std = density_measure._std + if density_measure.format == 'ellipse': + m.points = [density_measure.center, density_measure.point1, density_measure.point2] + m.type = const.DENSITY_ELLIPSE + elif density_measure.format == 'polygon': + m.points = density_measure.points + m.type = const.DENSITY_POLYGON + density_measure.index = m.index + + density_measure.set_measurement(m) + + self.measures.append((m, density_measure)) + + index = prj.Project().AddMeasurement(m) + + msg = 'Update measurement info in GUI', + Publisher.sendMessage(msg, + index=m.index, name=m.name, colour=m.colour, + location=density_measure.orientation, + type_='Density', + value='%.3f' % m.value) + def OnCloseProject(self): self.measures.clean() @@ -344,6 +412,75 @@ class Measurement(): self.points = info["points"] self.visible = info["visible"] + def get_as_dict(self): + d = { + 'index': self.index, + 'name': self.name, + 'colour': self.colour, + 'value': self.value, + 'location': self.location, + 'type': self.type, + 'slice_number': self.slice_number, + 'points': self.points, + 'visible': self.visible, + } + return d + + +class DensityMeasurement(): + general_index = -1 + def __init__(self): + DensityMeasurement.general_index += 1 + self.index = DensityMeasurement.general_index + self.name = const.MEASURE_NAME_PATTERN %(self.index+1) + self.colour = next(const.MEASURE_COLOUR) + self.area = 0 + self.min = 0 + self.max = 0 + self.mean = 0 + self.std = 0 + self.location = const.AXIAL + self.type = const.DENSITY_ELLIPSE + self.slice_number = 0 + self.points = [] + self.visible = True + + def Load(self, info): + self.index = info["index"] + self.name = info["name"] + self.colour = info["colour"] + self.value = info["value"] + self.location = info["location"] + self.type = info["type"] + self.slice_number = info["slice_number"] + self.points = info["points"] + self.visible = info["visible"] + self.area = info['area'] + self.min = info["min"] + self.max = info["max"] + self.mean = info["mean"] + self.std = info["std"] + + def get_as_dict(self): + d = { + 'index': self.index, + 'name': self.name, + 'colour': self.colour, + 'value': self.value, + 'location': self.location, + 'type': self.type, + 'slice_number': self.slice_number, + 'points': self.points, + 'visible': self.visible, + 'area': self.area, + 'min': self.min, + 'max': self.max, + 'mean': self.mean, + 'std': self.std, + } + return d + + class CirclePointRepresentation(object): """ This class represents a circle that indicate a point in the surface @@ -452,6 +589,7 @@ class LinearMeasure(object): self.line_actor = None self.text_actor = None self.renderer = None + self.layer = 0 if not representation: representation = CirclePointRepresentation(colour) self.representation = representation @@ -632,6 +770,7 @@ class AngularMeasure(object): self.point_actor3 = None self.line_actor = None self.text_actor = None + self.layer = 0 if not representation: representation = CirclePointRepresentation(colour) self.representation = representation @@ -799,7 +938,6 @@ class AngularMeasure(object): for p in self.points: coord.SetValue(p) cx, cy = coord.GetComputedDoubleDisplayValue(canvas.evt_renderer) - print(cx, cy) # canvas.draw_circle((cx, cy), 2.5) points.append((cx, cy)) @@ -811,8 +949,11 @@ class AngularMeasure(object): if len(points) == 3: txt = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue()) r, g, b = self.colour - canvas.draw_arc(points[1], points[0], points[2], line_colour=(r*255, g*255, b*255, 255)) - canvas.draw_text_box(txt, (points[1][0], points[1][1]), txt_colour=MEASURE_TEXT_COLOUR, bg_colour=MEASURE_TEXTBOX_COLOUR) + canvas.draw_arc(points[1], points[0], points[2], + line_colour=(int(r*255), int(g*255), int(b*255), 255)) + canvas.draw_text_box(txt, (points[1][0], points[1][1]), + txt_colour=MEASURE_TEXT_COLOUR, + bg_colour=MEASURE_TEXTBOX_COLOUR) def GetNumberOfPoints(self): return self.number_of_points @@ -894,3 +1035,539 @@ class AngularMeasure(object): def __del__(self): self.Remove() + + +class CircleDensityMeasure(CanvasHandlerBase): + def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True): + super(CircleDensityMeasure, self).__init__(None) + self.parent = None + self.children = [] + self.layer = 0 + + self.colour = colour + self.center = (0.0, 0.0, 0.0) + self.point1 = (0.0, 0.0, 0.0) + self.point2 = (0.0, 0.0, 0.0) + + self.orientation = orientation + self.slice_number = slice_number + + self.format = 'ellipse' + + self.location = map_locations_id[self.orientation] + self.index = 0 + + self._area = 0 + self._min = 0 + self._max = 0 + self._mean = 0 + self._std = 0 + + self._measurement = None + + self.ellipse = Ellipse(self, self.center, self.point1, self.point2, + fill=False, line_colour=self.colour) + self.ellipse.layer = 1 + self.add_child(self.ellipse) + self.text_box = None + + self._need_calc = True + self.interactive = interactive + + def set_center(self, pos): + self.center = pos + self._need_calc = True + self.ellipse.center = self.center + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + + def set_point1(self, pos): + self.point1 = pos + self._need_calc = True + self.ellipse.set_point1(self.point1) + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + + def set_point2(self, pos): + self.point2 = pos + self._need_calc = True + self.ellipse.set_point2(self.point2) + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + + def set_density_values(self, _min, _max, _mean, _std, _area): + self._min = _min + self._max = _max + self._mean = _mean + self._std = _std + self._area = _area + + text = _('Area: %.3f\n' + 'Min: %.3f\n' + 'Max: %.3f\n' + 'Mean: %.3f\n' + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) + + if self.text_box is None: + self.text_box = TextBox(self, text, self.point1, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR) + self.text_box.layer = 2 + self.add_child(self.text_box) + else: + self.text_box.set_text(text) + + if self._measurement: + self._measurement.value = self._mean + self._update_gui_info() + + def _update_gui_info(self): + msg = 'Update measurement info in GUI', + print(msg) + if self._measurement: + m = self._measurement + Publisher.sendMessage(msg, + index=m.index, name=m.name, colour=m.colour, + location= self.orientation, + type_=_('Density Ellipse'), + value='%.3f' % m.value) + + def set_measurement(self, dm): + self._measurement = dm + + def SetVisibility(self, value): + self.visible = value + self.ellipse.visible = value + + def _3d_to_2d(self, renderer, pos): + coord = vtk.vtkCoordinate() + coord.SetValue(pos) + cx, cy = coord.GetComputedDoubleDisplayValue(renderer) + return cx, cy + + def is_over(self, x, y): + return None + # if self.interactive: + # if self.ellipse.is_over(x, y): + # return self.ellipse.is_over(x, y) + # elif self.text_box.is_over(x, y): + # return self.text_box.is_over(x, y) + # return None + + def set_interactive(self, value): + self.interactive = bool(value) + self.ellipse.interactive = self.interactive + + 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. + """ + # cx, cy = self._3d_to_2d(canvas.evt_renderer, self.center) + # px, py = self._3d_to_2d(canvas.evt_renderer, self.point1) + # radius = ((px - cx)**2 + (py - cy)**2)**0.5 + if self._need_calc: + self._need_calc = False + self.calc_density() + + # canvas.draw_circle((cx, cy), radius, line_colour=self.colour) + # self.ellipse.draw_to_canvas(gc, canvas) + + # # canvas.draw_text_box(text, (px, py), ) + # self.text_box.draw_to_canvas(gc, canvas) + # # self.handle_tl.draw_to_canvas(gc, canvas) + + def calc_area(self): + if self.orientation == 'AXIAL': + a = abs(self.point1[0] - self.center[0]) + b = abs(self.point2[1] - self.center[1]) + + elif self.orientation == 'CORONAL': + a = abs(self.point1[0] - self.center[0]) + b = abs(self.point2[2] - self.center[2]) + + elif self.orientation == 'SAGITAL': + a = abs(self.point1[1] - self.center[1]) + b = abs(self.point2[2] - self.center[2]) + + return math_utils.calc_ellipse_area(a, b) + + def calc_density(self): + from invesalius.data.slice_ import Slice + slc = Slice() + n = self.slice_number + orientation = self.orientation + img_slice = slc.get_image_slice(orientation, n) + dy, dx = img_slice.shape + spacing = slc.spacing + + if orientation == 'AXIAL': + sx, sy = spacing[0], spacing[1] + cx, cy = self.center[0], self.center[1] + + a = abs(self.point1[0] - self.center[0]) + b = abs(self.point2[1] - self.center[1]) + + n = slc.buffer_slices["AXIAL"].index + 1 + m = slc.current_mask.matrix[n, 1:, 1:] + + elif orientation == 'CORONAL': + sx, sy = spacing[0], spacing[2] + cx, cy = self.center[0], self.center[2] + + a = abs(self.point1[0] - self.center[0]) + b = abs(self.point2[2] - self.center[2]) + + n = slc.buffer_slices["CORONAL"].index + 1 + m = slc.current_mask.matrix[1:, n, 1:] + + elif orientation == 'SAGITAL': + sx, sy = spacing[1], spacing[2] + cx, cy = self.center[1], self.center[2] + + a = abs(self.point1[1] - self.center[1]) + b = abs(self.point2[2] - self.center[2]) + + n = slc.buffer_slices["SAGITAL"].index + 1 + m = slc.current_mask.matrix[1:, 1:, n] + + # a = np.linalg.norm(np.array(self.point1) - np.array(self.center)) + # b = np.linalg.norm(np.array(self.point2) - np.array(self.center)) + + mask_y, mask_x = np.ogrid[0:dy*sy:sy, 0:dx*sx:sx] + # mask = ((mask_x - cx)**2 + (mask_y - cy)**2) <= (radius ** 2) + mask = (((mask_x-cx)**2 / a**2) + ((mask_y-cy)**2 / b**2)) <= 1.0 + + # try: + # test_img = np.zeros_like(img_slice) + # test_img[mask] = img_slice[mask] + # imsave('/tmp/manolo.png', test_img[::-1,:]) + if DEBUG_DENSITY: + try: + m[:] = 0 + m[mask] = 254 + slc.buffer_slices[self.orientation].discard_vtk_mask() + slc.buffer_slices[self.orientation].discard_mask() + Publisher.sendMessage('Reload actual slice') + except IndexError: + pass + + values = img_slice[mask] + + try: + _min = values.min() + _max = values.max() + _mean = values.mean() + _std = values.std() + except ValueError: + _min = 0 + _max = 0 + _mean = 0 + _std = 0 + + _area = self.calc_area() + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + self._measurement.value = float(_mean) + self._measurement.mean = float(_mean) + self._measurement.min = float(_min) + self._measurement.max = float(_max) + self._measurement.std = float(_std) + self._measurement.area = float(_area) + + self.set_density_values(_min, _max, _mean, _std, _area) + + def IsComplete(self): + return True + + def on_mouse_move(self, evt): + old_center = self.center + self.center = self.ellipse.center + self.set_point1(self.ellipse.point1) + self.set_point2(self.ellipse.point2) + + diff = tuple((i-j for i,j in zip(self.center, old_center))) + self.text_box.position = tuple((i+j for i,j in zip(self.text_box.position, diff))) + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + self._measurement.value = self._mean + self._measurement.mean = self._mean + self._measurement.min = self._min + self._measurement.max = self._max + self._measurement.std = self._std + + session = ses.Session() + session.ChangeProject() + + def on_select(self, evt): + self.layer = 50 + + def on_deselect(self, evt): + self.layer = 0 + + +class PolygonDensityMeasure(CanvasHandlerBase): + def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True): + super(PolygonDensityMeasure, self).__init__(None) + self.parent = None + self.children = [] + self.layer = 0 + + self.colour = colour + self.points = [] + + self.orientation = orientation + self.slice_number = slice_number + + self.complete = False + + self.format = 'polygon' + + self.location = map_locations_id[self.orientation] + self.index = 0 + + self._area = 0 + self._min = 0 + self._max = 0 + self._mean = 0 + self._std = 0 + + self._dist_tbox = (0, 0, 0) + + self._measurement = None + + self.polygon = Polygon(self, fill=False, closed=False, line_colour=self.colour) + self.polygon.layer = 1 + self.add_child(self.polygon) + + self.text_box = None + + self._need_calc = False + self.interactive = interactive + + + def on_mouse_move(self, evt): + self.points = self.polygon.points + self._need_calc = self.complete + + if self._measurement: + self._measurement.points = self.points + + if self.text_box: + bounds = self.get_bounds() + p = [bounds[3], bounds[4], bounds[5]] + if evt.root_event_obj is self.text_box: + self._dist_tbox = [i-j for i,j in zip(self.text_box.position, p)] + else: + self.text_box.position = [i+j for i,j in zip(self._dist_tbox, p)] + print("text box position", self.text_box.position) + + session = ses.Session() + session.ChangeProject() + + def draw_to_canvas(self, gc, canvas): + if self._need_calc: + self.calc_density(canvas) + # if self.visible: + # self.polygon.draw_to_canvas(gc, canvas) + # if self._need_calc: + # self.calc_density(canvas) + # if self.text_box: + # bounds = self.get_bounds() + # p = [bounds[3], bounds[4], bounds[5]] + # self.text_box.draw_to_canvas(gc, canvas) + # self._dist_tbox = [j-i for i,j in zip(p, self.text_box.position)] + + def insert_point(self, point): + print("insert points", len(self.points)) + self.polygon.append_point(point) + self.points.append(point) + + def complete_polygon(self): + # if len(self.points) >= 3: + self.polygon.closed = True + self._need_calc = True + self.complete = True + + bounds = self.get_bounds() + p = [bounds[3], bounds[4], bounds[5]] + if self.text_box is None: + p[0] += 5 + self.text_box = TextBox(self, '', p, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR) + self.text_box.layer = 2 + self.add_child(self.text_box) + + def calc_density(self, canvas): + from invesalius.data.slice_ import Slice + + slc = Slice() + n = self.slice_number + orientation = self.orientation + img_slice = slc.get_image_slice(orientation, n) + dy, dx = img_slice.shape + spacing = slc.spacing + + if orientation == 'AXIAL': + sx, sy = spacing[0], spacing[1] + n = slc.buffer_slices["AXIAL"].index + 1 + m = slc.current_mask.matrix[n, 1:, 1:] + plg_points = [(x/sx, y/sy) for (x, y, z) in self.points] + + elif orientation == 'CORONAL': + sx, sy = spacing[0], spacing[2] + n = slc.buffer_slices["CORONAL"].index + 1 + m = slc.current_mask.matrix[1:, n, 1:] + plg_points = [(x/sx, z/sy) for (x, y, z) in self.points] + + elif orientation == 'SAGITAL': + sx, sy = spacing[1], spacing[2] + n = slc.buffer_slices["SAGITAL"].index + 1 + m = slc.current_mask.matrix[1:, 1:, n] + + plg_points = [(y/sx, z/sy) for (x, y, z) in self.points] + + plg_tmp = Polygon(None, plg_points, fill=True, + line_colour=(0, 0, 0, 0), + fill_colour=(255, 255, 255, 255), width=1, + interactive=False, is_3d=False) + h, w = img_slice.shape + arr = canvas.draw_element_to_array([plg_tmp, ], size=(w, h), flip=False) + mask = arr[:, :, 0] >= 128 + + print("mask sum", mask.sum()) + + if DEBUG_DENSITY: + try: + m[:] = 0 + m[mask] = 254 + slc.buffer_slices[self.orientation].discard_vtk_mask() + slc.buffer_slices[self.orientation].discard_mask() + Publisher.sendMessage('Reload actual slice') + except IndexError: + pass + + values = img_slice[mask] + + try: + _min = values.min() + _max = values.max() + _mean = values.mean() + _std = values.std() + except ValueError: + _min = 0 + _max = 0 + _mean = 0 + _std = 0 + + _area = self.calc_area() + + if self._measurement: + self._measurement.points = self.points + self._measurement.value = float(_mean) + self._measurement.mean = float(_mean) + self._measurement.min = float(_min) + self._measurement.max = float(_max) + self._measurement.std = float(_std) + self._measurement.area = float(_area) + + self.set_density_values(_min, _max, _mean, _std, _area) + self.calc_area() + + self._need_calc = False + + def calc_area(self): + if self.orientation == 'AXIAL': + points = [(x, y) for (x, y, z) in self.points] + elif self.orientation == 'CORONAL': + points = [(x, z) for (x, y, z) in self.points] + elif self.orientation == 'SAGITAL': + points = [(y, z) for (x, y, z) in self.points] + area = math_utils.calc_polygon_area(points) + print('Points', points) + print('xv = %s;' % [i[0] for i in points]) + print('yv = %s;' % [i[1] for i in points]) + print('Area', area) + return area + + def get_bounds(self): + min_x = min(self.points, key=lambda x: x[0])[0] + max_x = max(self.points, key=lambda x: x[0])[0] + + min_y = min(self.points, key=lambda x: x[1])[1] + max_y = max(self.points, key=lambda x: x[1])[1] + + min_z = min(self.points, key=lambda x: x[2])[2] + max_z = max(self.points, key=lambda x: x[2])[2] + + print(self.points) + + return (min_x, min_y, min_z, max_x, max_y, max_z) + + def IsComplete(self): + return self.complete + + def set_measurement(self, dm): + self._measurement = dm + + def SetVisibility(self, value): + self.visible = value + self.polygon.visible = value + + def set_interactive(self, value): + self.interactive = bool(value) + self.polygon.interactive = self.interactive + + def is_over(self, x, y): + None + # if self.interactive: + # if self.polygon.is_over(x, y): + # return self.polygon.is_over(x, y) + # if self.text_box is not None: + # if self.text_box.is_over(x, y): + # return self.text_box.is_over(x, y) + # return None + + def set_density_values(self, _min, _max, _mean, _std, _area): + self._min = _min + self._max = _max + self._mean = _mean + self._std = _std + self._area = _area + + text = _('Area: %.3f\n' + 'Min: %.3f\n' + 'Max: %.3f\n' + 'Mean: %.3f\n' + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) + + bounds = self.get_bounds() + p = [bounds[3], bounds[4], bounds[5]] + + dx = self.text_box.position[0] - p[0] + dy = self.text_box.position[1] - p[1] + p[0] += dx + p[1] += dy + self.text_box.set_text(text) + self.text_box.position = p + + if self._measurement: + self._measurement.value = self._mean + self._update_gui_info() + + def _update_gui_info(self): + msg = 'Update measurement info in GUI', + print(msg) + if self._measurement: + m = self._measurement + Publisher.sendMessage(msg, + index=m.index, name=m.name, + colour=m.colour, + location=self.orientation, + type_=_('Density Polygon'), + value='%.3f' % m.value) diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index 35bd1eb..2a62253 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -23,6 +23,8 @@ import tempfile import numpy as np import vtk + +from scipy import ndimage from wx.lib.pubsub import pub as Publisher import invesalius.constants as const @@ -1540,3 +1542,61 @@ class Slice(with_metaclass(utils.Singleton, object)): self.buffer_slices['SAGITAL'].discard_vtk_mask() Publisher.sendMessage('Reload actual slice') + + def calc_image_density(self, mask=None): + if mask is None: + mask = self.current_mask + self.do_threshold_to_all_slices(mask) + values = self.matrix[mask.matrix[1:, 1:, 1:] > 127] + + if len(values): + _min = values.min() + _max = values.max() + _mean = values.mean() + _std = values.std() + return _min, _max, _mean, _std + else: + return 0, 0, 0, 0 + + def calc_mask_area(self, mask=None): + if mask is None: + mask = self.current_mask + + self.do_threshold_to_all_slices(mask) + bin_img = (mask.matrix[1:, 1:, 1:] > 127) + + sx, sy, sz = self.spacing + + kernel = np.zeros((3, 3, 3)) + kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz + kernel[0, 1, 1] = - (sx * sy) + kernel[2, 1, 1] = - (sx * sy) + + kernel[1, 0, 1] = - (sx * sz) + kernel[1, 2, 1] = - (sx * sz) + + kernel[1, 1, 0] = - (sy * sz) + kernel[1, 1, 2] = - (sy * sz) + + # area = ndimage.generic_filter(bin_img * 1.0, _conv_area, size=(3, 3, 3), mode='constant', cval=1, extra_arguments=(sx, sy, sz)).sum() + area = transforms.convolve_non_zero(bin_img * 1.0, kernel, 1).sum() + + return area + +def _conv_area(x, sx, sy, sz): + x = x.reshape((3, 3, 3)) + if x[1, 1, 1]: + kernel = np.zeros((3, 3, 3)) + kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz + kernel[0, 1, 1] = -(sx * sy) + kernel[2, 1, 1] = -(sx * sy) + + kernel[1, 0, 1] = -(sx * sz) + kernel[1, 2, 1] = -(sx * sz) + + kernel[1, 1, 0] = -(sy * sz) + kernel[1, 1, 2] = -(sy * sz) + + return (x * kernel).sum() + else: + return 0 diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index 552be08..ee06774 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -45,7 +45,7 @@ from scipy.ndimage import watershed_ift, generate_binary_structure from skimage.morphology import watershed import invesalius.gui.dialogs as dialogs -from invesalius.data.measures import MeasureData +from invesalius.data.measures import MeasureData, CircleDensityMeasure, PolygonDensityMeasure from . import floodfill @@ -140,6 +140,7 @@ class DefaultInteractorStyle(BaseImageInteractorStyle): # Zoom using right button self.AddObserver("RightButtonPressEvent",self.OnZoomRightClick) + self.AddObserver("RightButtonReleaseEvent",self.OnZoomRightRelease) self.AddObserver("MouseMoveEvent", self.OnZoomRightMove) self.AddObserver("MouseWheelForwardEvent",self.OnScrollForward) @@ -161,6 +162,12 @@ class DefaultInteractorStyle(BaseImageInteractorStyle): def OnZoomRightClick(self, evt, obj): evt.StartDolly() + def OnZoomRightRelease(self, evt, obj): + print('EndDolly') + evt.OnRightButtonUp() + # evt.EndDolly() + self.right_pressed = False + def OnScrollForward(self, evt, obj): iren = self.viewer.interactor viewer = self.viewer @@ -527,6 +534,150 @@ class AngularMeasureInteractorStyle(LinearMeasureInteractorStyle): self.state_code = const.STATE_MEASURE_ANGLE +class DensityMeasureStyle(DefaultInteractorStyle): + """ + Interactor style responsible for density measurements. + """ + def __init__(self, viewer): + DefaultInteractorStyle.__init__(self, viewer) + + self.state_code = const.STATE_MEASURE_DENSITY + + self.format = 'polygon' + + self._last_measure = None + + self.viewer = viewer + self.orientation = viewer.orientation + self.slice_data = viewer.slice_data + + self.picker = vtk.vtkCellPicker() + self.picker.PickFromListOn() + + self.measures = MeasureData() + + self._bind_events() + + def _bind_events(self): + # self.AddObserver("LeftButtonPressEvent", self.OnInsertPoint) + # self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseMeasurePoint) + # self.AddObserver("MouseMoveEvent", self.OnMoveMeasurePoint) + # self.AddObserver("LeaveEvent", self.OnLeaveMeasureInteractor) + self.viewer.canvas.subscribe_event('LeftButtonPressEvent', self.OnInsertPoint) + self.viewer.canvas.subscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon) + + def SetUp(self): + for n in self.viewer.draw_by_slice_number: + for i in self.viewer.draw_by_slice_number[n]: + if isinstance(i, PolygonDensityMeasure): + i.set_interactive(True) + self.viewer.canvas.Refresh() + + def CleanUp(self): + self.viewer.canvas.unsubscribe_event('LeftButtonPressEvent', self.OnInsertPoint) + self.viewer.canvas.unsubscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon) + old_list = self.viewer.draw_by_slice_number + self.viewer.draw_by_slice_number.clear() + for n in old_list: + for i in old_list[n]: + if isinstance(i, PolygonDensityMeasure): + if i.complete: + self.viewer.draw_by_slice_number[n].append(i) + else: + self.viewer.draw_by_slice_number[n].append(i) + + self.viewer.UpdateCanvas() + + def _2d_to_3d(self, pos): + mx, my = pos + iren = self.viewer.interactor + render = iren.FindPokedRenderer(mx, my) + self.picker.AddPickList(self.slice_data.actor) + self.picker.Pick(mx, my, 0, render) + x, y, z = self.picker.GetPickPosition() + self.picker.DeletePickList(self.slice_data.actor) + return (x, y, z) + + def _pick_position(self): + iren = self.viewer.interactor + mx, my = iren.GetEventPosition() + return (mx, my) + + def _get_pos_clicked(self): + mouse_x, mouse_y = self._pick_position() + position = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) + return position + + def OnInsertPoint(self, evt): + mouse_x, mouse_y = evt.position + print('OnInsertPoint', evt.position) + n = self.viewer.slice_data.number + pos = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) + + if self.format == 'ellipse': + pp1 = self.viewer.get_coordinate_cursor(mouse_x+50, mouse_y, self.picker) + pp2 = self.viewer.get_coordinate_cursor(mouse_x, mouse_y+50, self.picker) + + m = CircleDensityMeasure(self.orientation, n) + m.set_center(pos) + m.set_point1(pp1) + m.set_point2(pp2) + m.calc_density() + _new_measure = True + Publisher.sendMessage("Add density measurement", density_measure=m) + elif self.format == 'polygon': + if self._last_measure is None: + m = PolygonDensityMeasure(self.orientation, n) + _new_measure = True + else: + m = self._last_measure + _new_measure = False + if m.slice_number != n: + self.viewer.draw_by_slice_number[m.slice_number].remove(m) + del m + m = PolygonDensityMeasure(self.orientation, n) + _new_measure = True + + m.insert_point(pos) + + if _new_measure: + self.viewer.draw_by_slice_number[n].append(m) + + if self._last_measure: + self._last_measure.set_interactive(False) + + self._last_measure = m + # m.calc_density() + + self.viewer.UpdateCanvas() + + def OnInsertPolygon(self, evt): + if self.format == 'polygon' and self._last_measure: + m = self._last_measure + if len(m.points) >= 3: + n = self.viewer.slice_data.number + print(self.viewer.draw_by_slice_number[n], m) + self.viewer.draw_by_slice_number[n].remove(m) + m.complete_polygon() + self._last_measure = None + Publisher.sendMessage("Add density measurement", density_measure=m) + self.viewer.UpdateCanvas() + + +class DensityMeasureEllipseStyle(DensityMeasureStyle): + def __init__(self, viewer): + DensityMeasureStyle.__init__(self, viewer) + self.state_code = const.STATE_MEASURE_DENSITY_ELLIPSE + self.format = 'ellipse' + + +class DensityMeasurePolygonStyle(DensityMeasureStyle): + def __init__(self, viewer): + DensityMeasureStyle.__init__(self, viewer) + self.state_code = const.STATE_MEASURE_DENSITY_POLYGON + self.format = 'polygon' + + class PanMoveInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for translate the camera. @@ -2356,6 +2507,10 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): return out_mask + + + + def get_style(style): STYLES = { const.STATE_DEFAULT: DefaultInteractorStyle, @@ -2363,6 +2518,8 @@ def get_style(style): const.STATE_WL: WWWLInteractorStyle, const.STATE_MEASURE_DISTANCE: LinearMeasureInteractorStyle, const.STATE_MEASURE_ANGLE: AngularMeasureInteractorStyle, + const.STATE_MEASURE_DENSITY_ELLIPSE: DensityMeasureEllipseStyle, + const.STATE_MEASURE_DENSITY_POLYGON: DensityMeasurePolygonStyle, const.STATE_PAN: PanMoveInteractorStyle, const.STATE_SPIN: SpinInteractorStyle, const.STATE_ZOOM: ZoomInteractorStyle, diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index bce5ca7..aa7f66a 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -346,6 +346,8 @@ class SurfaceManager(): actor = vtk.vtkActor() actor.SetMapper(mapper) + print("BOunds", actor.GetBounds()) + if overwrite: surface = Surface(index = self.last_surface_index) else: diff --git a/invesalius/data/transforms.pyx b/invesalius/data/transforms.pyx index a815f23..5c746d2 100644 --- a/invesalius/data/transforms.pyx +++ b/invesalius/data/transforms.pyx @@ -133,3 +133,42 @@ def apply_view_matrix_transform(image_t[:, :, :] volume, for y in xrange(dy): out[z, y, count] = coord_transform(volume, M, x, y, z, sx, sy, sz, f_interp, cval) count += 1 + + +@cython.boundscheck(False) # turn of bounds-checking for entire function +@cython.cdivision(True) +@cython.wraparound(False) +def convolve_non_zero(image_t[:, :, :] volume, + image_t[:, :, :] kernel, + image_t cval): + cdef Py_ssize_t x, y, z, sx, sy, sz, kx, ky, kz, skx, sky, skz, i, j, k + cdef image_t v + + cdef image_t[:, :, :] out = np.zeros_like(volume) + + sz = volume.shape[0] + sy = volume.shape[1] + sx = volume.shape[2] + + skz = kernel.shape[0] + sky = kernel.shape[1] + skx = kernel.shape[2] + + for z in prange(sz, nogil=True): + for y in xrange(sy): + for x in xrange(sx): + if volume[z, y, x] != 0: + for k in xrange(skz): + kz = z - skz // 2 + k + for j in xrange(sky): + ky = y - sky // 2 + j + for i in xrange(skx): + kx = x - skx // 2 + i + + if 0 <= kz < sz and 0 <= ky < sy and 0 <= kx < sx: + v = volume[kz, ky, kx] + else: + v = cval + + out[z, y, x] += (v * kernel[k, j, i]) + return np.asarray(out) diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index ed4a166..0b0744d 100644 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -49,6 +49,8 @@ import invesalius.session as ses import invesalius.data.converters as converters import invesalius.data.measures as measures +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX + if sys.platform == 'win32': try: import win32api @@ -159,382 +161,6 @@ class ContourMIPConfig(wx.Panel): self.txt_mip_border.Disable() -class CanvasRendererCTX: - def __init__(self, evt_renderer, canvas_renderer, orientation=None): - """ - A Canvas to render over a vtktRenderer. - - Params: - evt_renderer: a vtkRenderer which this class is going to watch for - any render event to update the canvas content. - canvas_renderer: the vtkRenderer where the canvas is going to be - added. - - This class uses wx.GraphicsContext to render to a vtkImage. - - TODO: Verify why in Windows the color are strange when using transparency. - TODO: Add support to evento (ex. click on a square) - """ - self.canvas_renderer = canvas_renderer - 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 - self._drawn = False - self._init_canvas() - evt_renderer.AddObserver("StartEvent", self.OnPaint) - - def _init_canvas(self): - w, h = self._size - self._array = np.zeros((h, w, 4), dtype=np.uint8) - - self._cv_image = converters.np_rgba_to_vtk(self._array) - - self.mapper = vtk.vtkImageMapper() - self.mapper.SetInputData(self._cv_image) - self.mapper.SetColorWindow(255) - self.mapper.SetColorLevel(128) - - self.actor = vtk.vtkActor2D() - self.actor.SetPosition(0, 0) - self.actor.SetMapper(self.mapper) - self.actor.GetProperty().SetOpacity(0.99) - - self.canvas_renderer.AddActor2D(self.actor) - - self.rgb = np.zeros((h, w, 3), dtype=np.uint8) - self.alpha = np.zeros((h, w, 1), dtype=np.uint8) - - self.bitmap = wx.EmptyBitmapRGBA(w, h) - try: - self.image = wx.Image(w, h, self.rgb, self.alpha) - except TypeError: - self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha) - - def _resize_canvas(self, w, h): - self._array = np.zeros((h, w, 4), dtype=np.uint8) - self._cv_image = converters.np_rgba_to_vtk(self._array) - self.mapper.SetInputData(self._cv_image) - self.mapper.Update() - - self.rgb = np.zeros((h, w, 3), dtype=np.uint8) - self.alpha = np.zeros((h, w, 1), dtype=np.uint8) - - self.bitmap = wx.EmptyBitmapRGBA(w, h) - try: - self.image = wx.Image(w, h, self.rgb, self.alpha) - except TypeError: - self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha) - - self.modified = True - - def remove_from_renderer(self): - self.canvas_renderer.RemoveActor(self.actor) - self.evt_renderer.RemoveObservers("StartEvent") - - def OnPaint(self, evt, obj): - size = self.canvas_renderer.GetSize() - w, h = size - if self._size != size: - self._size = size - self._resize_canvas(w, h) - - cam_modif_time = self.evt_renderer.GetActiveCamera().GetMTime() - if (not self.modified) and cam_modif_time == self.last_cam_modif_time: - return - - self.last_cam_modif_time = cam_modif_time - - self._array[:] = 0 - - coord = vtk.vtkCoordinate() - - self.image.SetDataBuffer(self.rgb) - self.image.SetAlphaBuffer(self.alpha) - self.image.Clear() - gc = wx.GraphicsContext.Create(self.image) - if sys.platform != 'darwin': - gc.SetAntialiasMode(0) - - self.gc = gc - - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - # font.SetWeight(wx.BOLD) - font = gc.CreateFont(font, (0, 0, 255)) - gc.SetFont(font) - - pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID) - brush = wx.Brush(wx.Colour(0, 255, 0, 128)) - gc.SetPen(pen) - gc.SetBrush(brush) - gc.Scale(1, -1) - - for d in self.draw_list: - d.draw_to_canvas(gc, self) - - gc.Destroy() - - self.gc = None - - if self._drawn: - self.bitmap = self.image.ConvertToBitmap() - self.bitmap.CopyToBuffer(self._array, wx.BitmapBufferFormat_RGBA) - - self._cv_image.Modified() - self.modified = False - self._drawn = False - - def calc_text_size(self, text, font=None): - """ - Given an unicode text and a font returns the width and height of the - rendered text in pixels. - - Params: - text: An unicode text. - font: An wxFont. - - Returns: - A tuple with width and height values in pixels - """ - if self.gc is None: - return None - gc = self.gc - - if font is None: - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - - _font = gc.CreateFont(font) - gc.SetFont(_font) - w, h = gc.GetTextExtent(text) - return w, h - - def draw_line(self, pos0, pos1, arrow_start=False, arrow_end=False, colour=(255, 0, 0, 128), width=2, style=wx.SOLID): - """ - Draw a line from pos0 to pos1 - - Params: - pos0: the start of the line position (x, y). - pos1: the end of the line position (x, y). - arrow_start: if to draw a arrow at the start of the line. - arrow_end: if to draw a arrow at the end of the line. - colour: RGBA line colour. - width: the width of line. - style: default wx.SOLID. - """ - if self.gc is None: - return None - gc = self.gc - - p0x, p0y = pos0 - p1x, p1y = pos1 - - p0y = -p0y - p1y = -p1y - - pen = wx.Pen(wx.Colour(*[int(c) for c in colour]), width, wx.SOLID) - pen.SetCap(wx.CAP_BUTT) - gc.SetPen(pen) - - path = gc.CreatePath() - path.MoveToPoint(p0x, p0y) - path.AddLineToPoint(p1x, p1y) - gc.StrokePath(path) - - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - font = gc.CreateFont(font) - gc.SetFont(font) - w, h = gc.GetTextExtent("M") - - p0 = np.array((p0x, p0y)) - p3 = np.array((p1x, p1y)) - if arrow_start: - v = p3 - p0 - v = v / np.linalg.norm(v) - iv = np.array((v[1], -v[0])) - p1 = p0 + w*v + iv*w/2.0 - p2 = p0 + w*v + (-iv)*w/2.0 - - path = gc.CreatePath() - path.MoveToPoint(p0) - path.AddLineToPoint(p1) - path.MoveToPoint(p0) - path.AddLineToPoint(p2) - gc.StrokePath(path) - - if arrow_end: - v = p3 - p0 - v = v / np.linalg.norm(v) - iv = np.array((v[1], -v[0])) - p1 = p3 - w*v + iv*w/2.0 - p2 = p3 - w*v + (-iv)*w/2.0 - - path = gc.CreatePath() - path.MoveToPoint(p3) - path.AddLineToPoint(p1) - path.MoveToPoint(p3) - path.AddLineToPoint(p2) - gc.StrokePath(path) - - self._drawn = True - - def draw_circle(self, center, radius, width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)): - """ - Draw a circle centered at center with the given radius. - - Params: - center: (x, y) position. - radius: float number. - width: line width. - line_colour: RGBA line colour - fill_colour: RGBA fill colour. - """ - if self.gc is None: - return None - gc = self.gc - - pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID) - gc.SetPen(pen) - - brush = wx.Brush(wx.Colour(*fill_colour)) - gc.SetBrush(brush) - - cx, cy = center - cy = -cy - - path = gc.CreatePath() - path.AddCircle(cx, cy, 2.5) - gc.StrokePath(path) - gc.FillPath(path) - self._drawn = True - - def draw_rectangle(self, pos, width, height, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)): - """ - Draw a rectangle with its top left at pos and with the given width and height. - - Params: - pos: The top left pos (x, y) of the rectangle. - width: width of the rectangle. - height: heigth of the rectangle. - line_colour: RGBA line colour. - fill_colour: RGBA fill colour. - """ - if self.gc is None: - return None - gc = self.gc - - px, py = pos - gc.SetPen(wx.Pen(line_colour)) - gc.SetBrush(wx.Brush(fill_colour)) - gc.DrawRectangle(px, py, width, height) - self._drawn = True - - def draw_text(self, text, pos, font=None, txt_colour=(255, 255, 255)): - """ - Draw text. - - Params: - text: an unicode text. - pos: (x, y) top left position. - font: if None it'll use the default gui font. - txt_colour: RGB text colour - """ - if self.gc is None: - return None - gc = self.gc - - if font is None: - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - - font = gc.CreateFont(font, txt_colour) - gc.SetFont(font) - - px, py = pos - py = -py - - gc.DrawText(text, px, py) - self._drawn = True - - def draw_text_box(self, text, pos, font=None, txt_colour=(255, 255, 255), bg_colour=(128, 128, 128, 128), border=5): - """ - Draw text inside a text box. - - Params: - text: an unicode text. - pos: (x, y) top left position. - font: if None it'll use the default gui font. - txt_colour: RGB text colour - bg_colour: RGBA box colour - border: the border size. - """ - if self.gc is None: - return None - gc = self.gc - - if font is None: - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) - - _font = gc.CreateFont(font, txt_colour) - gc.SetFont(_font) - w, h = gc.GetTextExtent(text) - - px, py = pos - - # Drawing the box - cw, ch = w + border * 2, h + border * 2 - self.draw_rectangle((px, -py), cw, ch, bg_colour, bg_colour) - - # Drawing the text - tpx, tpy = px + border, py - border - self.draw_text(text, (tpx, tpy), font, txt_colour) - self._drawn = True - - def draw_arc(self, center, p0, p1, line_colour=(255, 0, 0, 128), width=2): - """ - Draw an arc passing in p0 and p1 centered at center. - - Params: - center: (x, y) center of the arc. - p0: (x, y). - p1: (x, y). - line_colour: RGBA line colour. - width: width of the line. - """ - if self.gc is None: - return None - gc = self.gc - pen = wx.Pen(wx.Colour(*[int(c) for c in line_colour]), width, wx.SOLID) - gc.SetPen(pen) - - c = np.array(center) - v0 = np.array(p0) - c - v1 = np.array(p1) - c - - c[1] = -c[1] - v0[1] = -v0[1] - v1[1] = -v1[1] - - s0 = np.linalg.norm(v0) - s1 = np.linalg.norm(v1) - - a0 = np.arctan2(v0[1] , v0[0]) - a1 = np.arctan2(v1[1] , v1[0]) - - if (a1 - a0) % (np.pi*2) < (a0 - a1) % (np.pi*2): - sa = a0 - ea = a1 - else: - sa = a1 - ea = a0 - - path = gc.CreatePath() - path.AddArc(float(c[0]), float(c[1]), float(min(s0, s1)), float(sa), float(ea), True) - gc.StrokePath(path) - self._drawn = True - - class Viewer(wx.Panel): def __init__(self, prnt, orientation='AXIAL'): @@ -563,6 +189,8 @@ class Viewer(wx.Panel): self.canvas = None + self.draw_by_slice_number = collections.defaultdict(list) + # The layout from slice_data, the first is number of cols, the second # is the number of rows self.layout = (1, 1) @@ -1037,8 +665,10 @@ class Viewer(wx.Panel): z = bounds[4] return x, y, z - def get_coordinate_cursor_edition(self, slice_data, picker=None): + def get_coordinate_cursor_edition(self, slice_data=None, picker=None): # Find position + if slice_data is None: + slice_data = self.slice_data actor = slice_data.actor slice_number = slice_data.number if picker is None: @@ -1460,7 +1090,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.orientation) + self.canvas = CanvasRendererCTX(self, self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation) self.canvas.draw_list.append(self.slice_data.text) # Set the slice number to the last slice to ensure the camera if far @@ -1602,21 +1232,27 @@ class Viewer(wx.Panel): def UpdateCanvas(self, evt=None): if self.canvas is not None: - cp_draw_list = self.canvas.draw_list[:] - self.canvas.draw_list = [] + self._update_draw_list() + self.canvas.modified = True + self.interactor.Render() - # Removing all measures - for i in cp_draw_list: - if not isinstance(i, (measures.AngularMeasure, measures.LinearMeasure)): - self.canvas.draw_list.append(i) + def _update_draw_list(self): + cp_draw_list = self.canvas.draw_list[:] + self.canvas.draw_list = [] - # Then add all needed measures - for (m, mr) in self.measures.get(self.orientation, self.slice_data.number): - if m.visible: - self.canvas.draw_list.append(mr) + # Removing all measures + for i in cp_draw_list: + if not isinstance(i, (measures.AngularMeasure, measures.LinearMeasure, measures.CircleDensityMeasure, measures.PolygonDensityMeasure)): + self.canvas.draw_list.append(i) + + # Then add all needed measures + for (m, mr) in self.measures.get(self.orientation, self.slice_data.number): + if m.visible: + self.canvas.draw_list.append(mr) + + n = self.slice_data.number + self.canvas.draw_list.extend(self.draw_by_slice_number[n]) - self.canvas.modified = True - self.interactor.Render() def __configure_scroll(self): actor = self.slice_data_list[0].actor @@ -1799,15 +1435,16 @@ class Viewer(wx.Panel): for actor in self.actors_by_slice_number[index]: self.slice_data.renderer.AddActor(actor) - for (m, mr) in self.measures.get(self.orientation, self.slice_data.number): - try: - self.canvas.draw_list.remove(mr) - except ValueError: - pass + # for (m, mr) in self.measures.get(self.orientation, self.slice_data.number): + # try: + # self.canvas.draw_list.remove(mr) + # except ValueError: + # pass + + # for (m, mr) in self.measures.get(self.orientation, index): + # if m.visible: + # self.canvas.draw_list.append(mr) - for (m, mr) in self.measures.get(self.orientation, index): - if m.visible: - self.canvas.draw_list.append(mr) if self.slice_._type_projection == const.PROJECTION_NORMAL: self.slice_data.SetNumber(index) @@ -1817,6 +1454,7 @@ class Viewer(wx.Panel): self.slice_data.SetNumber(index, end) self.__update_display_extent(image) self.cross.SetModelBounds(self.slice_data.actor.GetBounds()) + self._update_draw_list() def ChangeSliceNumber(self, index): #self.set_slice_number(index) diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 53ef78a..24100c9 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -32,6 +32,8 @@ from wx.lib.pubsub import pub as Publisher import random from scipy.spatial import distance +from scipy.misc import imsave + import invesalius.constants as const import invesalius.data.bases as bases import invesalius.data.transformations as tr @@ -51,6 +53,8 @@ else: PROP_MEASURE = 0.8 +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX, Polygon + class Viewer(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, size=wx.Size(320, 320)) @@ -86,17 +90,32 @@ class Viewer(wx.Panel): interactor.Enable(1) ren = vtk.vtkRenderer() - interactor.GetRenderWindow().AddRenderer(ren) self.ren = ren + canvas_renderer = vtk.vtkRenderer() + canvas_renderer.SetLayer(1) + canvas_renderer.SetInteractive(0) + canvas_renderer.PreserveDepthBufferOn() + self.canvas_renderer = canvas_renderer + + interactor.GetRenderWindow().SetNumberOfLayers(2) + interactor.GetRenderWindow().AddRenderer(ren) + interactor.GetRenderWindow().AddRenderer(canvas_renderer) + self.raycasting_volume = False self.onclick = False - self.text = vtku.Text() + self.text = vtku.TextZero() self.text.SetValue("") - self.ren.AddActor(self.text.actor) + self.text.SetPosition(const.TEXT_POS_LEFT_UP) + # self.ren.AddActor(self.text.actor) + + # self.polygon = Polygon(None, is_3d=False) + # self.canvas = CanvasRendererCTX(self, self.ren, self.canvas_renderer, 'AXIAL') + # self.canvas.draw_list.append(self.text) + # self.canvas.draw_list.append(self.polygon) # axes = vtk.vtkAxesActor() # axes.SetXAxisLabelText('x') # axes.SetYAxisLabelText('y') @@ -105,7 +124,6 @@ class Viewer(wx.Panel): # # self.ren.AddActor(axes) - self.slice_plane = None self.view_angle = None @@ -1230,8 +1248,17 @@ class Viewer(wx.Panel): def __bind_events_wx(self): #self.Bind(wx.EVT_SIZE, self.OnSize) + # self.canvas.subscribe_event('LeftButtonPressEvent', self.on_insert_point) pass + def on_insert_point(self, evt): + pos = evt.position + self.polygon.append_point(pos) + self.canvas.Refresh() + + arr = self.canvas.draw_element_to_array([self.polygon,]) + imsave('/tmp/polygon.png', arr) + def SetInteractorStyle(self, state): action = { const.STATE_PAN: @@ -1304,7 +1331,7 @@ class Viewer(wx.Panel): self.style = style # Check each event available for each mode - for event in action[state]: + for event in action.get(state, []): # Bind event style.AddObserver(event,action[state][event]) @@ -1483,6 +1510,7 @@ class Viewer(wx.Panel): def OnSetWindowLevelText(self, ww, wl): if self.raycasting_volume: self.text.SetValue("WL: %d WW: %d"%(wl, ww)) + self.canvas.modified = True def OnShowRaycasting(self): if not self.raycasting_volume: diff --git a/invesalius/data/vtk_utils.py b/invesalius/data/vtk_utils.py index 6df54aa..5b3564c 100644 --- a/invesalius/data/vtk_utils.py +++ b/invesalius/data/vtk_utils.py @@ -95,7 +95,8 @@ def ShowProgress(number_of_filters = 1, class Text(object): def __init__(self): - + self.layer = 99 + self.children = [] property = vtk.vtkTextProperty() property.SetFontSize(const.TEXT_SIZE) property.SetFontFamilyToArial() @@ -195,7 +196,8 @@ class Text(object): class TextZero(object): def __init__(self): - + self.layer = 99 + self.children = [] property = vtk.vtkTextProperty() property.SetFontSize(const.TEXT_SIZE_LARGE) property.SetFontFamilyToArial() diff --git a/invesalius/gui/data_notebook.py b/invesalius/gui/data_notebook.py index 245f2e4..92af066 100644 --- a/invesalius/gui/data_notebook.py +++ b/invesalius/gui/data_notebook.py @@ -48,6 +48,8 @@ BTN_NEW, BTN_REMOVE, BTN_DUPLICATE, BTN_OPEN = [wx.NewId() for i in range(4)] TYPE = {const.LINEAR: _(u"Linear"), const.ANGULAR: _(u"Angular"), + const.DENSITY_ELLIPSE: _(u"Density Ellipse"), + const.DENSITY_POLYGON: _(u"Density Polygon"), } LOCATION = {const.SURFACE: _(u"3D"), @@ -1171,8 +1173,10 @@ class MeasuresListCtrlPanel(wx.ListCtrl, listmix.TextEditMixin, listmix.CheckLis location = LOCATION[m.location] if m.type == const.LINEAR: value = (u"%.2f mm") % m.value - else: + elif m.type == const.ANGULAR: value = (u"%.2f°") % m.value + else: + value = (u"%.3f") % m.value self.InsertNewItem(m.index, m.name, colour, location, type, value) if not m.visible: diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 2a2ee18..e8913a9 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -18,9 +18,13 @@ # detalhes. #-------------------------------------------------------------------------- +import itertools import os import random import sys +import time + +from concurrent import futures if sys.platform == 'win32': try: @@ -3317,6 +3321,112 @@ class FillHolesAutoDialog(wx.Dialog): self.panel2dcon.Enable(0) +class MaskDensityDialog(wx.Dialog): + def __init__(self, title): + try: + pre = wx.PreDialog() + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Mask density"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT) + self.PostCreate(pre) + except AttributeError: + wx.Dialog.__init__(self, wx.GetApp().GetTopWindow(), -1, _(u"Mask density"), + style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT) + + self._init_gui() + self._bind_events() + + def _init_gui(self): + import invesalius.project as prj + project = prj.Project() + + self.cmb_mask = wx.ComboBox(self, -1, choices=[], style=wx.CB_READONLY) + if project.mask_dict.values(): + for mask in project.mask_dict.values(): + self.cmb_mask.Append(mask.name, mask) + self.cmb_mask.SetValue(list(project.mask_dict.values())[0].name) + + self.calc_button = wx.Button(self, -1, _(u'Calculate')) + + self.mean_density = self._create_selectable_label_text('') + self.min_density = self._create_selectable_label_text('') + self.max_density = self._create_selectable_label_text('') + self.std_density = self._create_selectable_label_text('') + + + slt_mask_sizer = wx.FlexGridSizer(rows=1, cols=3, vgap=5, hgap=5) + slt_mask_sizer.AddMany([ + (wx.StaticText(self, -1, _(u'Mask:'), style=wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_CENTRE), + (self.cmb_mask, 1, wx.EXPAND), + (self.calc_button, 0, wx.EXPAND), + ]) + + values_sizer = wx.FlexGridSizer(rows=4, cols=2, vgap=5, hgap=5) + values_sizer.AddMany([ + (wx.StaticText(self, -1, _(u'Mean:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT), + (self.mean_density, 1, wx.EXPAND), + + (wx.StaticText(self, -1, _(u'Minimun:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT), + (self.min_density, 1, wx.EXPAND), + + (wx.StaticText(self, -1, _(u'Maximun:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT), + (self.max_density, 1, wx.EXPAND), + + (wx.StaticText(self, -1, _(u'Standard deviation:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT), + (self.std_density, 1, wx.EXPAND), + ]) + + sizer = wx.FlexGridSizer(rows=4, cols=1, vgap=5, hgap=5) + sizer.AddSpacer(5) + sizer.AddMany([ + (slt_mask_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 5) , + (values_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5), + ]) + sizer.AddSpacer(5) + + self.SetSizer(sizer) + sizer.Fit(self) + self.Layout() + + self.CenterOnScreen() + + def _create_selectable_label_text(self, text): + label = wx.TextCtrl(self, -1, style=wx.TE_READONLY) + label.SetValue(text) + # label.SetBackgroundColour(self.GetBackgroundColour()) + return label + + def _bind_events(self): + self.calc_button.Bind(wx.EVT_BUTTON, self.OnCalcButton) + + def OnCalcButton(self, evt): + from invesalius.data.slice_ import Slice + mask = self.cmb_mask.GetClientData(self.cmb_mask.GetSelection()) + + slc = Slice() + + with futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit(slc.calc_image_density, mask) + for c in itertools.cycle(['', '.', '..', '...']): + s = _(u'Calculating ') + c + self.mean_density.SetValue(s) + self.min_density.SetValue(s) + self.max_density.SetValue(s) + self.std_density.SetValue(s) + self.Update() + self.Refresh() + if future.done(): + break + time.sleep(0.1) + + _min, _max, _mean, _std = future.result() + + self.mean_density.SetValue(str(_mean)) + self.min_density.SetValue(str(_min)) + self.max_density.SetValue(str(_max)) + self.std_density.SetValue(str(_std)) + + print(">>>> Area of mask", slc.calc_mask_area(mask)) + + class ObjectCalibrationDialog(wx.Dialog): def __init__(self, nav_prop): diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 45d1abf..29c9978 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -472,6 +472,10 @@ class Frame(wx.Frame): elif id == const.ID_REORIENT_IMG: self.OnReorientImg() + elif id == const.ID_MASK_DENSITY_MEASURE: + ddlg = dlg.MaskDensityDialog(self) + ddlg.Show() + elif id == const.ID_THRESHOLD_SEGMENTATION: Publisher.sendMessage("Show panel", panel_id=const.ID_THRESHOLD_SEGMENTATION) Publisher.sendMessage('Disable actual style') @@ -767,6 +771,7 @@ class MenuBar(wx.MenuBar): const.ID_WATERSHED_SEGMENTATION, const.ID_THRESHOLD_SEGMENTATION, const.ID_FLOODFILL_SEGMENTATION, + const.ID_MASK_DENSITY_MEASURE, const.ID_CREATE_SURFACE, const.ID_CREATE_MASK, const.ID_GOTO_SLICE] @@ -923,6 +928,7 @@ class MenuBar(wx.MenuBar): image_menu.AppendMenu(wx.NewId(), _('Flip'), flip_menu) image_menu.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu) + mask_density_menu = image_menu.Append(const.ID_MASK_DENSITY_MEASURE, _(u'Mask Density measure')) reorient_menu = image_menu.Append(const.ID_REORIENT_IMG, _(u'Reorient image\tCtrl+Shift+R')) reorient_menu.Enable(False) @@ -931,9 +937,7 @@ class MenuBar(wx.MenuBar): tools_menu.AppendMenu(-1, _(u"Segmentation"), segmentation_menu) tools_menu.AppendMenu(-1, _(u"Surface"), surface_menu) - #View - self.view_menu = view_menu = wx.Menu() view_menu.Append(const.ID_VIEW_INTERPOLATED, _(u'Interpolated slices'), "", wx.ITEM_CHECK) @@ -1378,8 +1382,11 @@ class ObjectToolBar(AuiToolBar): const.STATE_SPIN, const.STATE_ZOOM_SL, const.STATE_ZOOM, const.STATE_MEASURE_DISTANCE, - const.STATE_MEASURE_ANGLE] - #const.STATE_ANNOTATE] + const.STATE_MEASURE_ANGLE, + const.STATE_MEASURE_DENSITY_ELLIPSE, + const.STATE_MEASURE_DENSITY_POLYGON, + # const.STATE_ANNOTATE + ] self.__init_items() self.__bind_events() self.__bind_events_wx() @@ -1431,6 +1438,10 @@ class ObjectToolBar(AuiToolBar): path = os.path.join(d, "measure_angle_original.png") BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) + BMP_ELLIPSE = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (48, 48)) + + BMP_POLYGON = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, wx.ART_TOOLBAR, (48, 48)) + #path = os.path.join(d, "tool_annotation_original.png") #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) @@ -1456,6 +1467,10 @@ class ObjectToolBar(AuiToolBar): path = os.path.join(d, "measure_angle.png") BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) + BMP_ELLIPSE = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (32, 32)) + + BMP_POLYGON = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, wx.ART_TOOLBAR, (32, 32)) + #path = os.path.join(d, "tool_annotation.png") #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) @@ -1502,6 +1517,20 @@ class ObjectToolBar(AuiToolBar): wx.NullBitmap, short_help_string = _("Measure angle"), kind = wx.ITEM_CHECK) + + self.AddTool(const.STATE_MEASURE_DENSITY_ELLIPSE, + "", + BMP_ELLIPSE, + wx.NullBitmap, + short_help_string = _("Measure density ellipse"), + kind = wx.ITEM_CHECK) + + self.AddTool(const.STATE_MEASURE_DENSITY_POLYGON, + "", + BMP_POLYGON, + wx.NullBitmap, + short_help_string = _("Measure density polygon"), + kind = wx.ITEM_CHECK) #self.AddLabelTool(const.STATE_ANNOTATE, # "", # shortHelp = _("Add annotation"), diff --git a/invesalius/gui/widgets/canvas_renderer.py b/invesalius/gui/widgets/canvas_renderer.py new file mode 100644 index 0000000..b8efde7 --- /dev/null +++ b/invesalius/gui/widgets/canvas_renderer.py @@ -0,0 +1,1233 @@ +# -*- coding: utf-8 -*- +#-------------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# 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 sys + +import numpy as np +import wx +import vtk + +try: + from weakref import WeakMethod +except ImportError: + from weakrefmethod import WeakMethod + +from invesalius.data import converters +from wx.lib.pubsub import pub as Publisher + + +class CanvasEvent: + def __init__(self, event_name, root_event_obj, pos, viewer, renderer, + control_down=False, alt_down=False, shift_down=False): + self.root_event_obj = root_event_obj + self.event_name = event_name + self.position = pos + self.viewer = viewer + self.renderer = renderer + + self.control_down = control_down + self.alt_down = alt_down + self.shift_down = shift_down + + +class CanvasRendererCTX: + def __init__(self, viewer, evt_renderer, canvas_renderer, orientation=None): + """ + A Canvas to render over a vtktRenderer. + + Params: + evt_renderer: a vtkRenderer which this class is going to watch for + any render event to update the canvas content. + canvas_renderer: the vtkRenderer where the canvas is going to be + added. + + This class uses wx.GraphicsContext to render to a vtkImage. + + TODO: Verify why in Windows the color are strange when using transparency. + TODO: Add support to evento (ex. click on a square) + """ + self.viewer = viewer + self.canvas_renderer = canvas_renderer + self.evt_renderer = evt_renderer + self._size = self.canvas_renderer.GetSize() + self.draw_list = [] + self._ordered_draw_list = [] + self.orientation = orientation + self.gc = None + self.last_cam_modif_time = -1 + self.modified = True + self._drawn = False + self._init_canvas() + + self._over_obj = None + self._drag_obj = None + self._selected_obj = None + + self._callback_events = { + 'LeftButtonPressEvent': [], + 'LeftButtonReleaseEvent': [], + 'LeftButtonDoubleClickEvent': [], + 'MouseMoveEvent': [], + } + + self._bind_events() + + def _bind_events(self): + iren = self.viewer.interactor + iren.Bind(wx.EVT_MOTION, self.OnMouseMove) + iren.Bind(wx.EVT_LEFT_DOWN, self.OnLeftButtonPress) + iren.Bind(wx.EVT_LEFT_UP, self.OnLeftButtonRelease) + iren.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick) + self.canvas_renderer.AddObserver("StartEvent", self.OnPaint) + + def subscribe_event(self, event, callback): + ref = WeakMethod(callback) + self._callback_events[event].append(ref) + + def unsubscribe_event(self, event, callback): + for n, cb in enumerate(self._callback_events[event]): + if cb() == callback: + print('removed') + self._callback_events[event].pop(n) + return + + def propagate_event(self, root, event): + print('propagating', event.event_name, 'from', root) + node = root + callback_name = 'on_%s' % event.event_name + while node: + try: + getattr(node, callback_name)(event) + except AttributeError as e: + print('errror', node, e) + node = node.parent + + def _init_canvas(self): + w, h = self._size + self._array = np.zeros((h, w, 4), dtype=np.uint8) + + self._cv_image = converters.np_rgba_to_vtk(self._array) + + self.mapper = vtk.vtkImageMapper() + self.mapper.SetInputData(self._cv_image) + self.mapper.SetColorWindow(255) + self.mapper.SetColorLevel(128) + + self.actor = vtk.vtkActor2D() + self.actor.SetPosition(0, 0) + self.actor.SetMapper(self.mapper) + self.actor.GetProperty().SetOpacity(0.99) + + self.canvas_renderer.AddActor2D(self.actor) + + self.rgb = np.zeros((h, w, 3), dtype=np.uint8) + self.alpha = np.zeros((h, w, 1), dtype=np.uint8) + + self.bitmap = wx.EmptyBitmapRGBA(w, h) + try: + self.image = wx.Image(w, h, self.rgb, self.alpha) + except TypeError: + self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha) + + def _resize_canvas(self, w, h): + self._array = np.zeros((h, w, 4), dtype=np.uint8) + self._cv_image = converters.np_rgba_to_vtk(self._array) + self.mapper.SetInputData(self._cv_image) + self.mapper.Update() + + self.rgb = np.zeros((h, w, 3), dtype=np.uint8) + self.alpha = np.zeros((h, w, 1), dtype=np.uint8) + + self.bitmap = wx.EmptyBitmapRGBA(w, h) + try: + self.image = wx.Image(w, h, self.rgb, self.alpha) + except TypeError: + self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha) + + self.modified = True + + def remove_from_renderer(self): + self.canvas_renderer.RemoveActor(self.actor) + self.evt_renderer.RemoveObservers("StartEvent") + + def get_over_mouse_obj(self, x, y): + for n, i in self._ordered_draw_list[::-1]: + try: + obj = i.is_over(x, y) + self._over_obj = obj + if obj: + print("is over at", n, i) + return True + except AttributeError: + pass + return False + + def Refresh(self): + print('Refresh') + self.modified = True + self.viewer.interactor.Render() + + def OnMouseMove(self, evt): + x, y = evt.GetPosition() + y = self.viewer.interactor.GetSize()[1] - y + redraw = False + + if self._drag_obj: + redraw = True + evt_obj = CanvasEvent('mouse_move', self._drag_obj, (x, y), self.viewer, self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + self.propagate_event(self._drag_obj, evt_obj) + # self._drag_obj.mouse_move(evt_obj) + else: + was_over = self._over_obj + redraw = self.get_over_mouse_obj(x, y) or was_over + + if was_over and was_over != self._over_obj: + try: + evt_obj = CanvasEvent('mouse_leave', was_over, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + was_over.on_mouse_leave(evt_obj) + except AttributeError: + pass + + if self._over_obj: + try: + evt_obj = CanvasEvent('mouse_enter', self._over_obj, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + self._over_obj.on_mouse_enter(evt_obj) + except AttributeError: + pass + + if redraw: + # Publisher.sendMessage('Redraw canvas %s' % self.orientation) + self.Refresh() + + evt.Skip() + + def OnLeftButtonPress(self, evt): + x, y = evt.GetPosition() + y = self.viewer.interactor.GetSize()[1] - y + if self._over_obj and hasattr(self._over_obj, 'on_mouse_move'): + if hasattr(self._over_obj, 'on_select'): + try: + evt_obj = CanvasEvent('deselect', self._over_obj, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + # self._selected_obj.on_deselect(evt_obj) + self.propagate_event(self._selected_obj, evt_obj) + except AttributeError: + pass + evt_obj = CanvasEvent('select', self._over_obj, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + # self._over_obj.on_select(evt_obj) + self.propagate_event(self._over_obj, evt_obj) + self._selected_obj = self._over_obj + self.Refresh() + self._drag_obj = self._over_obj + else: + self.get_over_mouse_obj(x, y) + if not self._over_obj: + evt_obj = CanvasEvent('leftclick', None, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + # self._selected_obj.on_deselect(evt_obj) + for cb in self._callback_events['LeftButtonPressEvent']: + if cb() is not None: + cb()(evt_obj) + break + try: + evt_obj = CanvasEvent('deselect', self._over_obj, (x, y), self.viewer, + self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + # self._selected_obj.on_deselect(evt_obj) + if self._selected_obj.on_deselect(evt_obj): + self.Refresh() + except AttributeError: + pass + evt.Skip() + + def OnLeftButtonRelease(self, evt): + self._over_obj = None + self._drag_obj = None + evt.Skip() + + def OnDoubleClick(self, evt): + x, y = evt.GetPosition() + y = self.viewer.interactor.GetSize()[1] - y + evt_obj = CanvasEvent('double_left_click', None, (x, y), self.viewer, self.evt_renderer, + control_down=evt.ControlDown(), + alt_down=evt.AltDown(), + shift_down=evt.ShiftDown()) + for cb in self._callback_events['LeftButtonDoubleClickEvent']: + if cb() is not None: + cb()(evt_obj) + break + evt.Skip() + + def OnPaint(self, evt, obj): + size = self.canvas_renderer.GetSize() + w, h = size + if self._size != size: + self._size = size + self._resize_canvas(w, h) + + cam_modif_time = self.evt_renderer.GetActiveCamera().GetMTime() + if (not self.modified) and cam_modif_time == self.last_cam_modif_time: + return + + self.last_cam_modif_time = cam_modif_time + + self._array[:] = 0 + + coord = vtk.vtkCoordinate() + + self.image.SetDataBuffer(self.rgb) + self.image.SetAlphaBuffer(self.alpha) + self.image.Clear() + gc = wx.GraphicsContext.Create(self.image) + if sys.platform != 'darwin': + gc.SetAntialiasMode(0) + + self.gc = gc + + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + # font.SetWeight(wx.BOLD) + font = gc.CreateFont(font, (0, 0, 255)) + gc.SetFont(font) + + pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID) + brush = wx.Brush(wx.Colour(0, 255, 0, 128)) + gc.SetPen(pen) + gc.SetBrush(brush) + gc.Scale(1, -1) + + self._ordered_draw_list = sorted(self._follow_draw_list(), key=lambda x: x[0]) + for l, d in self._ordered_draw_list: #sorted(self.draw_list, key=lambda x: x.layer if hasattr(x, 'layer') else 0): + d.draw_to_canvas(gc, self) + + gc.Destroy() + + self.gc = None + + if self._drawn: + self.bitmap = self.image.ConvertToBitmap() + self.bitmap.CopyToBuffer(self._array, wx.BitmapBufferFormat_RGBA) + + self._cv_image.Modified() + self.modified = False + self._drawn = False + + def _follow_draw_list(self): + out = [] + def loop(node, layer): + for child in node.children: + loop(child, layer + child.layer) + out.append((layer + child.layer, child)) + + for element in self.draw_list: + out.append((element.layer, element)) + if hasattr(element, 'children'): + loop(element,element.layer) + + return out + + + def draw_element_to_array(self, elements, size=None, antialiasing=False, flip=True): + """ + Draws the given elements to a array. + + Params: + elements: a list of elements (objects that contains the + draw_to_canvas method) to draw to a array. + flip: indicates if it is necessary to flip. In this canvas the Y + coordinates starts in the bottom of the screen. + """ + if size is None: + size = self.canvas_renderer.GetSize() + w, h = size + image = wx.EmptyImage(w, h) + image.Clear() + + arr = np.zeros((h, w, 4), dtype=np.uint8) + + gc = wx.GraphicsContext.Create(image) + if antialiasing: + gc.SetAntialiasMode(0) + + old_gc = self.gc + self.gc = gc + + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + font = gc.CreateFont(font, (0, 0, 255)) + gc.SetFont(font) + + pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID) + brush = wx.Brush(wx.Colour(0, 255, 0, 128)) + gc.SetPen(pen) + gc.SetBrush(brush) + gc.Scale(1, -1) + + for element in elements: + element.draw_to_canvas(gc, self) + + gc.Destroy() + self.gc = old_gc + + bitmap = image.ConvertToBitmap() + bitmap.CopyToBuffer(arr, wx.BitmapBufferFormat_RGBA) + + if flip: + arr = arr[::-1] + + return arr + + def calc_text_size(self, text, font=None): + """ + Given an unicode text and a font returns the width and height of the + rendered text in pixels. + + Params: + text: An unicode text. + font: An wxFont. + + Returns: + A tuple with width and height values in pixels + """ + if self.gc is None: + return None + gc = self.gc + + if font is None: + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + + _font = gc.CreateFont(font) + gc.SetFont(_font) + + w = 0 + h = 0 + for t in text.split('\n'): + _w, _h = gc.GetTextExtent(t) + w = max(w, _w) + h += _h + return w, h + + def draw_line(self, pos0, pos1, arrow_start=False, arrow_end=False, colour=(255, 0, 0, 128), width=2, style=wx.SOLID): + """ + Draw a line from pos0 to pos1 + + Params: + pos0: the start of the line position (x, y). + pos1: the end of the line position (x, y). + arrow_start: if to draw a arrow at the start of the line. + arrow_end: if to draw a arrow at the end of the line. + colour: RGBA line colour. + width: the width of line. + style: default wx.SOLID. + """ + if self.gc is None: + return None + gc = self.gc + + p0x, p0y = pos0 + p1x, p1y = pos1 + + p0y = -p0y + p1y = -p1y + + pen = wx.Pen(wx.Colour(*[int(c) for c in colour]), width, wx.SOLID) + pen.SetCap(wx.CAP_BUTT) + gc.SetPen(pen) + + path = gc.CreatePath() + path.MoveToPoint(p0x, p0y) + path.AddLineToPoint(p1x, p1y) + gc.StrokePath(path) + + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + font = gc.CreateFont(font) + gc.SetFont(font) + w, h = gc.GetTextExtent("M") + + p0 = np.array((p0x, p0y)) + p3 = np.array((p1x, p1y)) + if arrow_start: + v = p3 - p0 + v = v / np.linalg.norm(v) + iv = np.array((v[1], -v[0])) + p1 = p0 + w*v + iv*w/2.0 + p2 = p0 + w*v + (-iv)*w/2.0 + + path = gc.CreatePath() + path.MoveToPoint(p0) + path.AddLineToPoint(p1) + path.MoveToPoint(p0) + path.AddLineToPoint(p2) + gc.StrokePath(path) + + if arrow_end: + v = p3 - p0 + v = v / np.linalg.norm(v) + iv = np.array((v[1], -v[0])) + p1 = p3 - w*v + iv*w/2.0 + p2 = p3 - w*v + (-iv)*w/2.0 + + path = gc.CreatePath() + path.MoveToPoint(p3) + path.AddLineToPoint(p1) + path.MoveToPoint(p3) + path.AddLineToPoint(p2) + gc.StrokePath(path) + + self._drawn = True + + def draw_circle(self, center, radius=2.5, width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)): + """ + Draw a circle centered at center with the given radius. + + Params: + center: (x, y) position. + radius: float number. + width: line width. + line_colour: RGBA line colour + fill_colour: RGBA fill colour. + """ + if self.gc is None: + return None + gc = self.gc + + pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID) + gc.SetPen(pen) + + brush = wx.Brush(wx.Colour(*fill_colour)) + gc.SetBrush(brush) + + cx, cy = center + cy = -cy + + path = gc.CreatePath() + path.AddCircle(cx, cy, radius) + gc.StrokePath(path) + gc.FillPath(path) + self._drawn = True + + return (cx, -cy, radius*2, radius*2) + + def draw_ellipse(self, center, width, height, line_width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)): + """ + Draw a ellipse centered at center with the given width and height. + + Params: + center: (x, y) position. + width: ellipse width (float number). + height: ellipse height (float number) + line_width: line width. + line_colour: RGBA line colour + fill_colour: RGBA fill colour. + """ + if self.gc is None: + return None + gc = self.gc + + pen = wx.Pen(wx.Colour(*line_colour), line_width, wx.SOLID) + gc.SetPen(pen) + + brush = wx.Brush(wx.Colour(*fill_colour)) + gc.SetBrush(brush) + + cx, cy = center + xi = cx - width/2.0 + xf = cx + width/2.0 + yi = cy - height/2.0 + yf = cy + height/2.0 + + cx -= width/2.0 + cy += height/2.0 + cy = -cy + + path = gc.CreatePath() + path.AddEllipse(cx, cy, width, height) + gc.StrokePath(path) + gc.FillPath(path) + self._drawn = True + + return (xi, yi, xf, yf) + + def draw_rectangle(self, pos, width, height, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)): + """ + Draw a rectangle with its top left at pos and with the given width and height. + + Params: + pos: The top left pos (x, y) of the rectangle. + width: width of the rectangle. + height: heigth of the rectangle. + line_colour: RGBA line colour. + fill_colour: RGBA fill colour. + """ + if self.gc is None: + return None + gc = self.gc + + px, py = pos + py = -py + gc.SetPen(wx.Pen(wx.Colour(*line_colour))) + gc.SetBrush(wx.Brush(wx.Colour(*fill_colour))) + gc.DrawRectangle(px, py, width, height) + self._drawn = True + + def draw_text(self, text, pos, font=None, txt_colour=(255, 255, 255)): + """ + Draw text. + + Params: + text: an unicode text. + pos: (x, y) top left position. + font: if None it'll use the default gui font. + txt_colour: RGB text colour + """ + if self.gc is None: + return None + gc = self.gc + + if font is None: + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + + font = gc.CreateFont(font, txt_colour) + gc.SetFont(font) + + px, py = pos + for t in text.split('\n'): + _py = -py + _px = px + gc.DrawText(t, _px, _py) + + w, h = self.calc_text_size(t) + py -= h + + self._drawn = True + + def draw_text_box(self, text, pos, font=None, txt_colour=(255, 255, 255), bg_colour=(128, 128, 128, 128), border=5): + """ + Draw text inside a text box. + + Params: + text: an unicode text. + pos: (x, y) top left position. + font: if None it'll use the default gui font. + txt_colour: RGB text colour + bg_colour: RGBA box colour + border: the border size. + """ + if self.gc is None: + return None + gc = self.gc + + if font is None: + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + + _font = gc.CreateFont(font, txt_colour) + gc.SetFont(_font) + w, h = self.calc_text_size(text) + + px, py = pos + + # Drawing the box + cw, ch = w + border * 2, h + border * 2 + self.draw_rectangle((px, py), cw, ch, bg_colour, bg_colour) + + # Drawing the text + tpx, tpy = px + border, py - border + self.draw_text(text, (tpx, tpy), font, txt_colour) + self._drawn = True + + return px, py, cw, ch + + def draw_arc(self, center, p0, p1, line_colour=(255, 0, 0, 128), width=2): + """ + Draw an arc passing in p0 and p1 centered at center. + + Params: + center: (x, y) center of the arc. + p0: (x, y). + p1: (x, y). + line_colour: RGBA line colour. + width: width of the line. + """ + if self.gc is None: + return None + gc = self.gc + pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID) + gc.SetPen(pen) + + c = np.array(center) + v0 = np.array(p0) - c + v1 = np.array(p1) - c + + c[1] = -c[1] + v0[1] = -v0[1] + v1[1] = -v1[1] + + s0 = np.linalg.norm(v0) + s1 = np.linalg.norm(v1) + + a0 = np.arctan2(v0[1] , v0[0]) + a1 = np.arctan2(v1[1] , v1[0]) + + if (a1 - a0) % (np.pi*2) < (a0 - a1) % (np.pi*2): + sa = a0 + ea = a1 + else: + sa = a1 + ea = a0 + + path = gc.CreatePath() + path.AddArc(float(c[0]), float(c[1]), float(min(s0, s1)), float(sa), float(ea), True) + gc.StrokePath(path) + self._drawn = True + + def draw_polygon(self, points, fill=True, closed=False, line_colour=(255, 255, 255, 255), + fill_colour=(255, 255, 255, 255), width=2): + + if self.gc is None: + return None + gc = self.gc + + gc.SetPen(wx.Pen(wx.Colour(*line_colour), width, wx.SOLID)) + gc.SetBrush(wx.Brush(wx.Colour(*fill_colour), wx.SOLID)) + + if points: + path = gc.CreatePath() + px, py = points[0] + path.MoveToPoint((px, -py)) + + for point in points: + px, py = point + path.AddLineToPoint((px, -py)) + + if closed: + px, py = points[0] + path.AddLineToPoint((px, -py)) + + gc.StrokePath(path) + gc.FillPath(path) + + self._drawn = True + + return path + + +class CanvasHandlerBase(object): + def __init__(self, parent): + self.parent = parent + self.children = [] + self.layer = 0 + self._visible = True + + @property + def visible(self): + return self._visible + + @visible.setter + def visible(self, value): + self._visible = value + for child in self.children: + child.visible = value + + def _3d_to_2d(self, renderer, pos): + coord = vtk.vtkCoordinate() + coord.SetValue(pos) + px, py = coord.GetComputedDoubleDisplayValue(renderer) + return px, py + + def add_child(self, child): + self.children.append(child) + + def draw_to_canvas(self, gc, canvas): + pass + + def is_over(self, x, y): + xi, yi, xf, yf = self.bbox + if xi <= x <= xf and yi <= y <= yf: + return self + return None + +class TextBox(CanvasHandlerBase): + def __init__(self, parent, + text, position=(0, 0, 0), + text_colour=(0, 0, 0, 255), + box_colour=(255, 255, 255, 255)): + super(TextBox, self).__init__(parent) + + self.layer = 0 + self.text = text + self.text_colour = text_colour + self.box_colour = box_colour + self.position = position + + self.children = [] + + self.bbox = (0, 0, 0, 0) + + self._highlight = False + + self._last_position = (0, 0, 0) + + def set_text(self, text): + self.text = text + + def draw_to_canvas(self, gc, canvas): + if self.visible: + px, py = self._3d_to_2d(canvas.evt_renderer, self.position) + + x, y, w, h = canvas.draw_text_box(self.text, (px, py), + txt_colour=self.text_colour, + bg_colour=self.box_colour) + if self._highlight: + rw, rh = canvas.evt_renderer.GetSize() + canvas.draw_rectangle((px, py), w, h, + (255, 0, 0, 25), + (255, 0, 0, 25)) + + self.bbox = (x, y - h, x + w, y) + + def is_over(self, x, y): + xi, yi, xf, yf = self.bbox + if xi <= x <= xf and yi <= y <= yf: + return self + return None + + def on_mouse_move(self, evt): + mx, my = evt.position + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + self.position = [i - j + k for (i, j, k) in zip((x, y, z), self._last_position, self.position)] + + self._last_position = (x, y, z) + + return True + + def on_mouse_enter(self, evt): + # self.layer = 99 + self._highlight = True + + def on_mouse_leave(self, evt): + # self.layer = 0 + self._highlight = False + + def on_select(self, evt): + mx, my = evt.position + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + self._last_position = (x, y, z) + + +class CircleHandler(CanvasHandlerBase): + def __init__(self, parent, position, radius=5, + line_colour=(255, 255, 255, 255), + fill_colour=(0, 0, 0, 0), is_3d=True): + + super(CircleHandler, self).__init__(parent) + + self.layer = 0 + self.position = position + self.radius = radius + self.line_colour = line_colour + self.fill_colour = fill_colour + self.bbox = (0, 0, 0, 0) + self.is_3d = is_3d + + self.children = [] + + self._on_move_function = None + + def on_move(self, evt_function): + self._on_move_function = WeakMethod(evt_function) + + def draw_to_canvas(self, gc, canvas): + if self.visible: + if self.is_3d: + px, py = self._3d_to_2d(canvas.evt_renderer, self.position) + else: + px, py = self.position + x, y, w, h = canvas.draw_circle((px, py), self.radius, + line_colour=self.line_colour, + fill_colour=self.fill_colour) + self.bbox = (x - w/2, y - h/2, x + w/2, y + h/2) + + def on_mouse_move(self, evt): + mx, my = evt.position + if self.is_3d: + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + self.position = (x, y, z) + + else: + self.position = mx, my + + if self._on_move_function and self._on_move_function(): + self._on_move_function()(self, evt) + + return True + + +class Polygon(CanvasHandlerBase): + def __init__(self, parent, + points=None, + fill=True, + closed=True, + line_colour=(255, 255, 255, 255), + fill_colour=(255, 255, 255, 128), width=2, + interactive=True, is_3d=True): + + super(Polygon, self).__init__(parent) + + self.layer = 0 + self.children = [] + + if points is None: + self.points = [] + else: + self.points = points + + self.handlers = [] + + self.fill = fill + self.closed = closed + self.line_colour = line_colour + + self._path = None + + if self.fill: + self.fill_colour = fill_colour + else: + self.fill_colour = (0, 0, 0, 0) + + self.width = width + self._interactive = interactive + self.is_3d = is_3d + + @property + def interactive(self): + return self._interactive + + @interactive.setter + def interactive(self, value): + self._interactive = value + for handler in self.handlers: + handler.visible = value + + def draw_to_canvas(self, gc, canvas): + if self.visible and self.points: + if self.is_3d: + points = [self._3d_to_2d(canvas.evt_renderer, p) for p in self.points] + else: + points = self.points + self._path = canvas.draw_polygon(points, self.fill, self.closed, self.line_colour, self.fill_colour, self.width) + + # if self.closed: + # U, L = self.convex_hull(points, merge=False) + # canvas.draw_polygon(U, self.fill, self.closed, self.line_colour, (0, 255, 0, 255), self.width) + # canvas.draw_polygon(L, self.fill, self.closed, self.line_colour, (0, 0, 255, 255), self.width) + # for p0, p1 in self.get_all_antipodal_pairs(points): + # canvas.draw_line(p0, p1) + + # if self.interactive: + # for handler in self.handlers: + # handler.draw_to_canvas(gc, canvas) + + def append_point(self, point): + handler = CircleHandler(self, point, is_3d=self.is_3d, fill_colour=(255, 0, 0, 255)) + handler.layer = 1 + self.add_child(handler) + # handler.on_move(self.on_move_point) + self.handlers.append(handler) + self.points.append(point) + + def on_mouse_move(self, evt): + if evt.root_event_obj is self: + self.on_mouse_move2(evt) + else: + self.points = [] + for handler in self.handlers: + self.points.append(handler.position) + + def is_over(self, x, y): + if self.closed and self._path and self._path.Contains(x, -y): + return self + + def on_mouse_move2(self, evt): + mx, my = evt.position + if self.is_3d: + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + new_pos = (x, y, z) + else: + new_pos = mx, my + + diff = [i-j for i,j in zip(new_pos, self._last_position)] + + for n, point in enumerate(self.points): + self.points[n] = tuple((i+j for i,j in zip(diff, point))) + self.handlers[n].position = self.points[n] + + self._last_position = new_pos + + return True + + def on_mouse_enter(self, evt): + pass + # self.interactive = True + # self.layer = 99 + + def on_mouse_leave(self, evt): + pass + # self.interactive = False + # self.layer = 0 + + def on_select(self, evt): + mx, my = evt.position + self.interactive = True + print("on_select", self.interactive) + if self.is_3d: + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + self._last_position = (x, y, z) + else: + self._last_position = (mx, my) + + def on_deselect(self, evt): + self.interactive = False + return True + + def convex_hull(self, points, merge=True): + spoints = sorted(points) + U = [] + L = [] + + _dir = lambda o, a, b: (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + for p in spoints: + while len(L) >= 2 and _dir(L[-2], L[-1], p) <= 0: + L.pop() + L.append(p) + + for p in reversed(spoints): + while len(U) >= 2 and _dir(U[-2], U[-1], p) <= 0: + U.pop() + U.append(p) + + if merge: + return U + L + return U, L + + def get_all_antipodal_pairs(self, points): + U, L = self.convex_hull(points, merge=False) + i = 0 + j = len(L) - 1 + while i < len(U) - 1 or j > 0: + yield U[i], L[j] + + if i == len(U) - 1: + j -= 1 + elif j == 0: + i += 1 + elif (U[i+1][1]-U[i][1])*(L[j][0]-L[j-1][0]) > (L[j][1]-L[j-1][1])*(U[i+1][0]-U[i][0]): + i += 1 + else: + j -= 1 + + +class Ellipse(CanvasHandlerBase): + def __init__(self, parent, + center, + point1, point2, + fill=True, + line_colour=(255, 255, 255, 255), + fill_colour=(255, 255, 255, 128), width=2, + interactive=True, is_3d=True): + + super(Ellipse, self).__init__(parent) + + self.children = [] + self.layer = 0 + + self.center = center + self.point1 = point1 + self.point2 = point2 + + self.bbox = (0, 0, 0, 0) + + self.fill = fill + self.line_colour = line_colour + if self.fill: + self.fill_colour = fill_colour + else: + self.fill_colour = (0, 0, 0, 0) + self.width = width + self._interactive = interactive + self.is_3d = is_3d + + self.handler_1 = CircleHandler(self, self.point1, is_3d=is_3d, fill_colour=(255, 0, 0, 255)) + self.handler_1.layer = 1 + self.handler_2 = CircleHandler(self, self.point2, is_3d=is_3d, fill_colour=(255, 0, 0, 255)) + self.handler_2.layer = 1 + + self.add_child(self.handler_1) + self.add_child(self.handler_2) + + @property + def interactive(self): + return self._interactive + + @interactive.setter + def interactive(self, value): + self._interactive = value + self.handler_1.visible = value + self.handler_2.visible = value + + def draw_to_canvas(self, gc, canvas): + if self.visible: + if self.is_3d: + cx, cy = self._3d_to_2d(canvas.evt_renderer, self.center) + p1x, p1y = self._3d_to_2d(canvas.evt_renderer, self.point1) + p2x, p2y = self._3d_to_2d(canvas.evt_renderer, self.point2) + else: + cx, cy = self.center + p1x, p1y = self.point1 + p2x, p2y = self.point2 + + width = abs(p1x - cx) * 2.0 + height = abs(p2y - cy) * 2.0 + + self.bbox = canvas.draw_ellipse((cx, cy), width, + height, self.width, + self.line_colour, + self.fill_colour) + # if self.interactive: + # self.handler_1.draw_to_canvas(gc, canvas) + # self.handler_2.draw_to_canvas(gc, canvas) + + def set_point1(self, pos): + self.point1 = pos + self.handler_1.position = pos + + def set_point2(self, pos): + self.point2 = pos + self.handler_2.position = pos + + def on_mouse_move(self, evt): + if evt.root_event_obj is self: + self.on_mouse_move2(evt) + else: + self.move_p1(evt) + self.move_p2(evt) + + def move_p1(self, evt): + pos = self.handler_1.position + if evt.viewer.orientation == 'AXIAL': + pos = pos[0], self.point1[1], self.point1[2] + elif evt.viewer.orientation == 'CORONAL': + pos = pos[0], self.point1[1], self.point1[2] + elif evt.viewer.orientation == 'SAGITAL': + pos = self.point1[0], pos[1], self.point1[2] + + self.set_point1(pos) + + if evt.control_down: + dist = np.linalg.norm(np.array(self.point1) - np.array(self.center)) + vec = np.array(self.point2) - np.array(self.center) + vec /= np.linalg.norm(vec) + point2 = np.array(self.center) + vec * dist + + self.set_point2(tuple(point2)) + + def move_p2(self, evt): + pos = self.handler_2.position + if evt.viewer.orientation == 'AXIAL': + pos = self.point2[0], pos[1], self.point2[2] + elif evt.viewer.orientation == 'CORONAL': + pos = self.point2[0], self.point2[1], pos[2] + elif evt.viewer.orientation == 'SAGITAL': + pos = self.point2[0], self.point2[1], pos[2] + + self.set_point2(pos) + + if evt.control_down: + dist = np.linalg.norm(np.array(self.point2) - np.array(self.center)) + vec = np.array(self.point1) - np.array(self.center) + vec /= np.linalg.norm(vec) + point1 = np.array(self.center) + vec * dist + + self.set_point1(tuple(point1)) + + def on_mouse_enter(self, evt): + # self.interactive = True + pass + + def on_mouse_leave(self, evt): + # self.interactive = False + pass + + def is_over(self, x, y): + xi, yi, xf, yf = self.bbox + if xi <= x <= xf and yi <= y <= yf: + return self + + def on_mouse_move2(self, evt): + mx, my = evt.position + if self.is_3d: + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + new_pos = (x, y, z) + else: + new_pos = mx, my + + diff = [i-j for i,j in zip(new_pos, self._last_position)] + + self.center = tuple((i+j for i,j in zip(diff, self.center))) + self.set_point1(tuple((i+j for i,j in zip(diff, self.point1)))) + self.set_point2(tuple((i+j for i,j in zip(diff, self.point2)))) + + self._last_position = new_pos + + return True + + def on_select(self, evt): + self.interactive = True + mx, my = evt.position + if self.is_3d: + x, y, z = evt.viewer.get_coordinate_cursor(mx, my) + self._last_position = (x, y, z) + else: + self._last_position = (mx, my) + + def on_deselect(self, evt): + self.interactive = False + return True diff --git a/invesalius/math_utils.py b/invesalius/math_utils.py index eddbb82..25ecd8c 100644 --- a/invesalius/math_utils.py +++ b/invesalius/math_utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import math -import numpy +import numpy as np def calculate_distance(p1, p2): """ @@ -15,20 +15,67 @@ def calculate_distance(p1, p2): """ return math.sqrt(sum([(j-i)**2 for i,j in zip(p1, p2)])) + def calculate_angle(v1, v2): """ Calculates the angle formed between vector v1 and v2. >>> calculate_angle((0, 1), (1, 0)) 90.0 - + >>> calculate_angle((1, 0), (0, 1)) 90.0 """ - cos_ = numpy.dot(v1, v2)/(numpy.linalg.norm(v1)*numpy.linalg.norm(v2)) + cos_ = np.dot(v1, v2)/(np.linalg.norm(v1)*np.linalg.norm(v2)) angle = math.degrees(math.acos(cos_)) return angle + +def calc_ellipse_area(a, b): + """ + Calculates the area of the ellipse with the given a and b radius. + + >>> area = calc_ellipse_area(3, 5) + >>> np.allclose(area, 47.1238) + True + + >>> area = calc_polygon_area(10, 10) + >>> np.allclose(area, 314.1592) + True + """ + return np.pi * a * b + + +def calc_polygon_area(points): + """ + Calculates the area from the polygon formed by given the points. + + >>> # Square + >>> calc_polygon_area([(0,0), (0,2), (2, 2), (2, 0)]) + 4.0 + + >>> # Triangle + >>> calc_polygon_area([(0, 0), (0, 9), (6, 0)]) + 27.0 + + >>> points = [(1.2*np.cos(i), 1.2*np.sin(i)) for i in np.linspace(0, 2.0*np.pi, 9)] + >>> area = calc_polygon_area(points) + >>> np.allclose(area, 4.0729) + True + + >>> points = [(108.73990145506055, 117.34406876547659), (71.04545419038097, 109.14962370793754), (71.04545419038097, 68.17739842024233), (83.33712177668953, 43.5940632476252), (139.05934816795505, 38.67739621310179), (152.9899047657714, 50.969063799410335), (143.9760152024784, 62.441286879965), (117.75379101835351, 62.441286879965), (127.58712508740032, 89.48295556984385), (154.62879377727918, 98.49684513313679)] + >>> area = calc_polygon_area(points) + >>> np.allclose(area, 4477.4906) + True + """ + area = 0.0 + j = len(points) - 1 + for i in range(len(points)): + area += (points[j][0]+points[i][0]) * (points[j][1]-points[i][1]) + j = i + area = abs(area / 2.0) + return area + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/invesalius/project.py b/invesalius/project.py index c50fe0d..14aaff7 100644 --- a/invesalius/project.py +++ b/invesalius/project.py @@ -192,17 +192,7 @@ class Project(with_metaclass(Singleton, object)): d = self.measurement_dict for i in d: m = d[i] - item = {} - item["index"] = m.index - item["name"] = m.name - item["colour"] = m.colour - item["value"] = m.value - item["location"] = m.location - item["type"] = m.type - item["slice_number"] = m.slice_number - item["points"] = m.points - item["visible"] = m.visible - measures[str(m.index)] = item + measures[str(m.index)] = m.get_as_dict() return measures def SavePlistProject(self, dir_, filename, compress=False): @@ -346,7 +336,10 @@ class Project(with_metaclass(Singleton, object)): measurements = plistlib.readPlist(os.path.join(dirpath, project["measurements"])) for index in measurements: - measure = ms.Measurement() + if measurements[index]["type"] in (const.DENSITY_ELLIPSE, const.DENSITY_POLYGON): + measure = ms.DensityMeasurement() + else: + measure = ms.Measurement() measure.Load(measurements[index]) self.measurement_dict[int(index)] = measure -- libgit2 0.21.2