Commit e21c88d2de6022b985f77287acb06efe0e64cbcb

Authored by Thiago Franco de Moraes
Committed by GitHub
1 parent 936e5ac8
Exists in master

Density tool canvas bublify (#171)

* Only painting mask if DEBUG_DENSITY is true
invesalius/constants.py
... ... @@ -105,6 +105,8 @@ SAGITAL_STR="SAGITAL"
105 105 # Measure type
106 106 LINEAR = 6
107 107 ANGULAR = 7
  108 +DENSITY_ELLIPSE = 8
  109 +DENSITY_POLYGON = 9
108 110  
109 111 # Colour representing each orientation
110 112 ORIENTATION_COLOUR = {'AXIAL': (1,0,0), # Red
... ... @@ -551,6 +553,8 @@ ID_WATERSHED_SEGMENTATION = wx.NewId()
551 553 ID_THRESHOLD_SEGMENTATION = wx.NewId()
552 554 ID_FLOODFILL_SEGMENTATION = wx.NewId()
553 555 ID_CROP_MASK = wx.NewId()
  556 +ID_DENSITY_MEASURE = wx.NewId()
  557 +ID_MASK_DENSITY_MEASURE = wx.NewId()
554 558 ID_CREATE_SURFACE = wx.NewId()
555 559 ID_CREATE_MASK = wx.NewId()
556 560  
... ... @@ -566,6 +570,9 @@ STATE_PAN = 1005
566 570 STATE_ANNOTATE = 1006
567 571 STATE_MEASURE_DISTANCE = 1007
568 572 STATE_MEASURE_ANGLE = 1008
  573 +STATE_MEASURE_DENSITY = 1009
  574 +STATE_MEASURE_DENSITY_ELLIPSE = 1010
  575 +STATE_MEASURE_DENSITY_POLYGON = 1011
569 576  
570 577 SLICE_STATE_CROSS = 3006
571 578 SLICE_STATE_SCROLL = 3007
... ... @@ -584,7 +591,11 @@ VOLUME_STATE_SEED = 2001
584 591  
585 592 TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM,
586 593 STATE_ZOOM_SL, STATE_PAN, STATE_MEASURE_DISTANCE,
587   - STATE_MEASURE_ANGLE] #, STATE_ANNOTATE]
  594 + STATE_MEASURE_ANGLE, STATE_MEASURE_DENSITY_ELLIPSE,
  595 + STATE_MEASURE_DENSITY_POLYGON,
  596 + ] #, STATE_ANNOTATE]
  597 +
  598 +
588 599  
589 600 TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL,
590 601 SLICE_STATE_REORIENT]
... ... @@ -599,6 +610,9 @@ SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS)
599 610 SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS)
600 611 SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION)
601 612 SLICE_STYLES.append(SLICE_STATE_CROP_MASK)
  613 +SLICE_STYLES.append(STATE_MEASURE_DENSITY)
  614 +SLICE_STYLES.append(STATE_MEASURE_DENSITY_ELLIPSE)
  615 +SLICE_STYLES.append(STATE_MEASURE_DENSITY_POLYGON)
602 616  
603 617 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
604 618 STATE_MEASURE_ANGLE]
... ... @@ -619,6 +633,9 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
619 633 STATE_DEFAULT: 0,
620 634 STATE_MEASURE_ANGLE: 2,
621 635 STATE_MEASURE_DISTANCE: 2,
  636 + STATE_MEASURE_DENSITY_ELLIPSE: 2,
  637 + STATE_MEASURE_DENSITY_POLYGON: 2,
  638 + STATE_MEASURE_DENSITY: 2,
622 639 STATE_WL: 2,
623 640 STATE_SPIN: 2,
624 641 STATE_ZOOM: 2,
... ...
invesalius/data/measures.py
... ... @@ -15,8 +15,15 @@ import invesalius.constants as const
15 15 import invesalius.project as prj
16 16 import invesalius.session as ses
17 17 import invesalius.utils as utils
  18 +
  19 +from invesalius import math_utils
  20 +from invesalius.gui.widgets.canvas_renderer import TextBox, CircleHandler, Ellipse, Polygon, CanvasHandlerBase
  21 +from scipy.misc import imsave
  22 +
18 23 TYPE = {const.LINEAR: _(u"Linear"),
19 24 const.ANGULAR: _(u"Angular"),
  25 + const.DENSITY_ELLIPSE: _(u"Density Ellipse"),
  26 + const.DENSITY_POLYGON: _(u"Density Polygon"),
20 27 }
21 28  
22 29 LOCATION = {const.SURFACE: _(u"3D"),
... ... @@ -47,6 +54,9 @@ else:
47 54 MEASURE_TEXT_COLOUR = (0, 0, 0)
48 55 MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255)
49 56  
  57 +
  58 +DEBUG_DENSITY = False
  59 +
50 60 class MeasureData(with_metaclass(utils.Singleton)):
51 61 """
52 62 Responsible to keep measures data.
... ... @@ -117,38 +127,63 @@ class MeasurementManager(object):
117 127 Publisher.subscribe(self._rm_incomplete_measurements,
118 128 "Remove incomplete measurements")
119 129 Publisher.subscribe(self._change_measure_point_pos, 'Change measurement point position')
  130 + Publisher.subscribe(self._add_density_measure, "Add density measurement")
120 131 Publisher.subscribe(self.OnCloseProject, 'Close project data')
121 132  
122 133 def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)):
123 134 for i in measurement_dict:
124 135 m = measurement_dict[i]
125 136  
126   - if m.location == const.AXIAL:
127   - radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE
  137 + if isinstance(m, DensityMeasurement):
  138 + if m.type == const.DENSITY_ELLIPSE:
  139 + mr = CircleDensityMeasure(map_id_locations[m.location],
  140 + m.slice_number,
  141 + m.colour)
  142 + mr.set_center(m.points[0])
  143 + mr.set_point1(m.points[1])
  144 + mr.set_point2(m.points[2])
  145 + elif m.type == const.DENSITY_POLYGON:
  146 + mr = PolygonDensityMeasure(map_id_locations[m.location],
  147 + m.slice_number,
  148 + m.colour)
  149 + for p in m.points:
  150 + mr.insert_point(p)
  151 + mr.complete_polygon()
  152 +
  153 + mr.set_density_values(m.min, m.max, m.mean, m.std, m.area)
  154 + print(m.min, m.max, m.mean, m.std)
  155 + mr._need_calc = False
  156 + self.measures.append((m, mr))
  157 + mr.set_measurement(m)
128 158  
129   - elif m.location == const.CORONAL:
130   - radius = min(spacing[0], spacing[1]) * const.PROP_MEASURE
  159 + else:
  160 + if m.location == const.AXIAL:
  161 + radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE
131 162  
132   - elif m.location == const.SAGITAL:
133   - radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE
  163 + elif m.location == const.CORONAL:
  164 + radius = min(spacing[0], spacing[1]) * const.PROP_MEASURE
134 165  
135   - else:
136   - radius = min(spacing) * const.PROP_MEASURE
  166 + elif m.location == const.SAGITAL:
  167 + radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE
137 168  
138   - representation = CirclePointRepresentation(m.colour, radius)
139   - if m.type == const.LINEAR:
140   - mr = LinearMeasure(m.colour, representation)
141   - else:
142   - mr = AngularMeasure(m.colour, representation)
143   - self.current = (m, mr)
144   - self.measures.append(self.current)
145   - for point in m.points:
146   - x, y, z = point
147   - actors = mr.AddPoint(x, y, z)
  169 + else:
  170 + radius = min(spacing) * const.PROP_MEASURE
  171 +
  172 + representation = CirclePointRepresentation(m.colour, radius)
  173 + if m.type == const.LINEAR:
  174 + mr = LinearMeasure(m.colour, representation)
  175 + else:
  176 + mr = AngularMeasure(m.colour, representation)
  177 + self.current = (m, mr)
  178 + self.measures.append(self.current)
  179 + for point in m.points:
  180 + x, y, z = point
  181 + actors = mr.AddPoint(x, y, z)
148 182  
149 183 if m.location == const.SURFACE:
150 184 Publisher.sendMessage(("Add actors " + str(m.location)),
151 185 actors=actors)
  186 +
152 187 self.current = None
153 188  
154 189 if not m.visible:
... ... @@ -315,6 +350,39 @@ class MeasurementManager(object):
315 350 # self.measures.pop()
316 351 self.current = None
317 352  
  353 + def _add_density_measure(self, density_measure):
  354 + m = DensityMeasurement()
  355 + m.index = len(self.measures)
  356 + m.location = density_measure.location
  357 + m.slice_number = density_measure.slice_number
  358 + m.colour = density_measure.colour
  359 + m.value = density_measure._mean
  360 + m.area = density_measure._area
  361 + m.mean = density_measure._mean
  362 + m.min = density_measure._min
  363 + m.max = density_measure._max
  364 + m.std = density_measure._std
  365 + if density_measure.format == 'ellipse':
  366 + m.points = [density_measure.center, density_measure.point1, density_measure.point2]
  367 + m.type = const.DENSITY_ELLIPSE
  368 + elif density_measure.format == 'polygon':
  369 + m.points = density_measure.points
  370 + m.type = const.DENSITY_POLYGON
  371 + density_measure.index = m.index
  372 +
  373 + density_measure.set_measurement(m)
  374 +
  375 + self.measures.append((m, density_measure))
  376 +
  377 + index = prj.Project().AddMeasurement(m)
  378 +
  379 + msg = 'Update measurement info in GUI',
  380 + Publisher.sendMessage(msg,
  381 + index=m.index, name=m.name, colour=m.colour,
  382 + location=density_measure.orientation,
  383 + type_='Density',
  384 + value='%.3f' % m.value)
  385 +
318 386 def OnCloseProject(self):
319 387 self.measures.clean()
320 388  
... ... @@ -344,6 +412,75 @@ class Measurement():
344 412 self.points = info["points"]
345 413 self.visible = info["visible"]
346 414  
  415 + def get_as_dict(self):
  416 + d = {
  417 + 'index': self.index,
  418 + 'name': self.name,
  419 + 'colour': self.colour,
  420 + 'value': self.value,
  421 + 'location': self.location,
  422 + 'type': self.type,
  423 + 'slice_number': self.slice_number,
  424 + 'points': self.points,
  425 + 'visible': self.visible,
  426 + }
  427 + return d
  428 +
  429 +
  430 +class DensityMeasurement():
  431 + general_index = -1
  432 + def __init__(self):
  433 + DensityMeasurement.general_index += 1
  434 + self.index = DensityMeasurement.general_index
  435 + self.name = const.MEASURE_NAME_PATTERN %(self.index+1)
  436 + self.colour = next(const.MEASURE_COLOUR)
  437 + self.area = 0
  438 + self.min = 0
  439 + self.max = 0
  440 + self.mean = 0
  441 + self.std = 0
  442 + self.location = const.AXIAL
  443 + self.type = const.DENSITY_ELLIPSE
  444 + self.slice_number = 0
  445 + self.points = []
  446 + self.visible = True
  447 +
  448 + def Load(self, info):
  449 + self.index = info["index"]
  450 + self.name = info["name"]
  451 + self.colour = info["colour"]
  452 + self.value = info["value"]
  453 + self.location = info["location"]
  454 + self.type = info["type"]
  455 + self.slice_number = info["slice_number"]
  456 + self.points = info["points"]
  457 + self.visible = info["visible"]
  458 + self.area = info['area']
  459 + self.min = info["min"]
  460 + self.max = info["max"]
  461 + self.mean = info["mean"]
  462 + self.std = info["std"]
  463 +
  464 + def get_as_dict(self):
  465 + d = {
  466 + 'index': self.index,
  467 + 'name': self.name,
  468 + 'colour': self.colour,
  469 + 'value': self.value,
  470 + 'location': self.location,
  471 + 'type': self.type,
  472 + 'slice_number': self.slice_number,
  473 + 'points': self.points,
  474 + 'visible': self.visible,
  475 + 'area': self.area,
  476 + 'min': self.min,
  477 + 'max': self.max,
  478 + 'mean': self.mean,
  479 + 'std': self.std,
  480 + }
  481 + return d
  482 +
  483 +
347 484 class CirclePointRepresentation(object):
348 485 """
349 486 This class represents a circle that indicate a point in the surface
... ... @@ -452,6 +589,7 @@ class LinearMeasure(object):
452 589 self.line_actor = None
453 590 self.text_actor = None
454 591 self.renderer = None
  592 + self.layer = 0
455 593 if not representation:
456 594 representation = CirclePointRepresentation(colour)
457 595 self.representation = representation
... ... @@ -632,6 +770,7 @@ class AngularMeasure(object):
632 770 self.point_actor3 = None
633 771 self.line_actor = None
634 772 self.text_actor = None
  773 + self.layer = 0
635 774 if not representation:
636 775 representation = CirclePointRepresentation(colour)
637 776 self.representation = representation
... ... @@ -799,7 +938,6 @@ class AngularMeasure(object):
799 938 for p in self.points:
800 939 coord.SetValue(p)
801 940 cx, cy = coord.GetComputedDoubleDisplayValue(canvas.evt_renderer)
802   - print(cx, cy)
803 941 # canvas.draw_circle((cx, cy), 2.5)
804 942 points.append((cx, cy))
805 943  
... ... @@ -811,8 +949,11 @@ class AngularMeasure(object):
811 949 if len(points) == 3:
812 950 txt = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue())
813 951 r, g, b = self.colour
814   - canvas.draw_arc(points[1], points[0], points[2], line_colour=(r*255, g*255, b*255, 255))
815   - canvas.draw_text_box(txt, (points[1][0], points[1][1]), txt_colour=MEASURE_TEXT_COLOUR, bg_colour=MEASURE_TEXTBOX_COLOUR)
  952 + canvas.draw_arc(points[1], points[0], points[2],
  953 + line_colour=(int(r*255), int(g*255), int(b*255), 255))
  954 + canvas.draw_text_box(txt, (points[1][0], points[1][1]),
  955 + txt_colour=MEASURE_TEXT_COLOUR,
  956 + bg_colour=MEASURE_TEXTBOX_COLOUR)
816 957  
817 958 def GetNumberOfPoints(self):
818 959 return self.number_of_points
... ... @@ -894,3 +1035,539 @@ class AngularMeasure(object):
894 1035  
895 1036 def __del__(self):
896 1037 self.Remove()
  1038 +
  1039 +
  1040 +class CircleDensityMeasure(CanvasHandlerBase):
  1041 + def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True):
  1042 + super(CircleDensityMeasure, self).__init__(None)
  1043 + self.parent = None
  1044 + self.children = []
  1045 + self.layer = 0
  1046 +
  1047 + self.colour = colour
  1048 + self.center = (0.0, 0.0, 0.0)
  1049 + self.point1 = (0.0, 0.0, 0.0)
  1050 + self.point2 = (0.0, 0.0, 0.0)
  1051 +
  1052 + self.orientation = orientation
  1053 + self.slice_number = slice_number
  1054 +
  1055 + self.format = 'ellipse'
  1056 +
  1057 + self.location = map_locations_id[self.orientation]
  1058 + self.index = 0
  1059 +
  1060 + self._area = 0
  1061 + self._min = 0
  1062 + self._max = 0
  1063 + self._mean = 0
  1064 + self._std = 0
  1065 +
  1066 + self._measurement = None
  1067 +
  1068 + self.ellipse = Ellipse(self, self.center, self.point1, self.point2,
  1069 + fill=False, line_colour=self.colour)
  1070 + self.ellipse.layer = 1
  1071 + self.add_child(self.ellipse)
  1072 + self.text_box = None
  1073 +
  1074 + self._need_calc = True
  1075 + self.interactive = interactive
  1076 +
  1077 + def set_center(self, pos):
  1078 + self.center = pos
  1079 + self._need_calc = True
  1080 + self.ellipse.center = self.center
  1081 +
  1082 + if self._measurement:
  1083 + self._measurement.points = [self.center, self.point1, self.point2]
  1084 +
  1085 + def set_point1(self, pos):
  1086 + self.point1 = pos
  1087 + self._need_calc = True
  1088 + self.ellipse.set_point1(self.point1)
  1089 +
  1090 + if self._measurement:
  1091 + self._measurement.points = [self.center, self.point1, self.point2]
  1092 +
  1093 + def set_point2(self, pos):
  1094 + self.point2 = pos
  1095 + self._need_calc = True
  1096 + self.ellipse.set_point2(self.point2)
  1097 +
  1098 + if self._measurement:
  1099 + self._measurement.points = [self.center, self.point1, self.point2]
  1100 +
  1101 + def set_density_values(self, _min, _max, _mean, _std, _area):
  1102 + self._min = _min
  1103 + self._max = _max
  1104 + self._mean = _mean
  1105 + self._std = _std
  1106 + self._area = _area
  1107 +
  1108 + text = _('Area: %.3f\n'
  1109 + 'Min: %.3f\n'
  1110 + 'Max: %.3f\n'
  1111 + 'Mean: %.3f\n'
  1112 + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std))
  1113 +
  1114 + if self.text_box is None:
  1115 + self.text_box = TextBox(self, text, self.point1, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR)
  1116 + self.text_box.layer = 2
  1117 + self.add_child(self.text_box)
  1118 + else:
  1119 + self.text_box.set_text(text)
  1120 +
  1121 + if self._measurement:
  1122 + self._measurement.value = self._mean
  1123 + self._update_gui_info()
  1124 +
  1125 + def _update_gui_info(self):
  1126 + msg = 'Update measurement info in GUI',
  1127 + print(msg)
  1128 + if self._measurement:
  1129 + m = self._measurement
  1130 + Publisher.sendMessage(msg,
  1131 + index=m.index, name=m.name, colour=m.colour,
  1132 + location= self.orientation,
  1133 + type_=_('Density Ellipse'),
  1134 + value='%.3f' % m.value)
  1135 +
  1136 + def set_measurement(self, dm):
  1137 + self._measurement = dm
  1138 +
  1139 + def SetVisibility(self, value):
  1140 + self.visible = value
  1141 + self.ellipse.visible = value
  1142 +
  1143 + def _3d_to_2d(self, renderer, pos):
  1144 + coord = vtk.vtkCoordinate()
  1145 + coord.SetValue(pos)
  1146 + cx, cy = coord.GetComputedDoubleDisplayValue(renderer)
  1147 + return cx, cy
  1148 +
  1149 + def is_over(self, x, y):
  1150 + return None
  1151 + # if self.interactive:
  1152 + # if self.ellipse.is_over(x, y):
  1153 + # return self.ellipse.is_over(x, y)
  1154 + # elif self.text_box.is_over(x, y):
  1155 + # return self.text_box.is_over(x, y)
  1156 + # return None
  1157 +
  1158 + def set_interactive(self, value):
  1159 + self.interactive = bool(value)
  1160 + self.ellipse.interactive = self.interactive
  1161 +
  1162 + def draw_to_canvas(self, gc, canvas):
  1163 + """
  1164 + Draws to an wx.GraphicsContext.
  1165 +
  1166 + Parameters:
  1167 + gc: is a wx.GraphicsContext
  1168 + canvas: the canvas it's being drawn.
  1169 + """
  1170 + # cx, cy = self._3d_to_2d(canvas.evt_renderer, self.center)
  1171 + # px, py = self._3d_to_2d(canvas.evt_renderer, self.point1)
  1172 + # radius = ((px - cx)**2 + (py - cy)**2)**0.5
  1173 + if self._need_calc:
  1174 + self._need_calc = False
  1175 + self.calc_density()
  1176 +
  1177 + # canvas.draw_circle((cx, cy), radius, line_colour=self.colour)
  1178 + # self.ellipse.draw_to_canvas(gc, canvas)
  1179 +
  1180 + # # canvas.draw_text_box(text, (px, py), )
  1181 + # self.text_box.draw_to_canvas(gc, canvas)
  1182 + # # self.handle_tl.draw_to_canvas(gc, canvas)
  1183 +
  1184 + def calc_area(self):
  1185 + if self.orientation == 'AXIAL':
  1186 + a = abs(self.point1[0] - self.center[0])
  1187 + b = abs(self.point2[1] - self.center[1])
  1188 +
  1189 + elif self.orientation == 'CORONAL':
  1190 + a = abs(self.point1[0] - self.center[0])
  1191 + b = abs(self.point2[2] - self.center[2])
  1192 +
  1193 + elif self.orientation == 'SAGITAL':
  1194 + a = abs(self.point1[1] - self.center[1])
  1195 + b = abs(self.point2[2] - self.center[2])
  1196 +
  1197 + return math_utils.calc_ellipse_area(a, b)
  1198 +
  1199 + def calc_density(self):
  1200 + from invesalius.data.slice_ import Slice
  1201 + slc = Slice()
  1202 + n = self.slice_number
  1203 + orientation = self.orientation
  1204 + img_slice = slc.get_image_slice(orientation, n)
  1205 + dy, dx = img_slice.shape
  1206 + spacing = slc.spacing
  1207 +
  1208 + if orientation == 'AXIAL':
  1209 + sx, sy = spacing[0], spacing[1]
  1210 + cx, cy = self.center[0], self.center[1]
  1211 +
  1212 + a = abs(self.point1[0] - self.center[0])
  1213 + b = abs(self.point2[1] - self.center[1])
  1214 +
  1215 + n = slc.buffer_slices["AXIAL"].index + 1
  1216 + m = slc.current_mask.matrix[n, 1:, 1:]
  1217 +
  1218 + elif orientation == 'CORONAL':
  1219 + sx, sy = spacing[0], spacing[2]
  1220 + cx, cy = self.center[0], self.center[2]
  1221 +
  1222 + a = abs(self.point1[0] - self.center[0])
  1223 + b = abs(self.point2[2] - self.center[2])
  1224 +
  1225 + n = slc.buffer_slices["CORONAL"].index + 1
  1226 + m = slc.current_mask.matrix[1:, n, 1:]
  1227 +
  1228 + elif orientation == 'SAGITAL':
  1229 + sx, sy = spacing[1], spacing[2]
  1230 + cx, cy = self.center[1], self.center[2]
  1231 +
  1232 + a = abs(self.point1[1] - self.center[1])
  1233 + b = abs(self.point2[2] - self.center[2])
  1234 +
  1235 + n = slc.buffer_slices["SAGITAL"].index + 1
  1236 + m = slc.current_mask.matrix[1:, 1:, n]
  1237 +
  1238 + # a = np.linalg.norm(np.array(self.point1) - np.array(self.center))
  1239 + # b = np.linalg.norm(np.array(self.point2) - np.array(self.center))
  1240 +
  1241 + mask_y, mask_x = np.ogrid[0:dy*sy:sy, 0:dx*sx:sx]
  1242 + # mask = ((mask_x - cx)**2 + (mask_y - cy)**2) <= (radius ** 2)
  1243 + mask = (((mask_x-cx)**2 / a**2) + ((mask_y-cy)**2 / b**2)) <= 1.0
  1244 +
  1245 + # try:
  1246 + # test_img = np.zeros_like(img_slice)
  1247 + # test_img[mask] = img_slice[mask]
  1248 + # imsave('/tmp/manolo.png', test_img[::-1,:])
  1249 + if DEBUG_DENSITY:
  1250 + try:
  1251 + m[:] = 0
  1252 + m[mask] = 254
  1253 + slc.buffer_slices[self.orientation].discard_vtk_mask()
  1254 + slc.buffer_slices[self.orientation].discard_mask()
  1255 + Publisher.sendMessage('Reload actual slice')
  1256 + except IndexError:
  1257 + pass
  1258 +
  1259 + values = img_slice[mask]
  1260 +
  1261 + try:
  1262 + _min = values.min()
  1263 + _max = values.max()
  1264 + _mean = values.mean()
  1265 + _std = values.std()
  1266 + except ValueError:
  1267 + _min = 0
  1268 + _max = 0
  1269 + _mean = 0
  1270 + _std = 0
  1271 +
  1272 + _area = self.calc_area()
  1273 +
  1274 + if self._measurement:
  1275 + self._measurement.points = [self.center, self.point1, self.point2]
  1276 + self._measurement.value = float(_mean)
  1277 + self._measurement.mean = float(_mean)
  1278 + self._measurement.min = float(_min)
  1279 + self._measurement.max = float(_max)
  1280 + self._measurement.std = float(_std)
  1281 + self._measurement.area = float(_area)
  1282 +
  1283 + self.set_density_values(_min, _max, _mean, _std, _area)
  1284 +
  1285 + def IsComplete(self):
  1286 + return True
  1287 +
  1288 + def on_mouse_move(self, evt):
  1289 + old_center = self.center
  1290 + self.center = self.ellipse.center
  1291 + self.set_point1(self.ellipse.point1)
  1292 + self.set_point2(self.ellipse.point2)
  1293 +
  1294 + diff = tuple((i-j for i,j in zip(self.center, old_center)))
  1295 + self.text_box.position = tuple((i+j for i,j in zip(self.text_box.position, diff)))
  1296 +
  1297 + if self._measurement:
  1298 + self._measurement.points = [self.center, self.point1, self.point2]
  1299 + self._measurement.value = self._mean
  1300 + self._measurement.mean = self._mean
  1301 + self._measurement.min = self._min
  1302 + self._measurement.max = self._max
  1303 + self._measurement.std = self._std
  1304 +
  1305 + session = ses.Session()
  1306 + session.ChangeProject()
  1307 +
  1308 + def on_select(self, evt):
  1309 + self.layer = 50
  1310 +
  1311 + def on_deselect(self, evt):
  1312 + self.layer = 0
  1313 +
  1314 +
  1315 +class PolygonDensityMeasure(CanvasHandlerBase):
  1316 + def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True):
  1317 + super(PolygonDensityMeasure, self).__init__(None)
  1318 + self.parent = None
  1319 + self.children = []
  1320 + self.layer = 0
  1321 +
  1322 + self.colour = colour
  1323 + self.points = []
  1324 +
  1325 + self.orientation = orientation
  1326 + self.slice_number = slice_number
  1327 +
  1328 + self.complete = False
  1329 +
  1330 + self.format = 'polygon'
  1331 +
  1332 + self.location = map_locations_id[self.orientation]
  1333 + self.index = 0
  1334 +
  1335 + self._area = 0
  1336 + self._min = 0
  1337 + self._max = 0
  1338 + self._mean = 0
  1339 + self._std = 0
  1340 +
  1341 + self._dist_tbox = (0, 0, 0)
  1342 +
  1343 + self._measurement = None
  1344 +
  1345 + self.polygon = Polygon(self, fill=False, closed=False, line_colour=self.colour)
  1346 + self.polygon.layer = 1
  1347 + self.add_child(self.polygon)
  1348 +
  1349 + self.text_box = None
  1350 +
  1351 + self._need_calc = False
  1352 + self.interactive = interactive
  1353 +
  1354 +
  1355 + def on_mouse_move(self, evt):
  1356 + self.points = self.polygon.points
  1357 + self._need_calc = self.complete
  1358 +
  1359 + if self._measurement:
  1360 + self._measurement.points = self.points
  1361 +
  1362 + if self.text_box:
  1363 + bounds = self.get_bounds()
  1364 + p = [bounds[3], bounds[4], bounds[5]]
  1365 + if evt.root_event_obj is self.text_box:
  1366 + self._dist_tbox = [i-j for i,j in zip(self.text_box.position, p)]
  1367 + else:
  1368 + self.text_box.position = [i+j for i,j in zip(self._dist_tbox, p)]
  1369 + print("text box position", self.text_box.position)
  1370 +
  1371 + session = ses.Session()
  1372 + session.ChangeProject()
  1373 +
  1374 + def draw_to_canvas(self, gc, canvas):
  1375 + if self._need_calc:
  1376 + self.calc_density(canvas)
  1377 + # if self.visible:
  1378 + # self.polygon.draw_to_canvas(gc, canvas)
  1379 + # if self._need_calc:
  1380 + # self.calc_density(canvas)
  1381 + # if self.text_box:
  1382 + # bounds = self.get_bounds()
  1383 + # p = [bounds[3], bounds[4], bounds[5]]
  1384 + # self.text_box.draw_to_canvas(gc, canvas)
  1385 + # self._dist_tbox = [j-i for i,j in zip(p, self.text_box.position)]
  1386 +
  1387 + def insert_point(self, point):
  1388 + print("insert points", len(self.points))
  1389 + self.polygon.append_point(point)
  1390 + self.points.append(point)
  1391 +
  1392 + def complete_polygon(self):
  1393 + # if len(self.points) >= 3:
  1394 + self.polygon.closed = True
  1395 + self._need_calc = True
  1396 + self.complete = True
  1397 +
  1398 + bounds = self.get_bounds()
  1399 + p = [bounds[3], bounds[4], bounds[5]]
  1400 + if self.text_box is None:
  1401 + p[0] += 5
  1402 + self.text_box = TextBox(self, '', p, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR)
  1403 + self.text_box.layer = 2
  1404 + self.add_child(self.text_box)
  1405 +
  1406 + def calc_density(self, canvas):
  1407 + from invesalius.data.slice_ import Slice
  1408 +
  1409 + slc = Slice()
  1410 + n = self.slice_number
  1411 + orientation = self.orientation
  1412 + img_slice = slc.get_image_slice(orientation, n)
  1413 + dy, dx = img_slice.shape
  1414 + spacing = slc.spacing
  1415 +
  1416 + if orientation == 'AXIAL':
  1417 + sx, sy = spacing[0], spacing[1]
  1418 + n = slc.buffer_slices["AXIAL"].index + 1
  1419 + m = slc.current_mask.matrix[n, 1:, 1:]
  1420 + plg_points = [(x/sx, y/sy) for (x, y, z) in self.points]
  1421 +
  1422 + elif orientation == 'CORONAL':
  1423 + sx, sy = spacing[0], spacing[2]
  1424 + n = slc.buffer_slices["CORONAL"].index + 1
  1425 + m = slc.current_mask.matrix[1:, n, 1:]
  1426 + plg_points = [(x/sx, z/sy) for (x, y, z) in self.points]
  1427 +
  1428 + elif orientation == 'SAGITAL':
  1429 + sx, sy = spacing[1], spacing[2]
  1430 + n = slc.buffer_slices["SAGITAL"].index + 1
  1431 + m = slc.current_mask.matrix[1:, 1:, n]
  1432 +
  1433 + plg_points = [(y/sx, z/sy) for (x, y, z) in self.points]
  1434 +
  1435 + plg_tmp = Polygon(None, plg_points, fill=True,
  1436 + line_colour=(0, 0, 0, 0),
  1437 + fill_colour=(255, 255, 255, 255), width=1,
  1438 + interactive=False, is_3d=False)
  1439 + h, w = img_slice.shape
  1440 + arr = canvas.draw_element_to_array([plg_tmp, ], size=(w, h), flip=False)
  1441 + mask = arr[:, :, 0] >= 128
  1442 +
  1443 + print("mask sum", mask.sum())
  1444 +
  1445 + if DEBUG_DENSITY:
  1446 + try:
  1447 + m[:] = 0
  1448 + m[mask] = 254
  1449 + slc.buffer_slices[self.orientation].discard_vtk_mask()
  1450 + slc.buffer_slices[self.orientation].discard_mask()
  1451 + Publisher.sendMessage('Reload actual slice')
  1452 + except IndexError:
  1453 + pass
  1454 +
  1455 + values = img_slice[mask]
  1456 +
  1457 + try:
  1458 + _min = values.min()
  1459 + _max = values.max()
  1460 + _mean = values.mean()
  1461 + _std = values.std()
  1462 + except ValueError:
  1463 + _min = 0
  1464 + _max = 0
  1465 + _mean = 0
  1466 + _std = 0
  1467 +
  1468 + _area = self.calc_area()
  1469 +
  1470 + if self._measurement:
  1471 + self._measurement.points = self.points
  1472 + self._measurement.value = float(_mean)
  1473 + self._measurement.mean = float(_mean)
  1474 + self._measurement.min = float(_min)
  1475 + self._measurement.max = float(_max)
  1476 + self._measurement.std = float(_std)
  1477 + self._measurement.area = float(_area)
  1478 +
  1479 + self.set_density_values(_min, _max, _mean, _std, _area)
  1480 + self.calc_area()
  1481 +
  1482 + self._need_calc = False
  1483 +
  1484 + def calc_area(self):
  1485 + if self.orientation == 'AXIAL':
  1486 + points = [(x, y) for (x, y, z) in self.points]
  1487 + elif self.orientation == 'CORONAL':
  1488 + points = [(x, z) for (x, y, z) in self.points]
  1489 + elif self.orientation == 'SAGITAL':
  1490 + points = [(y, z) for (x, y, z) in self.points]
  1491 + area = math_utils.calc_polygon_area(points)
  1492 + print('Points', points)
  1493 + print('xv = %s;' % [i[0] for i in points])
  1494 + print('yv = %s;' % [i[1] for i in points])
  1495 + print('Area', area)
  1496 + return area
  1497 +
  1498 + def get_bounds(self):
  1499 + min_x = min(self.points, key=lambda x: x[0])[0]
  1500 + max_x = max(self.points, key=lambda x: x[0])[0]
  1501 +
  1502 + min_y = min(self.points, key=lambda x: x[1])[1]
  1503 + max_y = max(self.points, key=lambda x: x[1])[1]
  1504 +
  1505 + min_z = min(self.points, key=lambda x: x[2])[2]
  1506 + max_z = max(self.points, key=lambda x: x[2])[2]
  1507 +
  1508 + print(self.points)
  1509 +
  1510 + return (min_x, min_y, min_z, max_x, max_y, max_z)
  1511 +
  1512 + def IsComplete(self):
  1513 + return self.complete
  1514 +
  1515 + def set_measurement(self, dm):
  1516 + self._measurement = dm
  1517 +
  1518 + def SetVisibility(self, value):
  1519 + self.visible = value
  1520 + self.polygon.visible = value
  1521 +
  1522 + def set_interactive(self, value):
  1523 + self.interactive = bool(value)
  1524 + self.polygon.interactive = self.interactive
  1525 +
  1526 + def is_over(self, x, y):
  1527 + None
  1528 + # if self.interactive:
  1529 + # if self.polygon.is_over(x, y):
  1530 + # return self.polygon.is_over(x, y)
  1531 + # if self.text_box is not None:
  1532 + # if self.text_box.is_over(x, y):
  1533 + # return self.text_box.is_over(x, y)
  1534 + # return None
  1535 +
  1536 + def set_density_values(self, _min, _max, _mean, _std, _area):
  1537 + self._min = _min
  1538 + self._max = _max
  1539 + self._mean = _mean
  1540 + self._std = _std
  1541 + self._area = _area
  1542 +
  1543 + text = _('Area: %.3f\n'
  1544 + 'Min: %.3f\n'
  1545 + 'Max: %.3f\n'
  1546 + 'Mean: %.3f\n'
  1547 + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std))
  1548 +
  1549 + bounds = self.get_bounds()
  1550 + p = [bounds[3], bounds[4], bounds[5]]
  1551 +
  1552 + dx = self.text_box.position[0] - p[0]
  1553 + dy = self.text_box.position[1] - p[1]
  1554 + p[0] += dx
  1555 + p[1] += dy
  1556 + self.text_box.set_text(text)
  1557 + self.text_box.position = p
  1558 +
  1559 + if self._measurement:
  1560 + self._measurement.value = self._mean
  1561 + self._update_gui_info()
  1562 +
  1563 + def _update_gui_info(self):
  1564 + msg = 'Update measurement info in GUI',
  1565 + print(msg)
  1566 + if self._measurement:
  1567 + m = self._measurement
  1568 + Publisher.sendMessage(msg,
  1569 + index=m.index, name=m.name,
  1570 + colour=m.colour,
  1571 + location=self.orientation,
  1572 + type_=_('Density Polygon'),
  1573 + value='%.3f' % m.value)
... ...
invesalius/data/slice_.py
... ... @@ -23,6 +23,8 @@ import tempfile
23 23  
24 24 import numpy as np
25 25 import vtk
  26 +
  27 +from scipy import ndimage
26 28 from wx.lib.pubsub import pub as Publisher
27 29  
28 30 import invesalius.constants as const
... ... @@ -1540,3 +1542,61 @@ class Slice(with_metaclass(utils.Singleton, object)):
1540 1542 self.buffer_slices['SAGITAL'].discard_vtk_mask()
1541 1543  
1542 1544 Publisher.sendMessage('Reload actual slice')
  1545 +
  1546 + def calc_image_density(self, mask=None):
  1547 + if mask is None:
  1548 + mask = self.current_mask
  1549 + self.do_threshold_to_all_slices(mask)
  1550 + values = self.matrix[mask.matrix[1:, 1:, 1:] > 127]
  1551 +
  1552 + if len(values):
  1553 + _min = values.min()
  1554 + _max = values.max()
  1555 + _mean = values.mean()
  1556 + _std = values.std()
  1557 + return _min, _max, _mean, _std
  1558 + else:
  1559 + return 0, 0, 0, 0
  1560 +
  1561 + def calc_mask_area(self, mask=None):
  1562 + if mask is None:
  1563 + mask = self.current_mask
  1564 +
  1565 + self.do_threshold_to_all_slices(mask)
  1566 + bin_img = (mask.matrix[1:, 1:, 1:] > 127)
  1567 +
  1568 + sx, sy, sz = self.spacing
  1569 +
  1570 + kernel = np.zeros((3, 3, 3))
  1571 + kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz
  1572 + kernel[0, 1, 1] = - (sx * sy)
  1573 + kernel[2, 1, 1] = - (sx * sy)
  1574 +
  1575 + kernel[1, 0, 1] = - (sx * sz)
  1576 + kernel[1, 2, 1] = - (sx * sz)
  1577 +
  1578 + kernel[1, 1, 0] = - (sy * sz)
  1579 + kernel[1, 1, 2] = - (sy * sz)
  1580 +
  1581 + # area = ndimage.generic_filter(bin_img * 1.0, _conv_area, size=(3, 3, 3), mode='constant', cval=1, extra_arguments=(sx, sy, sz)).sum()
  1582 + area = transforms.convolve_non_zero(bin_img * 1.0, kernel, 1).sum()
  1583 +
  1584 + return area
  1585 +
  1586 +def _conv_area(x, sx, sy, sz):
  1587 + x = x.reshape((3, 3, 3))
  1588 + if x[1, 1, 1]:
  1589 + kernel = np.zeros((3, 3, 3))
  1590 + kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz
  1591 + kernel[0, 1, 1] = -(sx * sy)
  1592 + kernel[2, 1, 1] = -(sx * sy)
  1593 +
  1594 + kernel[1, 0, 1] = -(sx * sz)
  1595 + kernel[1, 2, 1] = -(sx * sz)
  1596 +
  1597 + kernel[1, 1, 0] = -(sy * sz)
  1598 + kernel[1, 1, 2] = -(sy * sz)
  1599 +
  1600 + return (x * kernel).sum()
  1601 + else:
  1602 + return 0
... ...
invesalius/data/styles.py
... ... @@ -45,7 +45,7 @@ from scipy.ndimage import watershed_ift, generate_binary_structure
45 45 from skimage.morphology import watershed
46 46  
47 47 import invesalius.gui.dialogs as dialogs
48   -from invesalius.data.measures import MeasureData
  48 +from invesalius.data.measures import MeasureData, CircleDensityMeasure, PolygonDensityMeasure
49 49  
50 50 from . import floodfill
51 51  
... ... @@ -140,6 +140,7 @@ class DefaultInteractorStyle(BaseImageInteractorStyle):
140 140  
141 141 # Zoom using right button
142 142 self.AddObserver("RightButtonPressEvent",self.OnZoomRightClick)
  143 + self.AddObserver("RightButtonReleaseEvent",self.OnZoomRightRelease)
143 144 self.AddObserver("MouseMoveEvent", self.OnZoomRightMove)
144 145  
145 146 self.AddObserver("MouseWheelForwardEvent",self.OnScrollForward)
... ... @@ -161,6 +162,12 @@ class DefaultInteractorStyle(BaseImageInteractorStyle):
161 162 def OnZoomRightClick(self, evt, obj):
162 163 evt.StartDolly()
163 164  
  165 + def OnZoomRightRelease(self, evt, obj):
  166 + print('EndDolly')
  167 + evt.OnRightButtonUp()
  168 + # evt.EndDolly()
  169 + self.right_pressed = False
  170 +
164 171 def OnScrollForward(self, evt, obj):
165 172 iren = self.viewer.interactor
166 173 viewer = self.viewer
... ... @@ -527,6 +534,150 @@ class AngularMeasureInteractorStyle(LinearMeasureInteractorStyle):
527 534 self.state_code = const.STATE_MEASURE_ANGLE
528 535  
529 536  
  537 +class DensityMeasureStyle(DefaultInteractorStyle):
  538 + """
  539 + Interactor style responsible for density measurements.
  540 + """
  541 + def __init__(self, viewer):
  542 + DefaultInteractorStyle.__init__(self, viewer)
  543 +
  544 + self.state_code = const.STATE_MEASURE_DENSITY
  545 +
  546 + self.format = 'polygon'
  547 +
  548 + self._last_measure = None
  549 +
  550 + self.viewer = viewer
  551 + self.orientation = viewer.orientation
  552 + self.slice_data = viewer.slice_data
  553 +
  554 + self.picker = vtk.vtkCellPicker()
  555 + self.picker.PickFromListOn()
  556 +
  557 + self.measures = MeasureData()
  558 +
  559 + self._bind_events()
  560 +
  561 + def _bind_events(self):
  562 + # self.AddObserver("LeftButtonPressEvent", self.OnInsertPoint)
  563 + # self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseMeasurePoint)
  564 + # self.AddObserver("MouseMoveEvent", self.OnMoveMeasurePoint)
  565 + # self.AddObserver("LeaveEvent", self.OnLeaveMeasureInteractor)
  566 + self.viewer.canvas.subscribe_event('LeftButtonPressEvent', self.OnInsertPoint)
  567 + self.viewer.canvas.subscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon)
  568 +
  569 + def SetUp(self):
  570 + for n in self.viewer.draw_by_slice_number:
  571 + for i in self.viewer.draw_by_slice_number[n]:
  572 + if isinstance(i, PolygonDensityMeasure):
  573 + i.set_interactive(True)
  574 + self.viewer.canvas.Refresh()
  575 +
  576 + def CleanUp(self):
  577 + self.viewer.canvas.unsubscribe_event('LeftButtonPressEvent', self.OnInsertPoint)
  578 + self.viewer.canvas.unsubscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon)
  579 + old_list = self.viewer.draw_by_slice_number
  580 + self.viewer.draw_by_slice_number.clear()
  581 + for n in old_list:
  582 + for i in old_list[n]:
  583 + if isinstance(i, PolygonDensityMeasure):
  584 + if i.complete:
  585 + self.viewer.draw_by_slice_number[n].append(i)
  586 + else:
  587 + self.viewer.draw_by_slice_number[n].append(i)
  588 +
  589 + self.viewer.UpdateCanvas()
  590 +
  591 + def _2d_to_3d(self, pos):
  592 + mx, my = pos
  593 + iren = self.viewer.interactor
  594 + render = iren.FindPokedRenderer(mx, my)
  595 + self.picker.AddPickList(self.slice_data.actor)
  596 + self.picker.Pick(mx, my, 0, render)
  597 + x, y, z = self.picker.GetPickPosition()
  598 + self.picker.DeletePickList(self.slice_data.actor)
  599 + return (x, y, z)
  600 +
  601 + def _pick_position(self):
  602 + iren = self.viewer.interactor
  603 + mx, my = iren.GetEventPosition()
  604 + return (mx, my)
  605 +
  606 + def _get_pos_clicked(self):
  607 + mouse_x, mouse_y = self._pick_position()
  608 + position = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker)
  609 + return position
  610 +
  611 + def OnInsertPoint(self, evt):
  612 + mouse_x, mouse_y = evt.position
  613 + print('OnInsertPoint', evt.position)
  614 + n = self.viewer.slice_data.number
  615 + pos = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker)
  616 +
  617 + if self.format == 'ellipse':
  618 + pp1 = self.viewer.get_coordinate_cursor(mouse_x+50, mouse_y, self.picker)
  619 + pp2 = self.viewer.get_coordinate_cursor(mouse_x, mouse_y+50, self.picker)
  620 +
  621 + m = CircleDensityMeasure(self.orientation, n)
  622 + m.set_center(pos)
  623 + m.set_point1(pp1)
  624 + m.set_point2(pp2)
  625 + m.calc_density()
  626 + _new_measure = True
  627 + Publisher.sendMessage("Add density measurement", density_measure=m)
  628 + elif self.format == 'polygon':
  629 + if self._last_measure is None:
  630 + m = PolygonDensityMeasure(self.orientation, n)
  631 + _new_measure = True
  632 + else:
  633 + m = self._last_measure
  634 + _new_measure = False
  635 + if m.slice_number != n:
  636 + self.viewer.draw_by_slice_number[m.slice_number].remove(m)
  637 + del m
  638 + m = PolygonDensityMeasure(self.orientation, n)
  639 + _new_measure = True
  640 +
  641 + m.insert_point(pos)
  642 +
  643 + if _new_measure:
  644 + self.viewer.draw_by_slice_number[n].append(m)
  645 +
  646 + if self._last_measure:
  647 + self._last_measure.set_interactive(False)
  648 +
  649 + self._last_measure = m
  650 + # m.calc_density()
  651 +
  652 + self.viewer.UpdateCanvas()
  653 +
  654 + def OnInsertPolygon(self, evt):
  655 + if self.format == 'polygon' and self._last_measure:
  656 + m = self._last_measure
  657 + if len(m.points) >= 3:
  658 + n = self.viewer.slice_data.number
  659 + print(self.viewer.draw_by_slice_number[n], m)
  660 + self.viewer.draw_by_slice_number[n].remove(m)
  661 + m.complete_polygon()
  662 + self._last_measure = None
  663 + Publisher.sendMessage("Add density measurement", density_measure=m)
  664 + self.viewer.UpdateCanvas()
  665 +
  666 +
  667 +class DensityMeasureEllipseStyle(DensityMeasureStyle):
  668 + def __init__(self, viewer):
  669 + DensityMeasureStyle.__init__(self, viewer)
  670 + self.state_code = const.STATE_MEASURE_DENSITY_ELLIPSE
  671 + self.format = 'ellipse'
  672 +
  673 +
  674 +class DensityMeasurePolygonStyle(DensityMeasureStyle):
  675 + def __init__(self, viewer):
  676 + DensityMeasureStyle.__init__(self, viewer)
  677 + self.state_code = const.STATE_MEASURE_DENSITY_POLYGON
  678 + self.format = 'polygon'
  679 +
  680 +
530 681 class PanMoveInteractorStyle(DefaultInteractorStyle):
531 682 """
532 683 Interactor style responsible for translate the camera.
... ... @@ -2356,6 +2507,10 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2356 2507  
2357 2508 return out_mask
2358 2509  
  2510 +
  2511 +
  2512 +
  2513 +
2359 2514 def get_style(style):
2360 2515 STYLES = {
2361 2516 const.STATE_DEFAULT: DefaultInteractorStyle,
... ... @@ -2363,6 +2518,8 @@ def get_style(style):
2363 2518 const.STATE_WL: WWWLInteractorStyle,
2364 2519 const.STATE_MEASURE_DISTANCE: LinearMeasureInteractorStyle,
2365 2520 const.STATE_MEASURE_ANGLE: AngularMeasureInteractorStyle,
  2521 + const.STATE_MEASURE_DENSITY_ELLIPSE: DensityMeasureEllipseStyle,
  2522 + const.STATE_MEASURE_DENSITY_POLYGON: DensityMeasurePolygonStyle,
2366 2523 const.STATE_PAN: PanMoveInteractorStyle,
2367 2524 const.STATE_SPIN: SpinInteractorStyle,
2368 2525 const.STATE_ZOOM: ZoomInteractorStyle,
... ...
invesalius/data/surface.py
... ... @@ -346,6 +346,8 @@ class SurfaceManager():
346 346 actor = vtk.vtkActor()
347 347 actor.SetMapper(mapper)
348 348  
  349 + print("BOunds", actor.GetBounds())
  350 +
349 351 if overwrite:
350 352 surface = Surface(index = self.last_surface_index)
351 353 else:
... ...
invesalius/data/transforms.pyx
... ... @@ -133,3 +133,42 @@ def apply_view_matrix_transform(image_t[:, :, :] volume,
133 133 for y in xrange(dy):
134 134 out[z, y, count] = coord_transform(volume, M, x, y, z, sx, sy, sz, f_interp, cval)
135 135 count += 1
  136 +
  137 +
  138 +@cython.boundscheck(False) # turn of bounds-checking for entire function
  139 +@cython.cdivision(True)
  140 +@cython.wraparound(False)
  141 +def convolve_non_zero(image_t[:, :, :] volume,
  142 + image_t[:, :, :] kernel,
  143 + image_t cval):
  144 + cdef Py_ssize_t x, y, z, sx, sy, sz, kx, ky, kz, skx, sky, skz, i, j, k
  145 + cdef image_t v
  146 +
  147 + cdef image_t[:, :, :] out = np.zeros_like(volume)
  148 +
  149 + sz = volume.shape[0]
  150 + sy = volume.shape[1]
  151 + sx = volume.shape[2]
  152 +
  153 + skz = kernel.shape[0]
  154 + sky = kernel.shape[1]
  155 + skx = kernel.shape[2]
  156 +
  157 + for z in prange(sz, nogil=True):
  158 + for y in xrange(sy):
  159 + for x in xrange(sx):
  160 + if volume[z, y, x] != 0:
  161 + for k in xrange(skz):
  162 + kz = z - skz // 2 + k
  163 + for j in xrange(sky):
  164 + ky = y - sky // 2 + j
  165 + for i in xrange(skx):
  166 + kx = x - skx // 2 + i
  167 +
  168 + if 0 <= kz < sz and 0 <= ky < sy and 0 <= kx < sx:
  169 + v = volume[kz, ky, kx]
  170 + else:
  171 + v = cval
  172 +
  173 + out[z, y, x] += (v * kernel[k, j, i])
  174 + return np.asarray(out)
... ...
invesalius/data/viewer_slice.py
... ... @@ -49,6 +49,8 @@ import invesalius.session as ses
49 49 import invesalius.data.converters as converters
50 50 import invesalius.data.measures as measures
51 51  
  52 +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX
  53 +
52 54 if sys.platform == 'win32':
53 55 try:
54 56 import win32api
... ... @@ -159,382 +161,6 @@ class ContourMIPConfig(wx.Panel):
159 161 self.txt_mip_border.Disable()
160 162  
161 163  
162   -class CanvasRendererCTX:
163   - def __init__(self, evt_renderer, canvas_renderer, orientation=None):
164   - """
165   - A Canvas to render over a vtktRenderer.
166   -
167   - Params:
168   - evt_renderer: a vtkRenderer which this class is going to watch for
169   - any render event to update the canvas content.
170   - canvas_renderer: the vtkRenderer where the canvas is going to be
171   - added.
172   -
173   - This class uses wx.GraphicsContext to render to a vtkImage.
174   -
175   - TODO: Verify why in Windows the color are strange when using transparency.
176   - TODO: Add support to evento (ex. click on a square)
177   - """
178   - self.canvas_renderer = canvas_renderer
179   - self.evt_renderer = evt_renderer
180   - self._size = self.canvas_renderer.GetSize()
181   - self.draw_list = []
182   - self.orientation = orientation
183   - self.gc = None
184   - self.last_cam_modif_time = -1
185   - self.modified = True
186   - self._drawn = False
187   - self._init_canvas()
188   - evt_renderer.AddObserver("StartEvent", self.OnPaint)
189   -
190   - def _init_canvas(self):
191   - w, h = self._size
192   - self._array = np.zeros((h, w, 4), dtype=np.uint8)
193   -
194   - self._cv_image = converters.np_rgba_to_vtk(self._array)
195   -
196   - self.mapper = vtk.vtkImageMapper()
197   - self.mapper.SetInputData(self._cv_image)
198   - self.mapper.SetColorWindow(255)
199   - self.mapper.SetColorLevel(128)
200   -
201   - self.actor = vtk.vtkActor2D()
202   - self.actor.SetPosition(0, 0)
203   - self.actor.SetMapper(self.mapper)
204   - self.actor.GetProperty().SetOpacity(0.99)
205   -
206   - self.canvas_renderer.AddActor2D(self.actor)
207   -
208   - self.rgb = np.zeros((h, w, 3), dtype=np.uint8)
209   - self.alpha = np.zeros((h, w, 1), dtype=np.uint8)
210   -
211   - self.bitmap = wx.EmptyBitmapRGBA(w, h)
212   - try:
213   - self.image = wx.Image(w, h, self.rgb, self.alpha)
214   - except TypeError:
215   - self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha)
216   -
217   - def _resize_canvas(self, w, h):
218   - self._array = np.zeros((h, w, 4), dtype=np.uint8)
219   - self._cv_image = converters.np_rgba_to_vtk(self._array)
220   - self.mapper.SetInputData(self._cv_image)
221   - self.mapper.Update()
222   -
223   - self.rgb = np.zeros((h, w, 3), dtype=np.uint8)
224   - self.alpha = np.zeros((h, w, 1), dtype=np.uint8)
225   -
226   - self.bitmap = wx.EmptyBitmapRGBA(w, h)
227   - try:
228   - self.image = wx.Image(w, h, self.rgb, self.alpha)
229   - except TypeError:
230   - self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha)
231   -
232   - self.modified = True
233   -
234   - def remove_from_renderer(self):
235   - self.canvas_renderer.RemoveActor(self.actor)
236   - self.evt_renderer.RemoveObservers("StartEvent")
237   -
238   - def OnPaint(self, evt, obj):
239   - size = self.canvas_renderer.GetSize()
240   - w, h = size
241   - if self._size != size:
242   - self._size = size
243   - self._resize_canvas(w, h)
244   -
245   - cam_modif_time = self.evt_renderer.GetActiveCamera().GetMTime()
246   - if (not self.modified) and cam_modif_time == self.last_cam_modif_time:
247   - return
248   -
249   - self.last_cam_modif_time = cam_modif_time
250   -
251   - self._array[:] = 0
252   -
253   - coord = vtk.vtkCoordinate()
254   -
255   - self.image.SetDataBuffer(self.rgb)
256   - self.image.SetAlphaBuffer(self.alpha)
257   - self.image.Clear()
258   - gc = wx.GraphicsContext.Create(self.image)
259   - if sys.platform != 'darwin':
260   - gc.SetAntialiasMode(0)
261   -
262   - self.gc = gc
263   -
264   - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
265   - # font.SetWeight(wx.BOLD)
266   - font = gc.CreateFont(font, (0, 0, 255))
267   - gc.SetFont(font)
268   -
269   - pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID)
270   - brush = wx.Brush(wx.Colour(0, 255, 0, 128))
271   - gc.SetPen(pen)
272   - gc.SetBrush(brush)
273   - gc.Scale(1, -1)
274   -
275   - for d in self.draw_list:
276   - d.draw_to_canvas(gc, self)
277   -
278   - gc.Destroy()
279   -
280   - self.gc = None
281   -
282   - if self._drawn:
283   - self.bitmap = self.image.ConvertToBitmap()
284   - self.bitmap.CopyToBuffer(self._array, wx.BitmapBufferFormat_RGBA)
285   -
286   - self._cv_image.Modified()
287   - self.modified = False
288   - self._drawn = False
289   -
290   - def calc_text_size(self, text, font=None):
291   - """
292   - Given an unicode text and a font returns the width and height of the
293   - rendered text in pixels.
294   -
295   - Params:
296   - text: An unicode text.
297   - font: An wxFont.
298   -
299   - Returns:
300   - A tuple with width and height values in pixels
301   - """
302   - if self.gc is None:
303   - return None
304   - gc = self.gc
305   -
306   - if font is None:
307   - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
308   -
309   - _font = gc.CreateFont(font)
310   - gc.SetFont(_font)
311   - w, h = gc.GetTextExtent(text)
312   - return w, h
313   -
314   - def draw_line(self, pos0, pos1, arrow_start=False, arrow_end=False, colour=(255, 0, 0, 128), width=2, style=wx.SOLID):
315   - """
316   - Draw a line from pos0 to pos1
317   -
318   - Params:
319   - pos0: the start of the line position (x, y).
320   - pos1: the end of the line position (x, y).
321   - arrow_start: if to draw a arrow at the start of the line.
322   - arrow_end: if to draw a arrow at the end of the line.
323   - colour: RGBA line colour.
324   - width: the width of line.
325   - style: default wx.SOLID.
326   - """
327   - if self.gc is None:
328   - return None
329   - gc = self.gc
330   -
331   - p0x, p0y = pos0
332   - p1x, p1y = pos1
333   -
334   - p0y = -p0y
335   - p1y = -p1y
336   -
337   - pen = wx.Pen(wx.Colour(*[int(c) for c in colour]), width, wx.SOLID)
338   - pen.SetCap(wx.CAP_BUTT)
339   - gc.SetPen(pen)
340   -
341   - path = gc.CreatePath()
342   - path.MoveToPoint(p0x, p0y)
343   - path.AddLineToPoint(p1x, p1y)
344   - gc.StrokePath(path)
345   -
346   - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
347   - font = gc.CreateFont(font)
348   - gc.SetFont(font)
349   - w, h = gc.GetTextExtent("M")
350   -
351   - p0 = np.array((p0x, p0y))
352   - p3 = np.array((p1x, p1y))
353   - if arrow_start:
354   - v = p3 - p0
355   - v = v / np.linalg.norm(v)
356   - iv = np.array((v[1], -v[0]))
357   - p1 = p0 + w*v + iv*w/2.0
358   - p2 = p0 + w*v + (-iv)*w/2.0
359   -
360   - path = gc.CreatePath()
361   - path.MoveToPoint(p0)
362   - path.AddLineToPoint(p1)
363   - path.MoveToPoint(p0)
364   - path.AddLineToPoint(p2)
365   - gc.StrokePath(path)
366   -
367   - if arrow_end:
368   - v = p3 - p0
369   - v = v / np.linalg.norm(v)
370   - iv = np.array((v[1], -v[0]))
371   - p1 = p3 - w*v + iv*w/2.0
372   - p2 = p3 - w*v + (-iv)*w/2.0
373   -
374   - path = gc.CreatePath()
375   - path.MoveToPoint(p3)
376   - path.AddLineToPoint(p1)
377   - path.MoveToPoint(p3)
378   - path.AddLineToPoint(p2)
379   - gc.StrokePath(path)
380   -
381   - self._drawn = True
382   -
383   - def draw_circle(self, center, radius, width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)):
384   - """
385   - Draw a circle centered at center with the given radius.
386   -
387   - Params:
388   - center: (x, y) position.
389   - radius: float number.
390   - width: line width.
391   - line_colour: RGBA line colour
392   - fill_colour: RGBA fill colour.
393   - """
394   - if self.gc is None:
395   - return None
396   - gc = self.gc
397   -
398   - pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID)
399   - gc.SetPen(pen)
400   -
401   - brush = wx.Brush(wx.Colour(*fill_colour))
402   - gc.SetBrush(brush)
403   -
404   - cx, cy = center
405   - cy = -cy
406   -
407   - path = gc.CreatePath()
408   - path.AddCircle(cx, cy, 2.5)
409   - gc.StrokePath(path)
410   - gc.FillPath(path)
411   - self._drawn = True
412   -
413   - def draw_rectangle(self, pos, width, height, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)):
414   - """
415   - Draw a rectangle with its top left at pos and with the given width and height.
416   -
417   - Params:
418   - pos: The top left pos (x, y) of the rectangle.
419   - width: width of the rectangle.
420   - height: heigth of the rectangle.
421   - line_colour: RGBA line colour.
422   - fill_colour: RGBA fill colour.
423   - """
424   - if self.gc is None:
425   - return None
426   - gc = self.gc
427   -
428   - px, py = pos
429   - gc.SetPen(wx.Pen(line_colour))
430   - gc.SetBrush(wx.Brush(fill_colour))
431   - gc.DrawRectangle(px, py, width, height)
432   - self._drawn = True
433   -
434   - def draw_text(self, text, pos, font=None, txt_colour=(255, 255, 255)):
435   - """
436   - Draw text.
437   -
438   - Params:
439   - text: an unicode text.
440   - pos: (x, y) top left position.
441   - font: if None it'll use the default gui font.
442   - txt_colour: RGB text colour
443   - """
444   - if self.gc is None:
445   - return None
446   - gc = self.gc
447   -
448   - if font is None:
449   - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
450   -
451   - font = gc.CreateFont(font, txt_colour)
452   - gc.SetFont(font)
453   -
454   - px, py = pos
455   - py = -py
456   -
457   - gc.DrawText(text, px, py)
458   - self._drawn = True
459   -
460   - def draw_text_box(self, text, pos, font=None, txt_colour=(255, 255, 255), bg_colour=(128, 128, 128, 128), border=5):
461   - """
462   - Draw text inside a text box.
463   -
464   - Params:
465   - text: an unicode text.
466   - pos: (x, y) top left position.
467   - font: if None it'll use the default gui font.
468   - txt_colour: RGB text colour
469   - bg_colour: RGBA box colour
470   - border: the border size.
471   - """
472   - if self.gc is None:
473   - return None
474   - gc = self.gc
475   -
476   - if font is None:
477   - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
478   -
479   - _font = gc.CreateFont(font, txt_colour)
480   - gc.SetFont(_font)
481   - w, h = gc.GetTextExtent(text)
482   -
483   - px, py = pos
484   -
485   - # Drawing the box
486   - cw, ch = w + border * 2, h + border * 2
487   - self.draw_rectangle((px, -py), cw, ch, bg_colour, bg_colour)
488   -
489   - # Drawing the text
490   - tpx, tpy = px + border, py - border
491   - self.draw_text(text, (tpx, tpy), font, txt_colour)
492   - self._drawn = True
493   -
494   - def draw_arc(self, center, p0, p1, line_colour=(255, 0, 0, 128), width=2):
495   - """
496   - Draw an arc passing in p0 and p1 centered at center.
497   -
498   - Params:
499   - center: (x, y) center of the arc.
500   - p0: (x, y).
501   - p1: (x, y).
502   - line_colour: RGBA line colour.
503   - width: width of the line.
504   - """
505   - if self.gc is None:
506   - return None
507   - gc = self.gc
508   - pen = wx.Pen(wx.Colour(*[int(c) for c in line_colour]), width, wx.SOLID)
509   - gc.SetPen(pen)
510   -
511   - c = np.array(center)
512   - v0 = np.array(p0) - c
513   - v1 = np.array(p1) - c
514   -
515   - c[1] = -c[1]
516   - v0[1] = -v0[1]
517   - v1[1] = -v1[1]
518   -
519   - s0 = np.linalg.norm(v0)
520   - s1 = np.linalg.norm(v1)
521   -
522   - a0 = np.arctan2(v0[1] , v0[0])
523   - a1 = np.arctan2(v1[1] , v1[0])
524   -
525   - if (a1 - a0) % (np.pi*2) < (a0 - a1) % (np.pi*2):
526   - sa = a0
527   - ea = a1
528   - else:
529   - sa = a1
530   - ea = a0
531   -
532   - path = gc.CreatePath()
533   - path.AddArc(float(c[0]), float(c[1]), float(min(s0, s1)), float(sa), float(ea), True)
534   - gc.StrokePath(path)
535   - self._drawn = True
536   -
537   -
538 164 class Viewer(wx.Panel):
539 165  
540 166 def __init__(self, prnt, orientation='AXIAL'):
... ... @@ -563,6 +189,8 @@ class Viewer(wx.Panel):
563 189  
564 190 self.canvas = None
565 191  
  192 + self.draw_by_slice_number = collections.defaultdict(list)
  193 +
566 194 # The layout from slice_data, the first is number of cols, the second
567 195 # is the number of rows
568 196 self.layout = (1, 1)
... ... @@ -1037,8 +665,10 @@ class Viewer(wx.Panel):
1037 665 z = bounds[4]
1038 666 return x, y, z
1039 667  
1040   - def get_coordinate_cursor_edition(self, slice_data, picker=None):
  668 + def get_coordinate_cursor_edition(self, slice_data=None, picker=None):
1041 669 # Find position
  670 + if slice_data is None:
  671 + slice_data = self.slice_data
1042 672 actor = slice_data.actor
1043 673 slice_number = slice_data.number
1044 674 if picker is None:
... ... @@ -1460,7 +1090,7 @@ class Viewer(wx.Panel):
1460 1090 self.cam = self.slice_data.renderer.GetActiveCamera()
1461 1091 self.__build_cross_lines()
1462 1092  
1463   - self.canvas = CanvasRendererCTX(self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation)
  1093 + self.canvas = CanvasRendererCTX(self, self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation)
1464 1094 self.canvas.draw_list.append(self.slice_data.text)
1465 1095  
1466 1096 # Set the slice number to the last slice to ensure the camera if far
... ... @@ -1602,21 +1232,27 @@ class Viewer(wx.Panel):
1602 1232  
1603 1233 def UpdateCanvas(self, evt=None):
1604 1234 if self.canvas is not None:
1605   - cp_draw_list = self.canvas.draw_list[:]
1606   - self.canvas.draw_list = []
  1235 + self._update_draw_list()
  1236 + self.canvas.modified = True
  1237 + self.interactor.Render()
1607 1238  
1608   - # Removing all measures
1609   - for i in cp_draw_list:
1610   - if not isinstance(i, (measures.AngularMeasure, measures.LinearMeasure)):
1611   - self.canvas.draw_list.append(i)
  1239 + def _update_draw_list(self):
  1240 + cp_draw_list = self.canvas.draw_list[:]
  1241 + self.canvas.draw_list = []
1612 1242  
1613   - # Then add all needed measures
1614   - for (m, mr) in self.measures.get(self.orientation, self.slice_data.number):
1615   - if m.visible:
1616   - self.canvas.draw_list.append(mr)
  1243 + # Removing all measures
  1244 + for i in cp_draw_list:
  1245 + if not isinstance(i, (measures.AngularMeasure, measures.LinearMeasure, measures.CircleDensityMeasure, measures.PolygonDensityMeasure)):
  1246 + self.canvas.draw_list.append(i)
  1247 +
  1248 + # Then add all needed measures
  1249 + for (m, mr) in self.measures.get(self.orientation, self.slice_data.number):
  1250 + if m.visible:
  1251 + self.canvas.draw_list.append(mr)
  1252 +
  1253 + n = self.slice_data.number
  1254 + self.canvas.draw_list.extend(self.draw_by_slice_number[n])
1617 1255  
1618   - self.canvas.modified = True
1619   - self.interactor.Render()
1620 1256  
1621 1257 def __configure_scroll(self):
1622 1258 actor = self.slice_data_list[0].actor
... ... @@ -1799,15 +1435,16 @@ class Viewer(wx.Panel):
1799 1435 for actor in self.actors_by_slice_number[index]:
1800 1436 self.slice_data.renderer.AddActor(actor)
1801 1437  
1802   - for (m, mr) in self.measures.get(self.orientation, self.slice_data.number):
1803   - try:
1804   - self.canvas.draw_list.remove(mr)
1805   - except ValueError:
1806   - pass
  1438 + # for (m, mr) in self.measures.get(self.orientation, self.slice_data.number):
  1439 + # try:
  1440 + # self.canvas.draw_list.remove(mr)
  1441 + # except ValueError:
  1442 + # pass
  1443 +
  1444 + # for (m, mr) in self.measures.get(self.orientation, index):
  1445 + # if m.visible:
  1446 + # self.canvas.draw_list.append(mr)
1807 1447  
1808   - for (m, mr) in self.measures.get(self.orientation, index):
1809   - if m.visible:
1810   - self.canvas.draw_list.append(mr)
1811 1448  
1812 1449 if self.slice_._type_projection == const.PROJECTION_NORMAL:
1813 1450 self.slice_data.SetNumber(index)
... ... @@ -1817,6 +1454,7 @@ class Viewer(wx.Panel):
1817 1454 self.slice_data.SetNumber(index, end)
1818 1455 self.__update_display_extent(image)
1819 1456 self.cross.SetModelBounds(self.slice_data.actor.GetBounds())
  1457 + self._update_draw_list()
1820 1458  
1821 1459 def ChangeSliceNumber(self, index):
1822 1460 #self.set_slice_number(index)
... ...
invesalius/data/viewer_volume.py
... ... @@ -32,6 +32,8 @@ from wx.lib.pubsub import pub as Publisher
32 32 import random
33 33 from scipy.spatial import distance
34 34  
  35 +from scipy.misc import imsave
  36 +
35 37 import invesalius.constants as const
36 38 import invesalius.data.bases as bases
37 39 import invesalius.data.transformations as tr
... ... @@ -51,6 +53,8 @@ else:
51 53  
52 54 PROP_MEASURE = 0.8
53 55  
  56 +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX, Polygon
  57 +
54 58 class Viewer(wx.Panel):
55 59 def __init__(self, parent):
56 60 wx.Panel.__init__(self, parent, size=wx.Size(320, 320))
... ... @@ -86,17 +90,32 @@ class Viewer(wx.Panel):
86 90 interactor.Enable(1)
87 91  
88 92 ren = vtk.vtkRenderer()
89   - interactor.GetRenderWindow().AddRenderer(ren)
90 93 self.ren = ren
91 94  
  95 + canvas_renderer = vtk.vtkRenderer()
  96 + canvas_renderer.SetLayer(1)
  97 + canvas_renderer.SetInteractive(0)
  98 + canvas_renderer.PreserveDepthBufferOn()
  99 + self.canvas_renderer = canvas_renderer
  100 +
  101 + interactor.GetRenderWindow().SetNumberOfLayers(2)
  102 + interactor.GetRenderWindow().AddRenderer(ren)
  103 + interactor.GetRenderWindow().AddRenderer(canvas_renderer)
  104 +
92 105 self.raycasting_volume = False
93 106  
94 107 self.onclick = False
95 108  
96   - self.text = vtku.Text()
  109 + self.text = vtku.TextZero()
97 110 self.text.SetValue("")
98   - self.ren.AddActor(self.text.actor)
  111 + self.text.SetPosition(const.TEXT_POS_LEFT_UP)
  112 + # self.ren.AddActor(self.text.actor)
  113 +
  114 + # self.polygon = Polygon(None, is_3d=False)
99 115  
  116 + # self.canvas = CanvasRendererCTX(self, self.ren, self.canvas_renderer, 'AXIAL')
  117 + # self.canvas.draw_list.append(self.text)
  118 + # self.canvas.draw_list.append(self.polygon)
100 119 # axes = vtk.vtkAxesActor()
101 120 # axes.SetXAxisLabelText('x')
102 121 # axes.SetYAxisLabelText('y')
... ... @@ -105,7 +124,6 @@ class Viewer(wx.Panel):
105 124 #
106 125 # self.ren.AddActor(axes)
107 126  
108   -
109 127 self.slice_plane = None
110 128  
111 129 self.view_angle = None
... ... @@ -1230,8 +1248,17 @@ class Viewer(wx.Panel):
1230 1248  
1231 1249 def __bind_events_wx(self):
1232 1250 #self.Bind(wx.EVT_SIZE, self.OnSize)
  1251 + # self.canvas.subscribe_event('LeftButtonPressEvent', self.on_insert_point)
1233 1252 pass
1234 1253  
  1254 + def on_insert_point(self, evt):
  1255 + pos = evt.position
  1256 + self.polygon.append_point(pos)
  1257 + self.canvas.Refresh()
  1258 +
  1259 + arr = self.canvas.draw_element_to_array([self.polygon,])
  1260 + imsave('/tmp/polygon.png', arr)
  1261 +
1235 1262 def SetInteractorStyle(self, state):
1236 1263 action = {
1237 1264 const.STATE_PAN:
... ... @@ -1304,7 +1331,7 @@ class Viewer(wx.Panel):
1304 1331 self.style = style
1305 1332  
1306 1333 # Check each event available for each mode
1307   - for event in action[state]:
  1334 + for event in action.get(state, []):
1308 1335 # Bind event
1309 1336 style.AddObserver(event,action[state][event])
1310 1337  
... ... @@ -1483,6 +1510,7 @@ class Viewer(wx.Panel):
1483 1510 def OnSetWindowLevelText(self, ww, wl):
1484 1511 if self.raycasting_volume:
1485 1512 self.text.SetValue("WL: %d WW: %d"%(wl, ww))
  1513 + self.canvas.modified = True
1486 1514  
1487 1515 def OnShowRaycasting(self):
1488 1516 if not self.raycasting_volume:
... ...
invesalius/data/vtk_utils.py
... ... @@ -95,7 +95,8 @@ def ShowProgress(number_of_filters = 1,
95 95  
96 96 class Text(object):
97 97 def __init__(self):
98   -
  98 + self.layer = 99
  99 + self.children = []
99 100 property = vtk.vtkTextProperty()
100 101 property.SetFontSize(const.TEXT_SIZE)
101 102 property.SetFontFamilyToArial()
... ... @@ -195,7 +196,8 @@ class Text(object):
195 196  
196 197 class TextZero(object):
197 198 def __init__(self):
198   -
  199 + self.layer = 99
  200 + self.children = []
199 201 property = vtk.vtkTextProperty()
200 202 property.SetFontSize(const.TEXT_SIZE_LARGE)
201 203 property.SetFontFamilyToArial()
... ...
invesalius/gui/data_notebook.py
... ... @@ -48,6 +48,8 @@ BTN_NEW, BTN_REMOVE, BTN_DUPLICATE, BTN_OPEN = [wx.NewId() for i in range(4)]
48 48  
49 49 TYPE = {const.LINEAR: _(u"Linear"),
50 50 const.ANGULAR: _(u"Angular"),
  51 + const.DENSITY_ELLIPSE: _(u"Density Ellipse"),
  52 + const.DENSITY_POLYGON: _(u"Density Polygon"),
51 53 }
52 54  
53 55 LOCATION = {const.SURFACE: _(u"3D"),
... ... @@ -1171,8 +1173,10 @@ class MeasuresListCtrlPanel(wx.ListCtrl, listmix.TextEditMixin, listmix.CheckLis
1171 1173 location = LOCATION[m.location]
1172 1174 if m.type == const.LINEAR:
1173 1175 value = (u"%.2f mm") % m.value
1174   - else:
  1176 + elif m.type == const.ANGULAR:
1175 1177 value = (u"%.2f°") % m.value
  1178 + else:
  1179 + value = (u"%.3f") % m.value
1176 1180 self.InsertNewItem(m.index, m.name, colour, location, type, value)
1177 1181  
1178 1182 if not m.visible:
... ...
invesalius/gui/dialogs.py
... ... @@ -18,9 +18,13 @@
18 18 # detalhes.
19 19 #--------------------------------------------------------------------------
20 20  
  21 +import itertools
21 22 import os
22 23 import random
23 24 import sys
  25 +import time
  26 +
  27 +from concurrent import futures
24 28  
25 29 if sys.platform == 'win32':
26 30 try:
... ... @@ -3317,6 +3321,112 @@ class FillHolesAutoDialog(wx.Dialog):
3317 3321 self.panel2dcon.Enable(0)
3318 3322  
3319 3323  
  3324 +class MaskDensityDialog(wx.Dialog):
  3325 + def __init__(self, title):
  3326 + try:
  3327 + pre = wx.PreDialog()
  3328 + pre.Create(wx.GetApp().GetTopWindow(), -1, _(u"Mask density"), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT)
  3329 + self.PostCreate(pre)
  3330 + except AttributeError:
  3331 + wx.Dialog.__init__(self, wx.GetApp().GetTopWindow(), -1, _(u"Mask density"),
  3332 + style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT)
  3333 +
  3334 + self._init_gui()
  3335 + self._bind_events()
  3336 +
  3337 + def _init_gui(self):
  3338 + import invesalius.project as prj
  3339 + project = prj.Project()
  3340 +
  3341 + self.cmb_mask = wx.ComboBox(self, -1, choices=[], style=wx.CB_READONLY)
  3342 + if project.mask_dict.values():
  3343 + for mask in project.mask_dict.values():
  3344 + self.cmb_mask.Append(mask.name, mask)
  3345 + self.cmb_mask.SetValue(list(project.mask_dict.values())[0].name)
  3346 +
  3347 + self.calc_button = wx.Button(self, -1, _(u'Calculate'))
  3348 +
  3349 + self.mean_density = self._create_selectable_label_text('')
  3350 + self.min_density = self._create_selectable_label_text('')
  3351 + self.max_density = self._create_selectable_label_text('')
  3352 + self.std_density = self._create_selectable_label_text('')
  3353 +
  3354 +
  3355 + slt_mask_sizer = wx.FlexGridSizer(rows=1, cols=3, vgap=5, hgap=5)
  3356 + slt_mask_sizer.AddMany([
  3357 + (wx.StaticText(self, -1, _(u'Mask:'), style=wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_CENTRE),
  3358 + (self.cmb_mask, 1, wx.EXPAND),
  3359 + (self.calc_button, 0, wx.EXPAND),
  3360 + ])
  3361 +
  3362 + values_sizer = wx.FlexGridSizer(rows=4, cols=2, vgap=5, hgap=5)
  3363 + values_sizer.AddMany([
  3364 + (wx.StaticText(self, -1, _(u'Mean:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT),
  3365 + (self.mean_density, 1, wx.EXPAND),
  3366 +
  3367 + (wx.StaticText(self, -1, _(u'Minimun:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT),
  3368 + (self.min_density, 1, wx.EXPAND),
  3369 +
  3370 + (wx.StaticText(self, -1, _(u'Maximun:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT),
  3371 + (self.max_density, 1, wx.EXPAND),
  3372 +
  3373 + (wx.StaticText(self, -1, _(u'Standard deviation:')), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT),
  3374 + (self.std_density, 1, wx.EXPAND),
  3375 + ])
  3376 +
  3377 + sizer = wx.FlexGridSizer(rows=4, cols=1, vgap=5, hgap=5)
  3378 + sizer.AddSpacer(5)
  3379 + sizer.AddMany([
  3380 + (slt_mask_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 5) ,
  3381 + (values_sizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 5),
  3382 + ])
  3383 + sizer.AddSpacer(5)
  3384 +
  3385 + self.SetSizer(sizer)
  3386 + sizer.Fit(self)
  3387 + self.Layout()
  3388 +
  3389 + self.CenterOnScreen()
  3390 +
  3391 + def _create_selectable_label_text(self, text):
  3392 + label = wx.TextCtrl(self, -1, style=wx.TE_READONLY)
  3393 + label.SetValue(text)
  3394 + # label.SetBackgroundColour(self.GetBackgroundColour())
  3395 + return label
  3396 +
  3397 + def _bind_events(self):
  3398 + self.calc_button.Bind(wx.EVT_BUTTON, self.OnCalcButton)
  3399 +
  3400 + def OnCalcButton(self, evt):
  3401 + from invesalius.data.slice_ import Slice
  3402 + mask = self.cmb_mask.GetClientData(self.cmb_mask.GetSelection())
  3403 +
  3404 + slc = Slice()
  3405 +
  3406 + with futures.ThreadPoolExecutor(max_workers=1) as executor:
  3407 + future = executor.submit(slc.calc_image_density, mask)
  3408 + for c in itertools.cycle(['', '.', '..', '...']):
  3409 + s = _(u'Calculating ') + c
  3410 + self.mean_density.SetValue(s)
  3411 + self.min_density.SetValue(s)
  3412 + self.max_density.SetValue(s)
  3413 + self.std_density.SetValue(s)
  3414 + self.Update()
  3415 + self.Refresh()
  3416 + if future.done():
  3417 + break
  3418 + time.sleep(0.1)
  3419 +
  3420 + _min, _max, _mean, _std = future.result()
  3421 +
  3422 + self.mean_density.SetValue(str(_mean))
  3423 + self.min_density.SetValue(str(_min))
  3424 + self.max_density.SetValue(str(_max))
  3425 + self.std_density.SetValue(str(_std))
  3426 +
  3427 + print(">>>> Area of mask", slc.calc_mask_area(mask))
  3428 +
  3429 +
3320 3430 class ObjectCalibrationDialog(wx.Dialog):
3321 3431  
3322 3432 def __init__(self, nav_prop):
... ...
invesalius/gui/frame.py
... ... @@ -472,6 +472,10 @@ class Frame(wx.Frame):
472 472 elif id == const.ID_REORIENT_IMG:
473 473 self.OnReorientImg()
474 474  
  475 + elif id == const.ID_MASK_DENSITY_MEASURE:
  476 + ddlg = dlg.MaskDensityDialog(self)
  477 + ddlg.Show()
  478 +
475 479 elif id == const.ID_THRESHOLD_SEGMENTATION:
476 480 Publisher.sendMessage("Show panel", panel_id=const.ID_THRESHOLD_SEGMENTATION)
477 481 Publisher.sendMessage('Disable actual style')
... ... @@ -767,6 +771,7 @@ class MenuBar(wx.MenuBar):
767 771 const.ID_WATERSHED_SEGMENTATION,
768 772 const.ID_THRESHOLD_SEGMENTATION,
769 773 const.ID_FLOODFILL_SEGMENTATION,
  774 + const.ID_MASK_DENSITY_MEASURE,
770 775 const.ID_CREATE_SURFACE,
771 776 const.ID_CREATE_MASK,
772 777 const.ID_GOTO_SLICE]
... ... @@ -923,6 +928,7 @@ class MenuBar(wx.MenuBar):
923 928 image_menu.AppendMenu(wx.NewId(), _('Flip'), flip_menu)
924 929 image_menu.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu)
925 930  
  931 + mask_density_menu = image_menu.Append(const.ID_MASK_DENSITY_MEASURE, _(u'Mask Density measure'))
926 932 reorient_menu = image_menu.Append(const.ID_REORIENT_IMG, _(u'Reorient image\tCtrl+Shift+R'))
927 933  
928 934 reorient_menu.Enable(False)
... ... @@ -931,9 +937,7 @@ class MenuBar(wx.MenuBar):
931 937 tools_menu.AppendMenu(-1, _(u"Segmentation"), segmentation_menu)
932 938 tools_menu.AppendMenu(-1, _(u"Surface"), surface_menu)
933 939  
934   -
935 940 #View
936   -
937 941 self.view_menu = view_menu = wx.Menu()
938 942 view_menu.Append(const.ID_VIEW_INTERPOLATED, _(u'Interpolated slices'), "", wx.ITEM_CHECK)
939 943  
... ... @@ -1378,8 +1382,11 @@ class ObjectToolBar(AuiToolBar):
1378 1382 const.STATE_SPIN, const.STATE_ZOOM_SL,
1379 1383 const.STATE_ZOOM,
1380 1384 const.STATE_MEASURE_DISTANCE,
1381   - const.STATE_MEASURE_ANGLE]
1382   - #const.STATE_ANNOTATE]
  1385 + const.STATE_MEASURE_ANGLE,
  1386 + const.STATE_MEASURE_DENSITY_ELLIPSE,
  1387 + const.STATE_MEASURE_DENSITY_POLYGON,
  1388 + # const.STATE_ANNOTATE
  1389 + ]
1383 1390 self.__init_items()
1384 1391 self.__bind_events()
1385 1392 self.__bind_events_wx()
... ... @@ -1431,6 +1438,10 @@ class ObjectToolBar(AuiToolBar):
1431 1438 path = os.path.join(d, "measure_angle_original.png")
1432 1439 BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1433 1440  
  1441 + BMP_ELLIPSE = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (48, 48))
  1442 +
  1443 + BMP_POLYGON = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, wx.ART_TOOLBAR, (48, 48))
  1444 +
1434 1445 #path = os.path.join(d, "tool_annotation_original.png")
1435 1446 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1436 1447  
... ... @@ -1456,6 +1467,10 @@ class ObjectToolBar(AuiToolBar):
1456 1467 path = os.path.join(d, "measure_angle.png")
1457 1468 BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1458 1469  
  1470 + BMP_ELLIPSE = wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_TOOLBAR, (32, 32))
  1471 +
  1472 + BMP_POLYGON = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, wx.ART_TOOLBAR, (32, 32))
  1473 +
1459 1474 #path = os.path.join(d, "tool_annotation.png")
1460 1475 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1461 1476  
... ... @@ -1502,6 +1517,20 @@ class ObjectToolBar(AuiToolBar):
1502 1517 wx.NullBitmap,
1503 1518 short_help_string = _("Measure angle"),
1504 1519 kind = wx.ITEM_CHECK)
  1520 +
  1521 + self.AddTool(const.STATE_MEASURE_DENSITY_ELLIPSE,
  1522 + "",
  1523 + BMP_ELLIPSE,
  1524 + wx.NullBitmap,
  1525 + short_help_string = _("Measure density ellipse"),
  1526 + kind = wx.ITEM_CHECK)
  1527 +
  1528 + self.AddTool(const.STATE_MEASURE_DENSITY_POLYGON,
  1529 + "",
  1530 + BMP_POLYGON,
  1531 + wx.NullBitmap,
  1532 + short_help_string = _("Measure density polygon"),
  1533 + kind = wx.ITEM_CHECK)
1505 1534 #self.AddLabelTool(const.STATE_ANNOTATE,
1506 1535 # "",
1507 1536 # shortHelp = _("Add annotation"),
... ...
invesalius/gui/widgets/canvas_renderer.py 0 → 100644
... ... @@ -0,0 +1,1233 @@
  1 +# -*- coding: utf-8 -*-
  2 +#--------------------------------------------------------------------------
  3 +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas
  4 +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer
  5 +# Homepage: http://www.softwarepublico.gov.br
  6 +# Contact: invesalius@cti.gov.br
  7 +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt)
  8 +#--------------------------------------------------------------------------
  9 +# Este programa e software livre; voce pode redistribui-lo e/ou
  10 +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme
  11 +# publicada pela Free Software Foundation; de acordo com a versao 2
  12 +# da Licenca.
  13 +#
  14 +# Este programa eh distribuido na expectativa de ser util, mas SEM
  15 +# QUALQUER GARANTIA; sem mesmo a garantia implicita de
  16 +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM
  17 +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais
  18 +# detalhes.
  19 +#--------------------------------------------------------------------------
  20 +
  21 +import sys
  22 +
  23 +import numpy as np
  24 +import wx
  25 +import vtk
  26 +
  27 +try:
  28 + from weakref import WeakMethod
  29 +except ImportError:
  30 + from weakrefmethod import WeakMethod
  31 +
  32 +from invesalius.data import converters
  33 +from wx.lib.pubsub import pub as Publisher
  34 +
  35 +
  36 +class CanvasEvent:
  37 + def __init__(self, event_name, root_event_obj, pos, viewer, renderer,
  38 + control_down=False, alt_down=False, shift_down=False):
  39 + self.root_event_obj = root_event_obj
  40 + self.event_name = event_name
  41 + self.position = pos
  42 + self.viewer = viewer
  43 + self.renderer = renderer
  44 +
  45 + self.control_down = control_down
  46 + self.alt_down = alt_down
  47 + self.shift_down = shift_down
  48 +
  49 +
  50 +class CanvasRendererCTX:
  51 + def __init__(self, viewer, evt_renderer, canvas_renderer, orientation=None):
  52 + """
  53 + A Canvas to render over a vtktRenderer.
  54 +
  55 + Params:
  56 + evt_renderer: a vtkRenderer which this class is going to watch for
  57 + any render event to update the canvas content.
  58 + canvas_renderer: the vtkRenderer where the canvas is going to be
  59 + added.
  60 +
  61 + This class uses wx.GraphicsContext to render to a vtkImage.
  62 +
  63 + TODO: Verify why in Windows the color are strange when using transparency.
  64 + TODO: Add support to evento (ex. click on a square)
  65 + """
  66 + self.viewer = viewer
  67 + self.canvas_renderer = canvas_renderer
  68 + self.evt_renderer = evt_renderer
  69 + self._size = self.canvas_renderer.GetSize()
  70 + self.draw_list = []
  71 + self._ordered_draw_list = []
  72 + self.orientation = orientation
  73 + self.gc = None
  74 + self.last_cam_modif_time = -1
  75 + self.modified = True
  76 + self._drawn = False
  77 + self._init_canvas()
  78 +
  79 + self._over_obj = None
  80 + self._drag_obj = None
  81 + self._selected_obj = None
  82 +
  83 + self._callback_events = {
  84 + 'LeftButtonPressEvent': [],
  85 + 'LeftButtonReleaseEvent': [],
  86 + 'LeftButtonDoubleClickEvent': [],
  87 + 'MouseMoveEvent': [],
  88 + }
  89 +
  90 + self._bind_events()
  91 +
  92 + def _bind_events(self):
  93 + iren = self.viewer.interactor
  94 + iren.Bind(wx.EVT_MOTION, self.OnMouseMove)
  95 + iren.Bind(wx.EVT_LEFT_DOWN, self.OnLeftButtonPress)
  96 + iren.Bind(wx.EVT_LEFT_UP, self.OnLeftButtonRelease)
  97 + iren.Bind(wx.EVT_LEFT_DCLICK, self.OnDoubleClick)
  98 + self.canvas_renderer.AddObserver("StartEvent", self.OnPaint)
  99 +
  100 + def subscribe_event(self, event, callback):
  101 + ref = WeakMethod(callback)
  102 + self._callback_events[event].append(ref)
  103 +
  104 + def unsubscribe_event(self, event, callback):
  105 + for n, cb in enumerate(self._callback_events[event]):
  106 + if cb() == callback:
  107 + print('removed')
  108 + self._callback_events[event].pop(n)
  109 + return
  110 +
  111 + def propagate_event(self, root, event):
  112 + print('propagating', event.event_name, 'from', root)
  113 + node = root
  114 + callback_name = 'on_%s' % event.event_name
  115 + while node:
  116 + try:
  117 + getattr(node, callback_name)(event)
  118 + except AttributeError as e:
  119 + print('errror', node, e)
  120 + node = node.parent
  121 +
  122 + def _init_canvas(self):
  123 + w, h = self._size
  124 + self._array = np.zeros((h, w, 4), dtype=np.uint8)
  125 +
  126 + self._cv_image = converters.np_rgba_to_vtk(self._array)
  127 +
  128 + self.mapper = vtk.vtkImageMapper()
  129 + self.mapper.SetInputData(self._cv_image)
  130 + self.mapper.SetColorWindow(255)
  131 + self.mapper.SetColorLevel(128)
  132 +
  133 + self.actor = vtk.vtkActor2D()
  134 + self.actor.SetPosition(0, 0)
  135 + self.actor.SetMapper(self.mapper)
  136 + self.actor.GetProperty().SetOpacity(0.99)
  137 +
  138 + self.canvas_renderer.AddActor2D(self.actor)
  139 +
  140 + self.rgb = np.zeros((h, w, 3), dtype=np.uint8)
  141 + self.alpha = np.zeros((h, w, 1), dtype=np.uint8)
  142 +
  143 + self.bitmap = wx.EmptyBitmapRGBA(w, h)
  144 + try:
  145 + self.image = wx.Image(w, h, self.rgb, self.alpha)
  146 + except TypeError:
  147 + self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha)
  148 +
  149 + def _resize_canvas(self, w, h):
  150 + self._array = np.zeros((h, w, 4), dtype=np.uint8)
  151 + self._cv_image = converters.np_rgba_to_vtk(self._array)
  152 + self.mapper.SetInputData(self._cv_image)
  153 + self.mapper.Update()
  154 +
  155 + self.rgb = np.zeros((h, w, 3), dtype=np.uint8)
  156 + self.alpha = np.zeros((h, w, 1), dtype=np.uint8)
  157 +
  158 + self.bitmap = wx.EmptyBitmapRGBA(w, h)
  159 + try:
  160 + self.image = wx.Image(w, h, self.rgb, self.alpha)
  161 + except TypeError:
  162 + self.image = wx.ImageFromBuffer(w, h, self.rgb, self.alpha)
  163 +
  164 + self.modified = True
  165 +
  166 + def remove_from_renderer(self):
  167 + self.canvas_renderer.RemoveActor(self.actor)
  168 + self.evt_renderer.RemoveObservers("StartEvent")
  169 +
  170 + def get_over_mouse_obj(self, x, y):
  171 + for n, i in self._ordered_draw_list[::-1]:
  172 + try:
  173 + obj = i.is_over(x, y)
  174 + self._over_obj = obj
  175 + if obj:
  176 + print("is over at", n, i)
  177 + return True
  178 + except AttributeError:
  179 + pass
  180 + return False
  181 +
  182 + def Refresh(self):
  183 + print('Refresh')
  184 + self.modified = True
  185 + self.viewer.interactor.Render()
  186 +
  187 + def OnMouseMove(self, evt):
  188 + x, y = evt.GetPosition()
  189 + y = self.viewer.interactor.GetSize()[1] - y
  190 + redraw = False
  191 +
  192 + if self._drag_obj:
  193 + redraw = True
  194 + evt_obj = CanvasEvent('mouse_move', self._drag_obj, (x, y), self.viewer, self.evt_renderer,
  195 + control_down=evt.ControlDown(),
  196 + alt_down=evt.AltDown(),
  197 + shift_down=evt.ShiftDown())
  198 + self.propagate_event(self._drag_obj, evt_obj)
  199 + # self._drag_obj.mouse_move(evt_obj)
  200 + else:
  201 + was_over = self._over_obj
  202 + redraw = self.get_over_mouse_obj(x, y) or was_over
  203 +
  204 + if was_over and was_over != self._over_obj:
  205 + try:
  206 + evt_obj = CanvasEvent('mouse_leave', was_over, (x, y), self.viewer,
  207 + self.evt_renderer,
  208 + control_down=evt.ControlDown(),
  209 + alt_down=evt.AltDown(),
  210 + shift_down=evt.ShiftDown())
  211 + was_over.on_mouse_leave(evt_obj)
  212 + except AttributeError:
  213 + pass
  214 +
  215 + if self._over_obj:
  216 + try:
  217 + evt_obj = CanvasEvent('mouse_enter', self._over_obj, (x, y), self.viewer,
  218 + self.evt_renderer,
  219 + control_down=evt.ControlDown(),
  220 + alt_down=evt.AltDown(),
  221 + shift_down=evt.ShiftDown())
  222 + self._over_obj.on_mouse_enter(evt_obj)
  223 + except AttributeError:
  224 + pass
  225 +
  226 + if redraw:
  227 + # Publisher.sendMessage('Redraw canvas %s' % self.orientation)
  228 + self.Refresh()
  229 +
  230 + evt.Skip()
  231 +
  232 + def OnLeftButtonPress(self, evt):
  233 + x, y = evt.GetPosition()
  234 + y = self.viewer.interactor.GetSize()[1] - y
  235 + if self._over_obj and hasattr(self._over_obj, 'on_mouse_move'):
  236 + if hasattr(self._over_obj, 'on_select'):
  237 + try:
  238 + evt_obj = CanvasEvent('deselect', self._over_obj, (x, y), self.viewer,
  239 + self.evt_renderer,
  240 + control_down=evt.ControlDown(),
  241 + alt_down=evt.AltDown(),
  242 + shift_down=evt.ShiftDown())
  243 + # self._selected_obj.on_deselect(evt_obj)
  244 + self.propagate_event(self._selected_obj, evt_obj)
  245 + except AttributeError:
  246 + pass
  247 + evt_obj = CanvasEvent('select', self._over_obj, (x, y), self.viewer,
  248 + self.evt_renderer,
  249 + control_down=evt.ControlDown(),
  250 + alt_down=evt.AltDown(),
  251 + shift_down=evt.ShiftDown())
  252 + # self._over_obj.on_select(evt_obj)
  253 + self.propagate_event(self._over_obj, evt_obj)
  254 + self._selected_obj = self._over_obj
  255 + self.Refresh()
  256 + self._drag_obj = self._over_obj
  257 + else:
  258 + self.get_over_mouse_obj(x, y)
  259 + if not self._over_obj:
  260 + evt_obj = CanvasEvent('leftclick', None, (x, y), self.viewer,
  261 + self.evt_renderer,
  262 + control_down=evt.ControlDown(),
  263 + alt_down=evt.AltDown(),
  264 + shift_down=evt.ShiftDown())
  265 + # self._selected_obj.on_deselect(evt_obj)
  266 + for cb in self._callback_events['LeftButtonPressEvent']:
  267 + if cb() is not None:
  268 + cb()(evt_obj)
  269 + break
  270 + try:
  271 + evt_obj = CanvasEvent('deselect', self._over_obj, (x, y), self.viewer,
  272 + self.evt_renderer,
  273 + control_down=evt.ControlDown(),
  274 + alt_down=evt.AltDown(),
  275 + shift_down=evt.ShiftDown())
  276 + # self._selected_obj.on_deselect(evt_obj)
  277 + if self._selected_obj.on_deselect(evt_obj):
  278 + self.Refresh()
  279 + except AttributeError:
  280 + pass
  281 + evt.Skip()
  282 +
  283 + def OnLeftButtonRelease(self, evt):
  284 + self._over_obj = None
  285 + self._drag_obj = None
  286 + evt.Skip()
  287 +
  288 + def OnDoubleClick(self, evt):
  289 + x, y = evt.GetPosition()
  290 + y = self.viewer.interactor.GetSize()[1] - y
  291 + evt_obj = CanvasEvent('double_left_click', None, (x, y), self.viewer, self.evt_renderer,
  292 + control_down=evt.ControlDown(),
  293 + alt_down=evt.AltDown(),
  294 + shift_down=evt.ShiftDown())
  295 + for cb in self._callback_events['LeftButtonDoubleClickEvent']:
  296 + if cb() is not None:
  297 + cb()(evt_obj)
  298 + break
  299 + evt.Skip()
  300 +
  301 + def OnPaint(self, evt, obj):
  302 + size = self.canvas_renderer.GetSize()
  303 + w, h = size
  304 + if self._size != size:
  305 + self._size = size
  306 + self._resize_canvas(w, h)
  307 +
  308 + cam_modif_time = self.evt_renderer.GetActiveCamera().GetMTime()
  309 + if (not self.modified) and cam_modif_time == self.last_cam_modif_time:
  310 + return
  311 +
  312 + self.last_cam_modif_time = cam_modif_time
  313 +
  314 + self._array[:] = 0
  315 +
  316 + coord = vtk.vtkCoordinate()
  317 +
  318 + self.image.SetDataBuffer(self.rgb)
  319 + self.image.SetAlphaBuffer(self.alpha)
  320 + self.image.Clear()
  321 + gc = wx.GraphicsContext.Create(self.image)
  322 + if sys.platform != 'darwin':
  323 + gc.SetAntialiasMode(0)
  324 +
  325 + self.gc = gc
  326 +
  327 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  328 + # font.SetWeight(wx.BOLD)
  329 + font = gc.CreateFont(font, (0, 0, 255))
  330 + gc.SetFont(font)
  331 +
  332 + pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID)
  333 + brush = wx.Brush(wx.Colour(0, 255, 0, 128))
  334 + gc.SetPen(pen)
  335 + gc.SetBrush(brush)
  336 + gc.Scale(1, -1)
  337 +
  338 + self._ordered_draw_list = sorted(self._follow_draw_list(), key=lambda x: x[0])
  339 + for l, d in self._ordered_draw_list: #sorted(self.draw_list, key=lambda x: x.layer if hasattr(x, 'layer') else 0):
  340 + d.draw_to_canvas(gc, self)
  341 +
  342 + gc.Destroy()
  343 +
  344 + self.gc = None
  345 +
  346 + if self._drawn:
  347 + self.bitmap = self.image.ConvertToBitmap()
  348 + self.bitmap.CopyToBuffer(self._array, wx.BitmapBufferFormat_RGBA)
  349 +
  350 + self._cv_image.Modified()
  351 + self.modified = False
  352 + self._drawn = False
  353 +
  354 + def _follow_draw_list(self):
  355 + out = []
  356 + def loop(node, layer):
  357 + for child in node.children:
  358 + loop(child, layer + child.layer)
  359 + out.append((layer + child.layer, child))
  360 +
  361 + for element in self.draw_list:
  362 + out.append((element.layer, element))
  363 + if hasattr(element, 'children'):
  364 + loop(element,element.layer)
  365 +
  366 + return out
  367 +
  368 +
  369 + def draw_element_to_array(self, elements, size=None, antialiasing=False, flip=True):
  370 + """
  371 + Draws the given elements to a array.
  372 +
  373 + Params:
  374 + elements: a list of elements (objects that contains the
  375 + draw_to_canvas method) to draw to a array.
  376 + flip: indicates if it is necessary to flip. In this canvas the Y
  377 + coordinates starts in the bottom of the screen.
  378 + """
  379 + if size is None:
  380 + size = self.canvas_renderer.GetSize()
  381 + w, h = size
  382 + image = wx.EmptyImage(w, h)
  383 + image.Clear()
  384 +
  385 + arr = np.zeros((h, w, 4), dtype=np.uint8)
  386 +
  387 + gc = wx.GraphicsContext.Create(image)
  388 + if antialiasing:
  389 + gc.SetAntialiasMode(0)
  390 +
  391 + old_gc = self.gc
  392 + self.gc = gc
  393 +
  394 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  395 + font = gc.CreateFont(font, (0, 0, 255))
  396 + gc.SetFont(font)
  397 +
  398 + pen = wx.Pen(wx.Colour(255, 0, 0, 128), 2, wx.SOLID)
  399 + brush = wx.Brush(wx.Colour(0, 255, 0, 128))
  400 + gc.SetPen(pen)
  401 + gc.SetBrush(brush)
  402 + gc.Scale(1, -1)
  403 +
  404 + for element in elements:
  405 + element.draw_to_canvas(gc, self)
  406 +
  407 + gc.Destroy()
  408 + self.gc = old_gc
  409 +
  410 + bitmap = image.ConvertToBitmap()
  411 + bitmap.CopyToBuffer(arr, wx.BitmapBufferFormat_RGBA)
  412 +
  413 + if flip:
  414 + arr = arr[::-1]
  415 +
  416 + return arr
  417 +
  418 + def calc_text_size(self, text, font=None):
  419 + """
  420 + Given an unicode text and a font returns the width and height of the
  421 + rendered text in pixels.
  422 +
  423 + Params:
  424 + text: An unicode text.
  425 + font: An wxFont.
  426 +
  427 + Returns:
  428 + A tuple with width and height values in pixels
  429 + """
  430 + if self.gc is None:
  431 + return None
  432 + gc = self.gc
  433 +
  434 + if font is None:
  435 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  436 +
  437 + _font = gc.CreateFont(font)
  438 + gc.SetFont(_font)
  439 +
  440 + w = 0
  441 + h = 0
  442 + for t in text.split('\n'):
  443 + _w, _h = gc.GetTextExtent(t)
  444 + w = max(w, _w)
  445 + h += _h
  446 + return w, h
  447 +
  448 + def draw_line(self, pos0, pos1, arrow_start=False, arrow_end=False, colour=(255, 0, 0, 128), width=2, style=wx.SOLID):
  449 + """
  450 + Draw a line from pos0 to pos1
  451 +
  452 + Params:
  453 + pos0: the start of the line position (x, y).
  454 + pos1: the end of the line position (x, y).
  455 + arrow_start: if to draw a arrow at the start of the line.
  456 + arrow_end: if to draw a arrow at the end of the line.
  457 + colour: RGBA line colour.
  458 + width: the width of line.
  459 + style: default wx.SOLID.
  460 + """
  461 + if self.gc is None:
  462 + return None
  463 + gc = self.gc
  464 +
  465 + p0x, p0y = pos0
  466 + p1x, p1y = pos1
  467 +
  468 + p0y = -p0y
  469 + p1y = -p1y
  470 +
  471 + pen = wx.Pen(wx.Colour(*[int(c) for c in colour]), width, wx.SOLID)
  472 + pen.SetCap(wx.CAP_BUTT)
  473 + gc.SetPen(pen)
  474 +
  475 + path = gc.CreatePath()
  476 + path.MoveToPoint(p0x, p0y)
  477 + path.AddLineToPoint(p1x, p1y)
  478 + gc.StrokePath(path)
  479 +
  480 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  481 + font = gc.CreateFont(font)
  482 + gc.SetFont(font)
  483 + w, h = gc.GetTextExtent("M")
  484 +
  485 + p0 = np.array((p0x, p0y))
  486 + p3 = np.array((p1x, p1y))
  487 + if arrow_start:
  488 + v = p3 - p0
  489 + v = v / np.linalg.norm(v)
  490 + iv = np.array((v[1], -v[0]))
  491 + p1 = p0 + w*v + iv*w/2.0
  492 + p2 = p0 + w*v + (-iv)*w/2.0
  493 +
  494 + path = gc.CreatePath()
  495 + path.MoveToPoint(p0)
  496 + path.AddLineToPoint(p1)
  497 + path.MoveToPoint(p0)
  498 + path.AddLineToPoint(p2)
  499 + gc.StrokePath(path)
  500 +
  501 + if arrow_end:
  502 + v = p3 - p0
  503 + v = v / np.linalg.norm(v)
  504 + iv = np.array((v[1], -v[0]))
  505 + p1 = p3 - w*v + iv*w/2.0
  506 + p2 = p3 - w*v + (-iv)*w/2.0
  507 +
  508 + path = gc.CreatePath()
  509 + path.MoveToPoint(p3)
  510 + path.AddLineToPoint(p1)
  511 + path.MoveToPoint(p3)
  512 + path.AddLineToPoint(p2)
  513 + gc.StrokePath(path)
  514 +
  515 + self._drawn = True
  516 +
  517 + def draw_circle(self, center, radius=2.5, width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)):
  518 + """
  519 + Draw a circle centered at center with the given radius.
  520 +
  521 + Params:
  522 + center: (x, y) position.
  523 + radius: float number.
  524 + width: line width.
  525 + line_colour: RGBA line colour
  526 + fill_colour: RGBA fill colour.
  527 + """
  528 + if self.gc is None:
  529 + return None
  530 + gc = self.gc
  531 +
  532 + pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID)
  533 + gc.SetPen(pen)
  534 +
  535 + brush = wx.Brush(wx.Colour(*fill_colour))
  536 + gc.SetBrush(brush)
  537 +
  538 + cx, cy = center
  539 + cy = -cy
  540 +
  541 + path = gc.CreatePath()
  542 + path.AddCircle(cx, cy, radius)
  543 + gc.StrokePath(path)
  544 + gc.FillPath(path)
  545 + self._drawn = True
  546 +
  547 + return (cx, -cy, radius*2, radius*2)
  548 +
  549 + def draw_ellipse(self, center, width, height, line_width=2, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)):
  550 + """
  551 + Draw a ellipse centered at center with the given width and height.
  552 +
  553 + Params:
  554 + center: (x, y) position.
  555 + width: ellipse width (float number).
  556 + height: ellipse height (float number)
  557 + line_width: line width.
  558 + line_colour: RGBA line colour
  559 + fill_colour: RGBA fill colour.
  560 + """
  561 + if self.gc is None:
  562 + return None
  563 + gc = self.gc
  564 +
  565 + pen = wx.Pen(wx.Colour(*line_colour), line_width, wx.SOLID)
  566 + gc.SetPen(pen)
  567 +
  568 + brush = wx.Brush(wx.Colour(*fill_colour))
  569 + gc.SetBrush(brush)
  570 +
  571 + cx, cy = center
  572 + xi = cx - width/2.0
  573 + xf = cx + width/2.0
  574 + yi = cy - height/2.0
  575 + yf = cy + height/2.0
  576 +
  577 + cx -= width/2.0
  578 + cy += height/2.0
  579 + cy = -cy
  580 +
  581 + path = gc.CreatePath()
  582 + path.AddEllipse(cx, cy, width, height)
  583 + gc.StrokePath(path)
  584 + gc.FillPath(path)
  585 + self._drawn = True
  586 +
  587 + return (xi, yi, xf, yf)
  588 +
  589 + def draw_rectangle(self, pos, width, height, line_colour=(255, 0, 0, 128), fill_colour=(0, 0, 0, 0)):
  590 + """
  591 + Draw a rectangle with its top left at pos and with the given width and height.
  592 +
  593 + Params:
  594 + pos: The top left pos (x, y) of the rectangle.
  595 + width: width of the rectangle.
  596 + height: heigth of the rectangle.
  597 + line_colour: RGBA line colour.
  598 + fill_colour: RGBA fill colour.
  599 + """
  600 + if self.gc is None:
  601 + return None
  602 + gc = self.gc
  603 +
  604 + px, py = pos
  605 + py = -py
  606 + gc.SetPen(wx.Pen(wx.Colour(*line_colour)))
  607 + gc.SetBrush(wx.Brush(wx.Colour(*fill_colour)))
  608 + gc.DrawRectangle(px, py, width, height)
  609 + self._drawn = True
  610 +
  611 + def draw_text(self, text, pos, font=None, txt_colour=(255, 255, 255)):
  612 + """
  613 + Draw text.
  614 +
  615 + Params:
  616 + text: an unicode text.
  617 + pos: (x, y) top left position.
  618 + font: if None it'll use the default gui font.
  619 + txt_colour: RGB text colour
  620 + """
  621 + if self.gc is None:
  622 + return None
  623 + gc = self.gc
  624 +
  625 + if font is None:
  626 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  627 +
  628 + font = gc.CreateFont(font, txt_colour)
  629 + gc.SetFont(font)
  630 +
  631 + px, py = pos
  632 + for t in text.split('\n'):
  633 + _py = -py
  634 + _px = px
  635 + gc.DrawText(t, _px, _py)
  636 +
  637 + w, h = self.calc_text_size(t)
  638 + py -= h
  639 +
  640 + self._drawn = True
  641 +
  642 + def draw_text_box(self, text, pos, font=None, txt_colour=(255, 255, 255), bg_colour=(128, 128, 128, 128), border=5):
  643 + """
  644 + Draw text inside a text box.
  645 +
  646 + Params:
  647 + text: an unicode text.
  648 + pos: (x, y) top left position.
  649 + font: if None it'll use the default gui font.
  650 + txt_colour: RGB text colour
  651 + bg_colour: RGBA box colour
  652 + border: the border size.
  653 + """
  654 + if self.gc is None:
  655 + return None
  656 + gc = self.gc
  657 +
  658 + if font is None:
  659 + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
  660 +
  661 + _font = gc.CreateFont(font, txt_colour)
  662 + gc.SetFont(_font)
  663 + w, h = self.calc_text_size(text)
  664 +
  665 + px, py = pos
  666 +
  667 + # Drawing the box
  668 + cw, ch = w + border * 2, h + border * 2
  669 + self.draw_rectangle((px, py), cw, ch, bg_colour, bg_colour)
  670 +
  671 + # Drawing the text
  672 + tpx, tpy = px + border, py - border
  673 + self.draw_text(text, (tpx, tpy), font, txt_colour)
  674 + self._drawn = True
  675 +
  676 + return px, py, cw, ch
  677 +
  678 + def draw_arc(self, center, p0, p1, line_colour=(255, 0, 0, 128), width=2):
  679 + """
  680 + Draw an arc passing in p0 and p1 centered at center.
  681 +
  682 + Params:
  683 + center: (x, y) center of the arc.
  684 + p0: (x, y).
  685 + p1: (x, y).
  686 + line_colour: RGBA line colour.
  687 + width: width of the line.
  688 + """
  689 + if self.gc is None:
  690 + return None
  691 + gc = self.gc
  692 + pen = wx.Pen(wx.Colour(*line_colour), width, wx.SOLID)
  693 + gc.SetPen(pen)
  694 +
  695 + c = np.array(center)
  696 + v0 = np.array(p0) - c
  697 + v1 = np.array(p1) - c
  698 +
  699 + c[1] = -c[1]
  700 + v0[1] = -v0[1]
  701 + v1[1] = -v1[1]
  702 +
  703 + s0 = np.linalg.norm(v0)
  704 + s1 = np.linalg.norm(v1)
  705 +
  706 + a0 = np.arctan2(v0[1] , v0[0])
  707 + a1 = np.arctan2(v1[1] , v1[0])
  708 +
  709 + if (a1 - a0) % (np.pi*2) < (a0 - a1) % (np.pi*2):
  710 + sa = a0
  711 + ea = a1
  712 + else:
  713 + sa = a1
  714 + ea = a0
  715 +
  716 + path = gc.CreatePath()
  717 + path.AddArc(float(c[0]), float(c[1]), float(min(s0, s1)), float(sa), float(ea), True)
  718 + gc.StrokePath(path)
  719 + self._drawn = True
  720 +
  721 + def draw_polygon(self, points, fill=True, closed=False, line_colour=(255, 255, 255, 255),
  722 + fill_colour=(255, 255, 255, 255), width=2):
  723 +
  724 + if self.gc is None:
  725 + return None
  726 + gc = self.gc
  727 +
  728 + gc.SetPen(wx.Pen(wx.Colour(*line_colour), width, wx.SOLID))
  729 + gc.SetBrush(wx.Brush(wx.Colour(*fill_colour), wx.SOLID))
  730 +
  731 + if points:
  732 + path = gc.CreatePath()
  733 + px, py = points[0]
  734 + path.MoveToPoint((px, -py))
  735 +
  736 + for point in points:
  737 + px, py = point
  738 + path.AddLineToPoint((px, -py))
  739 +
  740 + if closed:
  741 + px, py = points[0]
  742 + path.AddLineToPoint((px, -py))
  743 +
  744 + gc.StrokePath(path)
  745 + gc.FillPath(path)
  746 +
  747 + self._drawn = True
  748 +
  749 + return path
  750 +
  751 +
  752 +class CanvasHandlerBase(object):
  753 + def __init__(self, parent):
  754 + self.parent = parent
  755 + self.children = []
  756 + self.layer = 0
  757 + self._visible = True
  758 +
  759 + @property
  760 + def visible(self):
  761 + return self._visible
  762 +
  763 + @visible.setter
  764 + def visible(self, value):
  765 + self._visible = value
  766 + for child in self.children:
  767 + child.visible = value
  768 +
  769 + def _3d_to_2d(self, renderer, pos):
  770 + coord = vtk.vtkCoordinate()
  771 + coord.SetValue(pos)
  772 + px, py = coord.GetComputedDoubleDisplayValue(renderer)
  773 + return px, py
  774 +
  775 + def add_child(self, child):
  776 + self.children.append(child)
  777 +
  778 + def draw_to_canvas(self, gc, canvas):
  779 + pass
  780 +
  781 + def is_over(self, x, y):
  782 + xi, yi, xf, yf = self.bbox
  783 + if xi <= x <= xf and yi <= y <= yf:
  784 + return self
  785 + return None
  786 +
  787 +class TextBox(CanvasHandlerBase):
  788 + def __init__(self, parent,
  789 + text, position=(0, 0, 0),
  790 + text_colour=(0, 0, 0, 255),
  791 + box_colour=(255, 255, 255, 255)):
  792 + super(TextBox, self).__init__(parent)
  793 +
  794 + self.layer = 0
  795 + self.text = text
  796 + self.text_colour = text_colour
  797 + self.box_colour = box_colour
  798 + self.position = position
  799 +
  800 + self.children = []
  801 +
  802 + self.bbox = (0, 0, 0, 0)
  803 +
  804 + self._highlight = False
  805 +
  806 + self._last_position = (0, 0, 0)
  807 +
  808 + def set_text(self, text):
  809 + self.text = text
  810 +
  811 + def draw_to_canvas(self, gc, canvas):
  812 + if self.visible:
  813 + px, py = self._3d_to_2d(canvas.evt_renderer, self.position)
  814 +
  815 + x, y, w, h = canvas.draw_text_box(self.text, (px, py),
  816 + txt_colour=self.text_colour,
  817 + bg_colour=self.box_colour)
  818 + if self._highlight:
  819 + rw, rh = canvas.evt_renderer.GetSize()
  820 + canvas.draw_rectangle((px, py), w, h,
  821 + (255, 0, 0, 25),
  822 + (255, 0, 0, 25))
  823 +
  824 + self.bbox = (x, y - h, x + w, y)
  825 +
  826 + def is_over(self, x, y):
  827 + xi, yi, xf, yf = self.bbox
  828 + if xi <= x <= xf and yi <= y <= yf:
  829 + return self
  830 + return None
  831 +
  832 + def on_mouse_move(self, evt):
  833 + mx, my = evt.position
  834 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  835 + self.position = [i - j + k for (i, j, k) in zip((x, y, z), self._last_position, self.position)]
  836 +
  837 + self._last_position = (x, y, z)
  838 +
  839 + return True
  840 +
  841 + def on_mouse_enter(self, evt):
  842 + # self.layer = 99
  843 + self._highlight = True
  844 +
  845 + def on_mouse_leave(self, evt):
  846 + # self.layer = 0
  847 + self._highlight = False
  848 +
  849 + def on_select(self, evt):
  850 + mx, my = evt.position
  851 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  852 + self._last_position = (x, y, z)
  853 +
  854 +
  855 +class CircleHandler(CanvasHandlerBase):
  856 + def __init__(self, parent, position, radius=5,
  857 + line_colour=(255, 255, 255, 255),
  858 + fill_colour=(0, 0, 0, 0), is_3d=True):
  859 +
  860 + super(CircleHandler, self).__init__(parent)
  861 +
  862 + self.layer = 0
  863 + self.position = position
  864 + self.radius = radius
  865 + self.line_colour = line_colour
  866 + self.fill_colour = fill_colour
  867 + self.bbox = (0, 0, 0, 0)
  868 + self.is_3d = is_3d
  869 +
  870 + self.children = []
  871 +
  872 + self._on_move_function = None
  873 +
  874 + def on_move(self, evt_function):
  875 + self._on_move_function = WeakMethod(evt_function)
  876 +
  877 + def draw_to_canvas(self, gc, canvas):
  878 + if self.visible:
  879 + if self.is_3d:
  880 + px, py = self._3d_to_2d(canvas.evt_renderer, self.position)
  881 + else:
  882 + px, py = self.position
  883 + x, y, w, h = canvas.draw_circle((px, py), self.radius,
  884 + line_colour=self.line_colour,
  885 + fill_colour=self.fill_colour)
  886 + self.bbox = (x - w/2, y - h/2, x + w/2, y + h/2)
  887 +
  888 + def on_mouse_move(self, evt):
  889 + mx, my = evt.position
  890 + if self.is_3d:
  891 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  892 + self.position = (x, y, z)
  893 +
  894 + else:
  895 + self.position = mx, my
  896 +
  897 + if self._on_move_function and self._on_move_function():
  898 + self._on_move_function()(self, evt)
  899 +
  900 + return True
  901 +
  902 +
  903 +class Polygon(CanvasHandlerBase):
  904 + def __init__(self, parent,
  905 + points=None,
  906 + fill=True,
  907 + closed=True,
  908 + line_colour=(255, 255, 255, 255),
  909 + fill_colour=(255, 255, 255, 128), width=2,
  910 + interactive=True, is_3d=True):
  911 +
  912 + super(Polygon, self).__init__(parent)
  913 +
  914 + self.layer = 0
  915 + self.children = []
  916 +
  917 + if points is None:
  918 + self.points = []
  919 + else:
  920 + self.points = points
  921 +
  922 + self.handlers = []
  923 +
  924 + self.fill = fill
  925 + self.closed = closed
  926 + self.line_colour = line_colour
  927 +
  928 + self._path = None
  929 +
  930 + if self.fill:
  931 + self.fill_colour = fill_colour
  932 + else:
  933 + self.fill_colour = (0, 0, 0, 0)
  934 +
  935 + self.width = width
  936 + self._interactive = interactive
  937 + self.is_3d = is_3d
  938 +
  939 + @property
  940 + def interactive(self):
  941 + return self._interactive
  942 +
  943 + @interactive.setter
  944 + def interactive(self, value):
  945 + self._interactive = value
  946 + for handler in self.handlers:
  947 + handler.visible = value
  948 +
  949 + def draw_to_canvas(self, gc, canvas):
  950 + if self.visible and self.points:
  951 + if self.is_3d:
  952 + points = [self._3d_to_2d(canvas.evt_renderer, p) for p in self.points]
  953 + else:
  954 + points = self.points
  955 + self._path = canvas.draw_polygon(points, self.fill, self.closed, self.line_colour, self.fill_colour, self.width)
  956 +
  957 + # if self.closed:
  958 + # U, L = self.convex_hull(points, merge=False)
  959 + # canvas.draw_polygon(U, self.fill, self.closed, self.line_colour, (0, 255, 0, 255), self.width)
  960 + # canvas.draw_polygon(L, self.fill, self.closed, self.line_colour, (0, 0, 255, 255), self.width)
  961 + # for p0, p1 in self.get_all_antipodal_pairs(points):
  962 + # canvas.draw_line(p0, p1)
  963 +
  964 + # if self.interactive:
  965 + # for handler in self.handlers:
  966 + # handler.draw_to_canvas(gc, canvas)
  967 +
  968 + def append_point(self, point):
  969 + handler = CircleHandler(self, point, is_3d=self.is_3d, fill_colour=(255, 0, 0, 255))
  970 + handler.layer = 1
  971 + self.add_child(handler)
  972 + # handler.on_move(self.on_move_point)
  973 + self.handlers.append(handler)
  974 + self.points.append(point)
  975 +
  976 + def on_mouse_move(self, evt):
  977 + if evt.root_event_obj is self:
  978 + self.on_mouse_move2(evt)
  979 + else:
  980 + self.points = []
  981 + for handler in self.handlers:
  982 + self.points.append(handler.position)
  983 +
  984 + def is_over(self, x, y):
  985 + if self.closed and self._path and self._path.Contains(x, -y):
  986 + return self
  987 +
  988 + def on_mouse_move2(self, evt):
  989 + mx, my = evt.position
  990 + if self.is_3d:
  991 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  992 + new_pos = (x, y, z)
  993 + else:
  994 + new_pos = mx, my
  995 +
  996 + diff = [i-j for i,j in zip(new_pos, self._last_position)]
  997 +
  998 + for n, point in enumerate(self.points):
  999 + self.points[n] = tuple((i+j for i,j in zip(diff, point)))
  1000 + self.handlers[n].position = self.points[n]
  1001 +
  1002 + self._last_position = new_pos
  1003 +
  1004 + return True
  1005 +
  1006 + def on_mouse_enter(self, evt):
  1007 + pass
  1008 + # self.interactive = True
  1009 + # self.layer = 99
  1010 +
  1011 + def on_mouse_leave(self, evt):
  1012 + pass
  1013 + # self.interactive = False
  1014 + # self.layer = 0
  1015 +
  1016 + def on_select(self, evt):
  1017 + mx, my = evt.position
  1018 + self.interactive = True
  1019 + print("on_select", self.interactive)
  1020 + if self.is_3d:
  1021 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  1022 + self._last_position = (x, y, z)
  1023 + else:
  1024 + self._last_position = (mx, my)
  1025 +
  1026 + def on_deselect(self, evt):
  1027 + self.interactive = False
  1028 + return True
  1029 +
  1030 + def convex_hull(self, points, merge=True):
  1031 + spoints = sorted(points)
  1032 + U = []
  1033 + L = []
  1034 +
  1035 + _dir = lambda o, a, b: (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
  1036 +
  1037 + for p in spoints:
  1038 + while len(L) >= 2 and _dir(L[-2], L[-1], p) <= 0:
  1039 + L.pop()
  1040 + L.append(p)
  1041 +
  1042 + for p in reversed(spoints):
  1043 + while len(U) >= 2 and _dir(U[-2], U[-1], p) <= 0:
  1044 + U.pop()
  1045 + U.append(p)
  1046 +
  1047 + if merge:
  1048 + return U + L
  1049 + return U, L
  1050 +
  1051 + def get_all_antipodal_pairs(self, points):
  1052 + U, L = self.convex_hull(points, merge=False)
  1053 + i = 0
  1054 + j = len(L) - 1
  1055 + while i < len(U) - 1 or j > 0:
  1056 + yield U[i], L[j]
  1057 +
  1058 + if i == len(U) - 1:
  1059 + j -= 1
  1060 + elif j == 0:
  1061 + i += 1
  1062 + 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]):
  1063 + i += 1
  1064 + else:
  1065 + j -= 1
  1066 +
  1067 +
  1068 +class Ellipse(CanvasHandlerBase):
  1069 + def __init__(self, parent,
  1070 + center,
  1071 + point1, point2,
  1072 + fill=True,
  1073 + line_colour=(255, 255, 255, 255),
  1074 + fill_colour=(255, 255, 255, 128), width=2,
  1075 + interactive=True, is_3d=True):
  1076 +
  1077 + super(Ellipse, self).__init__(parent)
  1078 +
  1079 + self.children = []
  1080 + self.layer = 0
  1081 +
  1082 + self.center = center
  1083 + self.point1 = point1
  1084 + self.point2 = point2
  1085 +
  1086 + self.bbox = (0, 0, 0, 0)
  1087 +
  1088 + self.fill = fill
  1089 + self.line_colour = line_colour
  1090 + if self.fill:
  1091 + self.fill_colour = fill_colour
  1092 + else:
  1093 + self.fill_colour = (0, 0, 0, 0)
  1094 + self.width = width
  1095 + self._interactive = interactive
  1096 + self.is_3d = is_3d
  1097 +
  1098 + self.handler_1 = CircleHandler(self, self.point1, is_3d=is_3d, fill_colour=(255, 0, 0, 255))
  1099 + self.handler_1.layer = 1
  1100 + self.handler_2 = CircleHandler(self, self.point2, is_3d=is_3d, fill_colour=(255, 0, 0, 255))
  1101 + self.handler_2.layer = 1
  1102 +
  1103 + self.add_child(self.handler_1)
  1104 + self.add_child(self.handler_2)
  1105 +
  1106 + @property
  1107 + def interactive(self):
  1108 + return self._interactive
  1109 +
  1110 + @interactive.setter
  1111 + def interactive(self, value):
  1112 + self._interactive = value
  1113 + self.handler_1.visible = value
  1114 + self.handler_2.visible = value
  1115 +
  1116 + def draw_to_canvas(self, gc, canvas):
  1117 + if self.visible:
  1118 + if self.is_3d:
  1119 + cx, cy = self._3d_to_2d(canvas.evt_renderer, self.center)
  1120 + p1x, p1y = self._3d_to_2d(canvas.evt_renderer, self.point1)
  1121 + p2x, p2y = self._3d_to_2d(canvas.evt_renderer, self.point2)
  1122 + else:
  1123 + cx, cy = self.center
  1124 + p1x, p1y = self.point1
  1125 + p2x, p2y = self.point2
  1126 +
  1127 + width = abs(p1x - cx) * 2.0
  1128 + height = abs(p2y - cy) * 2.0
  1129 +
  1130 + self.bbox = canvas.draw_ellipse((cx, cy), width,
  1131 + height, self.width,
  1132 + self.line_colour,
  1133 + self.fill_colour)
  1134 + # if self.interactive:
  1135 + # self.handler_1.draw_to_canvas(gc, canvas)
  1136 + # self.handler_2.draw_to_canvas(gc, canvas)
  1137 +
  1138 + def set_point1(self, pos):
  1139 + self.point1 = pos
  1140 + self.handler_1.position = pos
  1141 +
  1142 + def set_point2(self, pos):
  1143 + self.point2 = pos
  1144 + self.handler_2.position = pos
  1145 +
  1146 + def on_mouse_move(self, evt):
  1147 + if evt.root_event_obj is self:
  1148 + self.on_mouse_move2(evt)
  1149 + else:
  1150 + self.move_p1(evt)
  1151 + self.move_p2(evt)
  1152 +
  1153 + def move_p1(self, evt):
  1154 + pos = self.handler_1.position
  1155 + if evt.viewer.orientation == 'AXIAL':
  1156 + pos = pos[0], self.point1[1], self.point1[2]
  1157 + elif evt.viewer.orientation == 'CORONAL':
  1158 + pos = pos[0], self.point1[1], self.point1[2]
  1159 + elif evt.viewer.orientation == 'SAGITAL':
  1160 + pos = self.point1[0], pos[1], self.point1[2]
  1161 +
  1162 + self.set_point1(pos)
  1163 +
  1164 + if evt.control_down:
  1165 + dist = np.linalg.norm(np.array(self.point1) - np.array(self.center))
  1166 + vec = np.array(self.point2) - np.array(self.center)
  1167 + vec /= np.linalg.norm(vec)
  1168 + point2 = np.array(self.center) + vec * dist
  1169 +
  1170 + self.set_point2(tuple(point2))
  1171 +
  1172 + def move_p2(self, evt):
  1173 + pos = self.handler_2.position
  1174 + if evt.viewer.orientation == 'AXIAL':
  1175 + pos = self.point2[0], pos[1], self.point2[2]
  1176 + elif evt.viewer.orientation == 'CORONAL':
  1177 + pos = self.point2[0], self.point2[1], pos[2]
  1178 + elif evt.viewer.orientation == 'SAGITAL':
  1179 + pos = self.point2[0], self.point2[1], pos[2]
  1180 +
  1181 + self.set_point2(pos)
  1182 +
  1183 + if evt.control_down:
  1184 + dist = np.linalg.norm(np.array(self.point2) - np.array(self.center))
  1185 + vec = np.array(self.point1) - np.array(self.center)
  1186 + vec /= np.linalg.norm(vec)
  1187 + point1 = np.array(self.center) + vec * dist
  1188 +
  1189 + self.set_point1(tuple(point1))
  1190 +
  1191 + def on_mouse_enter(self, evt):
  1192 + # self.interactive = True
  1193 + pass
  1194 +
  1195 + def on_mouse_leave(self, evt):
  1196 + # self.interactive = False
  1197 + pass
  1198 +
  1199 + def is_over(self, x, y):
  1200 + xi, yi, xf, yf = self.bbox
  1201 + if xi <= x <= xf and yi <= y <= yf:
  1202 + return self
  1203 +
  1204 + def on_mouse_move2(self, evt):
  1205 + mx, my = evt.position
  1206 + if self.is_3d:
  1207 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  1208 + new_pos = (x, y, z)
  1209 + else:
  1210 + new_pos = mx, my
  1211 +
  1212 + diff = [i-j for i,j in zip(new_pos, self._last_position)]
  1213 +
  1214 + self.center = tuple((i+j for i,j in zip(diff, self.center)))
  1215 + self.set_point1(tuple((i+j for i,j in zip(diff, self.point1))))
  1216 + self.set_point2(tuple((i+j for i,j in zip(diff, self.point2))))
  1217 +
  1218 + self._last_position = new_pos
  1219 +
  1220 + return True
  1221 +
  1222 + def on_select(self, evt):
  1223 + self.interactive = True
  1224 + mx, my = evt.position
  1225 + if self.is_3d:
  1226 + x, y, z = evt.viewer.get_coordinate_cursor(mx, my)
  1227 + self._last_position = (x, y, z)
  1228 + else:
  1229 + self._last_position = (mx, my)
  1230 +
  1231 + def on_deselect(self, evt):
  1232 + self.interactive = False
  1233 + return True
... ...
invesalius/math_utils.py
1 1 # -*- coding: utf-8 -*-
2 2  
3 3 import math
4   -import numpy
  4 +import numpy as np
5 5  
6 6 def calculate_distance(p1, p2):
7 7 """
... ... @@ -15,20 +15,67 @@ def calculate_distance(p1, p2):
15 15 """
16 16 return math.sqrt(sum([(j-i)**2 for i,j in zip(p1, p2)]))
17 17  
  18 +
18 19 def calculate_angle(v1, v2):
19 20 """
20 21 Calculates the angle formed between vector v1 and v2.
21 22  
22 23 >>> calculate_angle((0, 1), (1, 0))
23 24 90.0
24   -
  25 +
25 26 >>> calculate_angle((1, 0), (0, 1))
26 27 90.0
27 28 """
28   - cos_ = numpy.dot(v1, v2)/(numpy.linalg.norm(v1)*numpy.linalg.norm(v2))
  29 + cos_ = np.dot(v1, v2)/(np.linalg.norm(v1)*np.linalg.norm(v2))
29 30 angle = math.degrees(math.acos(cos_))
30 31 return angle
31 32  
  33 +
  34 +def calc_ellipse_area(a, b):
  35 + """
  36 + Calculates the area of the ellipse with the given a and b radius.
  37 +
  38 + >>> area = calc_ellipse_area(3, 5)
  39 + >>> np.allclose(area, 47.1238)
  40 + True
  41 +
  42 + >>> area = calc_polygon_area(10, 10)
  43 + >>> np.allclose(area, 314.1592)
  44 + True
  45 + """
  46 + return np.pi * a * b
  47 +
  48 +
  49 +def calc_polygon_area(points):
  50 + """
  51 + Calculates the area from the polygon formed by given the points.
  52 +
  53 + >>> # Square
  54 + >>> calc_polygon_area([(0,0), (0,2), (2, 2), (2, 0)])
  55 + 4.0
  56 +
  57 + >>> # Triangle
  58 + >>> calc_polygon_area([(0, 0), (0, 9), (6, 0)])
  59 + 27.0
  60 +
  61 + >>> points = [(1.2*np.cos(i), 1.2*np.sin(i)) for i in np.linspace(0, 2.0*np.pi, 9)]
  62 + >>> area = calc_polygon_area(points)
  63 + >>> np.allclose(area, 4.0729)
  64 + True
  65 +
  66 + >>> 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)]
  67 + >>> area = calc_polygon_area(points)
  68 + >>> np.allclose(area, 4477.4906)
  69 + True
  70 + """
  71 + area = 0.0
  72 + j = len(points) - 1
  73 + for i in range(len(points)):
  74 + area += (points[j][0]+points[i][0]) * (points[j][1]-points[i][1])
  75 + j = i
  76 + area = abs(area / 2.0)
  77 + return area
  78 +
32 79 if __name__ == '__main__':
33 80 import doctest
34 81 doctest.testmod()
... ...
invesalius/project.py
... ... @@ -192,17 +192,7 @@ class Project(with_metaclass(Singleton, object)):
192 192 d = self.measurement_dict
193 193 for i in d:
194 194 m = d[i]
195   - item = {}
196   - item["index"] = m.index
197   - item["name"] = m.name
198   - item["colour"] = m.colour
199   - item["value"] = m.value
200   - item["location"] = m.location
201   - item["type"] = m.type
202   - item["slice_number"] = m.slice_number
203   - item["points"] = m.points
204   - item["visible"] = m.visible
205   - measures[str(m.index)] = item
  195 + measures[str(m.index)] = m.get_as_dict()
206 196 return measures
207 197  
208 198 def SavePlistProject(self, dir_, filename, compress=False):
... ... @@ -346,7 +336,10 @@ class Project(with_metaclass(Singleton, object)):
346 336 measurements = plistlib.readPlist(os.path.join(dirpath,
347 337 project["measurements"]))
348 338 for index in measurements:
349   - measure = ms.Measurement()
  339 + if measurements[index]["type"] in (const.DENSITY_ELLIPSE, const.DENSITY_POLYGON):
  340 + measure = ms.DensityMeasurement()
  341 + else:
  342 + measure = ms.Measurement()
350 343 measure.Load(measurements[index])
351 344 self.measurement_dict[int(index)] = measure
352 345  
... ...