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,6 +105,8 @@ SAGITAL_STR="SAGITAL"
105 # Measure type 105 # Measure type
106 LINEAR = 6 106 LINEAR = 6
107 ANGULAR = 7 107 ANGULAR = 7
  108 +DENSITY_ELLIPSE = 8
  109 +DENSITY_POLYGON = 9
108 110
109 # Colour representing each orientation 111 # Colour representing each orientation
110 ORIENTATION_COLOUR = {'AXIAL': (1,0,0), # Red 112 ORIENTATION_COLOUR = {'AXIAL': (1,0,0), # Red
@@ -551,6 +553,8 @@ ID_WATERSHED_SEGMENTATION = wx.NewId() @@ -551,6 +553,8 @@ ID_WATERSHED_SEGMENTATION = wx.NewId()
551 ID_THRESHOLD_SEGMENTATION = wx.NewId() 553 ID_THRESHOLD_SEGMENTATION = wx.NewId()
552 ID_FLOODFILL_SEGMENTATION = wx.NewId() 554 ID_FLOODFILL_SEGMENTATION = wx.NewId()
553 ID_CROP_MASK = wx.NewId() 555 ID_CROP_MASK = wx.NewId()
  556 +ID_DENSITY_MEASURE = wx.NewId()
  557 +ID_MASK_DENSITY_MEASURE = wx.NewId()
554 ID_CREATE_SURFACE = wx.NewId() 558 ID_CREATE_SURFACE = wx.NewId()
555 ID_CREATE_MASK = wx.NewId() 559 ID_CREATE_MASK = wx.NewId()
556 560
@@ -566,6 +570,9 @@ STATE_PAN = 1005 @@ -566,6 +570,9 @@ STATE_PAN = 1005
566 STATE_ANNOTATE = 1006 570 STATE_ANNOTATE = 1006
567 STATE_MEASURE_DISTANCE = 1007 571 STATE_MEASURE_DISTANCE = 1007
568 STATE_MEASURE_ANGLE = 1008 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 SLICE_STATE_CROSS = 3006 577 SLICE_STATE_CROSS = 3006
571 SLICE_STATE_SCROLL = 3007 578 SLICE_STATE_SCROLL = 3007
@@ -584,7 +591,11 @@ VOLUME_STATE_SEED = 2001 @@ -584,7 +591,11 @@ VOLUME_STATE_SEED = 2001
584 591
585 TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM, 592 TOOL_STATES = [STATE_WL, STATE_SPIN, STATE_ZOOM,
586 STATE_ZOOM_SL, STATE_PAN, STATE_MEASURE_DISTANCE, 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 TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL, 600 TOOL_SLICE_STATES = [SLICE_STATE_CROSS, SLICE_STATE_SCROLL,
590 SLICE_STATE_REORIENT] 601 SLICE_STATE_REORIENT]
@@ -599,6 +610,9 @@ SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS) @@ -599,6 +610,9 @@ SLICE_STYLES.append(SLICE_STATE_REMOVE_MASK_PARTS)
599 SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS) 610 SLICE_STYLES.append(SLICE_STATE_SELECT_MASK_PARTS)
600 SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION) 611 SLICE_STYLES.append(SLICE_STATE_FFILL_SEGMENTATION)
601 SLICE_STYLES.append(SLICE_STATE_CROP_MASK) 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 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE, 617 VOLUME_STYLES = TOOL_STATES + [VOLUME_STATE_SEED, STATE_MEASURE_DISTANCE,
604 STATE_MEASURE_ANGLE] 618 STATE_MEASURE_ANGLE]
@@ -619,6 +633,9 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1, @@ -619,6 +633,9 @@ STYLE_LEVEL = {SLICE_STATE_EDITOR: 1,
619 STATE_DEFAULT: 0, 633 STATE_DEFAULT: 0,
620 STATE_MEASURE_ANGLE: 2, 634 STATE_MEASURE_ANGLE: 2,
621 STATE_MEASURE_DISTANCE: 2, 635 STATE_MEASURE_DISTANCE: 2,
  636 + STATE_MEASURE_DENSITY_ELLIPSE: 2,
  637 + STATE_MEASURE_DENSITY_POLYGON: 2,
  638 + STATE_MEASURE_DENSITY: 2,
622 STATE_WL: 2, 639 STATE_WL: 2,
623 STATE_SPIN: 2, 640 STATE_SPIN: 2,
624 STATE_ZOOM: 2, 641 STATE_ZOOM: 2,
invesalius/data/measures.py
@@ -15,8 +15,15 @@ import invesalius.constants as const @@ -15,8 +15,15 @@ import invesalius.constants as const
15 import invesalius.project as prj 15 import invesalius.project as prj
16 import invesalius.session as ses 16 import invesalius.session as ses
17 import invesalius.utils as utils 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 TYPE = {const.LINEAR: _(u"Linear"), 23 TYPE = {const.LINEAR: _(u"Linear"),
19 const.ANGULAR: _(u"Angular"), 24 const.ANGULAR: _(u"Angular"),
  25 + const.DENSITY_ELLIPSE: _(u"Density Ellipse"),
  26 + const.DENSITY_POLYGON: _(u"Density Polygon"),
20 } 27 }
21 28
22 LOCATION = {const.SURFACE: _(u"3D"), 29 LOCATION = {const.SURFACE: _(u"3D"),
@@ -47,6 +54,9 @@ else: @@ -47,6 +54,9 @@ else:
47 MEASURE_TEXT_COLOUR = (0, 0, 0) 54 MEASURE_TEXT_COLOUR = (0, 0, 0)
48 MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255) 55 MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255)
49 56
  57 +
  58 +DEBUG_DENSITY = False
  59 +
50 class MeasureData(with_metaclass(utils.Singleton)): 60 class MeasureData(with_metaclass(utils.Singleton)):
51 """ 61 """
52 Responsible to keep measures data. 62 Responsible to keep measures data.
@@ -117,38 +127,63 @@ class MeasurementManager(object): @@ -117,38 +127,63 @@ class MeasurementManager(object):
117 Publisher.subscribe(self._rm_incomplete_measurements, 127 Publisher.subscribe(self._rm_incomplete_measurements,
118 "Remove incomplete measurements") 128 "Remove incomplete measurements")
119 Publisher.subscribe(self._change_measure_point_pos, 'Change measurement point position') 129 Publisher.subscribe(self._change_measure_point_pos, 'Change measurement point position')
  130 + Publisher.subscribe(self._add_density_measure, "Add density measurement")
120 Publisher.subscribe(self.OnCloseProject, 'Close project data') 131 Publisher.subscribe(self.OnCloseProject, 'Close project data')
121 132
122 def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)): 133 def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)):
123 for i in measurement_dict: 134 for i in measurement_dict:
124 m = measurement_dict[i] 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 if m.location == const.SURFACE: 183 if m.location == const.SURFACE:
150 Publisher.sendMessage(("Add actors " + str(m.location)), 184 Publisher.sendMessage(("Add actors " + str(m.location)),
151 actors=actors) 185 actors=actors)
  186 +
152 self.current = None 187 self.current = None
153 188
154 if not m.visible: 189 if not m.visible:
@@ -315,6 +350,39 @@ class MeasurementManager(object): @@ -315,6 +350,39 @@ class MeasurementManager(object):
315 # self.measures.pop() 350 # self.measures.pop()
316 self.current = None 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 def OnCloseProject(self): 386 def OnCloseProject(self):
319 self.measures.clean() 387 self.measures.clean()
320 388
@@ -344,6 +412,75 @@ class Measurement(): @@ -344,6 +412,75 @@ class Measurement():
344 self.points = info["points"] 412 self.points = info["points"]
345 self.visible = info["visible"] 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 class CirclePointRepresentation(object): 484 class CirclePointRepresentation(object):
348 """ 485 """
349 This class represents a circle that indicate a point in the surface 486 This class represents a circle that indicate a point in the surface
@@ -452,6 +589,7 @@ class LinearMeasure(object): @@ -452,6 +589,7 @@ class LinearMeasure(object):
452 self.line_actor = None 589 self.line_actor = None
453 self.text_actor = None 590 self.text_actor = None
454 self.renderer = None 591 self.renderer = None
  592 + self.layer = 0
455 if not representation: 593 if not representation:
456 representation = CirclePointRepresentation(colour) 594 representation = CirclePointRepresentation(colour)
457 self.representation = representation 595 self.representation = representation
@@ -632,6 +770,7 @@ class AngularMeasure(object): @@ -632,6 +770,7 @@ class AngularMeasure(object):
632 self.point_actor3 = None 770 self.point_actor3 = None
633 self.line_actor = None 771 self.line_actor = None
634 self.text_actor = None 772 self.text_actor = None
  773 + self.layer = 0
635 if not representation: 774 if not representation:
636 representation = CirclePointRepresentation(colour) 775 representation = CirclePointRepresentation(colour)
637 self.representation = representation 776 self.representation = representation
@@ -799,7 +938,6 @@ class AngularMeasure(object): @@ -799,7 +938,6 @@ class AngularMeasure(object):
799 for p in self.points: 938 for p in self.points:
800 coord.SetValue(p) 939 coord.SetValue(p)
801 cx, cy = coord.GetComputedDoubleDisplayValue(canvas.evt_renderer) 940 cx, cy = coord.GetComputedDoubleDisplayValue(canvas.evt_renderer)
802 - print(cx, cy)  
803 # canvas.draw_circle((cx, cy), 2.5) 941 # canvas.draw_circle((cx, cy), 2.5)
804 points.append((cx, cy)) 942 points.append((cx, cy))
805 943
@@ -811,8 +949,11 @@ class AngularMeasure(object): @@ -811,8 +949,11 @@ class AngularMeasure(object):
811 if len(points) == 3: 949 if len(points) == 3:
812 txt = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue()) 950 txt = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue())
813 r, g, b = self.colour 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 def GetNumberOfPoints(self): 958 def GetNumberOfPoints(self):
818 return self.number_of_points 959 return self.number_of_points
@@ -894,3 +1035,539 @@ class AngularMeasure(object): @@ -894,3 +1035,539 @@ class AngularMeasure(object):
894 1035
895 def __del__(self): 1036 def __del__(self):
896 self.Remove() 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,6 +23,8 @@ import tempfile
23 23
24 import numpy as np 24 import numpy as np
25 import vtk 25 import vtk
  26 +
  27 +from scipy import ndimage
26 from wx.lib.pubsub import pub as Publisher 28 from wx.lib.pubsub import pub as Publisher
27 29
28 import invesalius.constants as const 30 import invesalius.constants as const
@@ -1540,3 +1542,61 @@ class Slice(with_metaclass(utils.Singleton, object)): @@ -1540,3 +1542,61 @@ class Slice(with_metaclass(utils.Singleton, object)):
1540 self.buffer_slices['SAGITAL'].discard_vtk_mask() 1542 self.buffer_slices['SAGITAL'].discard_vtk_mask()
1541 1543
1542 Publisher.sendMessage('Reload actual slice') 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,7 +45,7 @@ from scipy.ndimage import watershed_ift, generate_binary_structure
45 from skimage.morphology import watershed 45 from skimage.morphology import watershed
46 46
47 import invesalius.gui.dialogs as dialogs 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 from . import floodfill 50 from . import floodfill
51 51
@@ -140,6 +140,7 @@ class DefaultInteractorStyle(BaseImageInteractorStyle): @@ -140,6 +140,7 @@ class DefaultInteractorStyle(BaseImageInteractorStyle):
140 140
141 # Zoom using right button 141 # Zoom using right button
142 self.AddObserver("RightButtonPressEvent",self.OnZoomRightClick) 142 self.AddObserver("RightButtonPressEvent",self.OnZoomRightClick)
  143 + self.AddObserver("RightButtonReleaseEvent",self.OnZoomRightRelease)
143 self.AddObserver("MouseMoveEvent", self.OnZoomRightMove) 144 self.AddObserver("MouseMoveEvent", self.OnZoomRightMove)
144 145
145 self.AddObserver("MouseWheelForwardEvent",self.OnScrollForward) 146 self.AddObserver("MouseWheelForwardEvent",self.OnScrollForward)
@@ -161,6 +162,12 @@ class DefaultInteractorStyle(BaseImageInteractorStyle): @@ -161,6 +162,12 @@ class DefaultInteractorStyle(BaseImageInteractorStyle):
161 def OnZoomRightClick(self, evt, obj): 162 def OnZoomRightClick(self, evt, obj):
162 evt.StartDolly() 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 def OnScrollForward(self, evt, obj): 171 def OnScrollForward(self, evt, obj):
165 iren = self.viewer.interactor 172 iren = self.viewer.interactor
166 viewer = self.viewer 173 viewer = self.viewer
@@ -527,6 +534,150 @@ class AngularMeasureInteractorStyle(LinearMeasureInteractorStyle): @@ -527,6 +534,150 @@ class AngularMeasureInteractorStyle(LinearMeasureInteractorStyle):
527 self.state_code = const.STATE_MEASURE_ANGLE 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 class PanMoveInteractorStyle(DefaultInteractorStyle): 681 class PanMoveInteractorStyle(DefaultInteractorStyle):
531 """ 682 """
532 Interactor style responsible for translate the camera. 683 Interactor style responsible for translate the camera.
@@ -2356,6 +2507,10 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): @@ -2356,6 +2507,10 @@ class FloodFillSegmentInteractorStyle(DefaultInteractorStyle):
2356 2507
2357 return out_mask 2508 return out_mask
2358 2509
  2510 +
  2511 +
  2512 +
  2513 +
2359 def get_style(style): 2514 def get_style(style):
2360 STYLES = { 2515 STYLES = {
2361 const.STATE_DEFAULT: DefaultInteractorStyle, 2516 const.STATE_DEFAULT: DefaultInteractorStyle,
@@ -2363,6 +2518,8 @@ def get_style(style): @@ -2363,6 +2518,8 @@ def get_style(style):
2363 const.STATE_WL: WWWLInteractorStyle, 2518 const.STATE_WL: WWWLInteractorStyle,
2364 const.STATE_MEASURE_DISTANCE: LinearMeasureInteractorStyle, 2519 const.STATE_MEASURE_DISTANCE: LinearMeasureInteractorStyle,
2365 const.STATE_MEASURE_ANGLE: AngularMeasureInteractorStyle, 2520 const.STATE_MEASURE_ANGLE: AngularMeasureInteractorStyle,
  2521 + const.STATE_MEASURE_DENSITY_ELLIPSE: DensityMeasureEllipseStyle,
  2522 + const.STATE_MEASURE_DENSITY_POLYGON: DensityMeasurePolygonStyle,
2366 const.STATE_PAN: PanMoveInteractorStyle, 2523 const.STATE_PAN: PanMoveInteractorStyle,
2367 const.STATE_SPIN: SpinInteractorStyle, 2524 const.STATE_SPIN: SpinInteractorStyle,
2368 const.STATE_ZOOM: ZoomInteractorStyle, 2525 const.STATE_ZOOM: ZoomInteractorStyle,
invesalius/data/surface.py
@@ -346,6 +346,8 @@ class SurfaceManager(): @@ -346,6 +346,8 @@ class SurfaceManager():
346 actor = vtk.vtkActor() 346 actor = vtk.vtkActor()
347 actor.SetMapper(mapper) 347 actor.SetMapper(mapper)
348 348
  349 + print("BOunds", actor.GetBounds())
  350 +
349 if overwrite: 351 if overwrite:
350 surface = Surface(index = self.last_surface_index) 352 surface = Surface(index = self.last_surface_index)
351 else: 353 else:
invesalius/data/transforms.pyx
@@ -133,3 +133,42 @@ def apply_view_matrix_transform(image_t[:, :, :] volume, @@ -133,3 +133,42 @@ def apply_view_matrix_transform(image_t[:, :, :] volume,
133 for y in xrange(dy): 133 for y in xrange(dy):
134 out[z, y, count] = coord_transform(volume, M, x, y, z, sx, sy, sz, f_interp, cval) 134 out[z, y, count] = coord_transform(volume, M, x, y, z, sx, sy, sz, f_interp, cval)
135 count += 1 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,6 +49,8 @@ import invesalius.session as ses
49 import invesalius.data.converters as converters 49 import invesalius.data.converters as converters
50 import invesalius.data.measures as measures 50 import invesalius.data.measures as measures
51 51
  52 +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX
  53 +
52 if sys.platform == 'win32': 54 if sys.platform == 'win32':
53 try: 55 try:
54 import win32api 56 import win32api
@@ -159,382 +161,6 @@ class ContourMIPConfig(wx.Panel): @@ -159,382 +161,6 @@ class ContourMIPConfig(wx.Panel):
159 self.txt_mip_border.Disable() 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 class Viewer(wx.Panel): 164 class Viewer(wx.Panel):
539 165
540 def __init__(self, prnt, orientation='AXIAL'): 166 def __init__(self, prnt, orientation='AXIAL'):
@@ -563,6 +189,8 @@ class Viewer(wx.Panel): @@ -563,6 +189,8 @@ class Viewer(wx.Panel):
563 189
564 self.canvas = None 190 self.canvas = None
565 191
  192 + self.draw_by_slice_number = collections.defaultdict(list)
  193 +
566 # The layout from slice_data, the first is number of cols, the second 194 # The layout from slice_data, the first is number of cols, the second
567 # is the number of rows 195 # is the number of rows
568 self.layout = (1, 1) 196 self.layout = (1, 1)
@@ -1037,8 +665,10 @@ class Viewer(wx.Panel): @@ -1037,8 +665,10 @@ class Viewer(wx.Panel):
1037 z = bounds[4] 665 z = bounds[4]
1038 return x, y, z 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 # Find position 669 # Find position
  670 + if slice_data is None:
  671 + slice_data = self.slice_data
1042 actor = slice_data.actor 672 actor = slice_data.actor
1043 slice_number = slice_data.number 673 slice_number = slice_data.number
1044 if picker is None: 674 if picker is None:
@@ -1460,7 +1090,7 @@ class Viewer(wx.Panel): @@ -1460,7 +1090,7 @@ class Viewer(wx.Panel):
1460 self.cam = self.slice_data.renderer.GetActiveCamera() 1090 self.cam = self.slice_data.renderer.GetActiveCamera()
1461 self.__build_cross_lines() 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 self.canvas.draw_list.append(self.slice_data.text) 1094 self.canvas.draw_list.append(self.slice_data.text)
1465 1095
1466 # Set the slice number to the last slice to ensure the camera if far 1096 # Set the slice number to the last slice to ensure the camera if far
@@ -1602,21 +1232,27 @@ class Viewer(wx.Panel): @@ -1602,21 +1232,27 @@ class Viewer(wx.Panel):
1602 1232
1603 def UpdateCanvas(self, evt=None): 1233 def UpdateCanvas(self, evt=None):
1604 if self.canvas is not None: 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 def __configure_scroll(self): 1257 def __configure_scroll(self):
1622 actor = self.slice_data_list[0].actor 1258 actor = self.slice_data_list[0].actor
@@ -1799,15 +1435,16 @@ class Viewer(wx.Panel): @@ -1799,15 +1435,16 @@ class Viewer(wx.Panel):
1799 for actor in self.actors_by_slice_number[index]: 1435 for actor in self.actors_by_slice_number[index]:
1800 self.slice_data.renderer.AddActor(actor) 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 if self.slice_._type_projection == const.PROJECTION_NORMAL: 1449 if self.slice_._type_projection == const.PROJECTION_NORMAL:
1813 self.slice_data.SetNumber(index) 1450 self.slice_data.SetNumber(index)
@@ -1817,6 +1454,7 @@ class Viewer(wx.Panel): @@ -1817,6 +1454,7 @@ class Viewer(wx.Panel):
1817 self.slice_data.SetNumber(index, end) 1454 self.slice_data.SetNumber(index, end)
1818 self.__update_display_extent(image) 1455 self.__update_display_extent(image)
1819 self.cross.SetModelBounds(self.slice_data.actor.GetBounds()) 1456 self.cross.SetModelBounds(self.slice_data.actor.GetBounds())
  1457 + self._update_draw_list()
1820 1458
1821 def ChangeSliceNumber(self, index): 1459 def ChangeSliceNumber(self, index):
1822 #self.set_slice_number(index) 1460 #self.set_slice_number(index)
invesalius/data/viewer_volume.py
@@ -32,6 +32,8 @@ from wx.lib.pubsub import pub as Publisher @@ -32,6 +32,8 @@ from wx.lib.pubsub import pub as Publisher
32 import random 32 import random
33 from scipy.spatial import distance 33 from scipy.spatial import distance
34 34
  35 +from scipy.misc import imsave
  36 +
35 import invesalius.constants as const 37 import invesalius.constants as const
36 import invesalius.data.bases as bases 38 import invesalius.data.bases as bases
37 import invesalius.data.transformations as tr 39 import invesalius.data.transformations as tr
@@ -51,6 +53,8 @@ else: @@ -51,6 +53,8 @@ else:
51 53
52 PROP_MEASURE = 0.8 54 PROP_MEASURE = 0.8
53 55
  56 +from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX, Polygon
  57 +
54 class Viewer(wx.Panel): 58 class Viewer(wx.Panel):
55 def __init__(self, parent): 59 def __init__(self, parent):
56 wx.Panel.__init__(self, parent, size=wx.Size(320, 320)) 60 wx.Panel.__init__(self, parent, size=wx.Size(320, 320))
@@ -86,17 +90,32 @@ class Viewer(wx.Panel): @@ -86,17 +90,32 @@ class Viewer(wx.Panel):
86 interactor.Enable(1) 90 interactor.Enable(1)
87 91
88 ren = vtk.vtkRenderer() 92 ren = vtk.vtkRenderer()
89 - interactor.GetRenderWindow().AddRenderer(ren)  
90 self.ren = ren 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 self.raycasting_volume = False 105 self.raycasting_volume = False
93 106
94 self.onclick = False 107 self.onclick = False
95 108
96 - self.text = vtku.Text() 109 + self.text = vtku.TextZero()
97 self.text.SetValue("") 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 # axes = vtk.vtkAxesActor() 119 # axes = vtk.vtkAxesActor()
101 # axes.SetXAxisLabelText('x') 120 # axes.SetXAxisLabelText('x')
102 # axes.SetYAxisLabelText('y') 121 # axes.SetYAxisLabelText('y')
@@ -105,7 +124,6 @@ class Viewer(wx.Panel): @@ -105,7 +124,6 @@ class Viewer(wx.Panel):
105 # 124 #
106 # self.ren.AddActor(axes) 125 # self.ren.AddActor(axes)
107 126
108 -  
109 self.slice_plane = None 127 self.slice_plane = None
110 128
111 self.view_angle = None 129 self.view_angle = None
@@ -1230,8 +1248,17 @@ class Viewer(wx.Panel): @@ -1230,8 +1248,17 @@ class Viewer(wx.Panel):
1230 1248
1231 def __bind_events_wx(self): 1249 def __bind_events_wx(self):
1232 #self.Bind(wx.EVT_SIZE, self.OnSize) 1250 #self.Bind(wx.EVT_SIZE, self.OnSize)
  1251 + # self.canvas.subscribe_event('LeftButtonPressEvent', self.on_insert_point)
1233 pass 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 def SetInteractorStyle(self, state): 1262 def SetInteractorStyle(self, state):
1236 action = { 1263 action = {
1237 const.STATE_PAN: 1264 const.STATE_PAN:
@@ -1304,7 +1331,7 @@ class Viewer(wx.Panel): @@ -1304,7 +1331,7 @@ class Viewer(wx.Panel):
1304 self.style = style 1331 self.style = style
1305 1332
1306 # Check each event available for each mode 1333 # Check each event available for each mode
1307 - for event in action[state]: 1334 + for event in action.get(state, []):
1308 # Bind event 1335 # Bind event
1309 style.AddObserver(event,action[state][event]) 1336 style.AddObserver(event,action[state][event])
1310 1337
@@ -1483,6 +1510,7 @@ class Viewer(wx.Panel): @@ -1483,6 +1510,7 @@ class Viewer(wx.Panel):
1483 def OnSetWindowLevelText(self, ww, wl): 1510 def OnSetWindowLevelText(self, ww, wl):
1484 if self.raycasting_volume: 1511 if self.raycasting_volume:
1485 self.text.SetValue("WL: %d WW: %d"%(wl, ww)) 1512 self.text.SetValue("WL: %d WW: %d"%(wl, ww))
  1513 + self.canvas.modified = True
1486 1514
1487 def OnShowRaycasting(self): 1515 def OnShowRaycasting(self):
1488 if not self.raycasting_volume: 1516 if not self.raycasting_volume:
invesalius/data/vtk_utils.py
@@ -95,7 +95,8 @@ def ShowProgress(number_of_filters = 1, @@ -95,7 +95,8 @@ def ShowProgress(number_of_filters = 1,
95 95
96 class Text(object): 96 class Text(object):
97 def __init__(self): 97 def __init__(self):
98 - 98 + self.layer = 99
  99 + self.children = []
99 property = vtk.vtkTextProperty() 100 property = vtk.vtkTextProperty()
100 property.SetFontSize(const.TEXT_SIZE) 101 property.SetFontSize(const.TEXT_SIZE)
101 property.SetFontFamilyToArial() 102 property.SetFontFamilyToArial()
@@ -195,7 +196,8 @@ class Text(object): @@ -195,7 +196,8 @@ class Text(object):
195 196
196 class TextZero(object): 197 class TextZero(object):
197 def __init__(self): 198 def __init__(self):
198 - 199 + self.layer = 99
  200 + self.children = []
199 property = vtk.vtkTextProperty() 201 property = vtk.vtkTextProperty()
200 property.SetFontSize(const.TEXT_SIZE_LARGE) 202 property.SetFontSize(const.TEXT_SIZE_LARGE)
201 property.SetFontFamilyToArial() 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,6 +48,8 @@ BTN_NEW, BTN_REMOVE, BTN_DUPLICATE, BTN_OPEN = [wx.NewId() for i in range(4)]
48 48
49 TYPE = {const.LINEAR: _(u"Linear"), 49 TYPE = {const.LINEAR: _(u"Linear"),
50 const.ANGULAR: _(u"Angular"), 50 const.ANGULAR: _(u"Angular"),
  51 + const.DENSITY_ELLIPSE: _(u"Density Ellipse"),
  52 + const.DENSITY_POLYGON: _(u"Density Polygon"),
51 } 53 }
52 54
53 LOCATION = {const.SURFACE: _(u"3D"), 55 LOCATION = {const.SURFACE: _(u"3D"),
@@ -1171,8 +1173,10 @@ class MeasuresListCtrlPanel(wx.ListCtrl, listmix.TextEditMixin, listmix.CheckLis @@ -1171,8 +1173,10 @@ class MeasuresListCtrlPanel(wx.ListCtrl, listmix.TextEditMixin, listmix.CheckLis
1171 location = LOCATION[m.location] 1173 location = LOCATION[m.location]
1172 if m.type == const.LINEAR: 1174 if m.type == const.LINEAR:
1173 value = (u"%.2f mm") % m.value 1175 value = (u"%.2f mm") % m.value
1174 - else: 1176 + elif m.type == const.ANGULAR:
1175 value = (u"%.2f°") % m.value 1177 value = (u"%.2f°") % m.value
  1178 + else:
  1179 + value = (u"%.3f") % m.value
1176 self.InsertNewItem(m.index, m.name, colour, location, type, value) 1180 self.InsertNewItem(m.index, m.name, colour, location, type, value)
1177 1181
1178 if not m.visible: 1182 if not m.visible:
invesalius/gui/dialogs.py
@@ -18,9 +18,13 @@ @@ -18,9 +18,13 @@
18 # detalhes. 18 # detalhes.
19 #-------------------------------------------------------------------------- 19 #--------------------------------------------------------------------------
20 20
  21 +import itertools
21 import os 22 import os
22 import random 23 import random
23 import sys 24 import sys
  25 +import time
  26 +
  27 +from concurrent import futures
24 28
25 if sys.platform == 'win32': 29 if sys.platform == 'win32':
26 try: 30 try:
@@ -3317,6 +3321,112 @@ class FillHolesAutoDialog(wx.Dialog): @@ -3317,6 +3321,112 @@ class FillHolesAutoDialog(wx.Dialog):
3317 self.panel2dcon.Enable(0) 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 class ObjectCalibrationDialog(wx.Dialog): 3430 class ObjectCalibrationDialog(wx.Dialog):
3321 3431
3322 def __init__(self, nav_prop): 3432 def __init__(self, nav_prop):
invesalius/gui/frame.py
@@ -472,6 +472,10 @@ class Frame(wx.Frame): @@ -472,6 +472,10 @@ class Frame(wx.Frame):
472 elif id == const.ID_REORIENT_IMG: 472 elif id == const.ID_REORIENT_IMG:
473 self.OnReorientImg() 473 self.OnReorientImg()
474 474
  475 + elif id == const.ID_MASK_DENSITY_MEASURE:
  476 + ddlg = dlg.MaskDensityDialog(self)
  477 + ddlg.Show()
  478 +
475 elif id == const.ID_THRESHOLD_SEGMENTATION: 479 elif id == const.ID_THRESHOLD_SEGMENTATION:
476 Publisher.sendMessage("Show panel", panel_id=const.ID_THRESHOLD_SEGMENTATION) 480 Publisher.sendMessage("Show panel", panel_id=const.ID_THRESHOLD_SEGMENTATION)
477 Publisher.sendMessage('Disable actual style') 481 Publisher.sendMessage('Disable actual style')
@@ -767,6 +771,7 @@ class MenuBar(wx.MenuBar): @@ -767,6 +771,7 @@ class MenuBar(wx.MenuBar):
767 const.ID_WATERSHED_SEGMENTATION, 771 const.ID_WATERSHED_SEGMENTATION,
768 const.ID_THRESHOLD_SEGMENTATION, 772 const.ID_THRESHOLD_SEGMENTATION,
769 const.ID_FLOODFILL_SEGMENTATION, 773 const.ID_FLOODFILL_SEGMENTATION,
  774 + const.ID_MASK_DENSITY_MEASURE,
770 const.ID_CREATE_SURFACE, 775 const.ID_CREATE_SURFACE,
771 const.ID_CREATE_MASK, 776 const.ID_CREATE_MASK,
772 const.ID_GOTO_SLICE] 777 const.ID_GOTO_SLICE]
@@ -923,6 +928,7 @@ class MenuBar(wx.MenuBar): @@ -923,6 +928,7 @@ class MenuBar(wx.MenuBar):
923 image_menu.AppendMenu(wx.NewId(), _('Flip'), flip_menu) 928 image_menu.AppendMenu(wx.NewId(), _('Flip'), flip_menu)
924 image_menu.AppendMenu(wx.NewId(), _('Swap axes'), swap_axes_menu) 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 reorient_menu = image_menu.Append(const.ID_REORIENT_IMG, _(u'Reorient image\tCtrl+Shift+R')) 932 reorient_menu = image_menu.Append(const.ID_REORIENT_IMG, _(u'Reorient image\tCtrl+Shift+R'))
927 933
928 reorient_menu.Enable(False) 934 reorient_menu.Enable(False)
@@ -931,9 +937,7 @@ class MenuBar(wx.MenuBar): @@ -931,9 +937,7 @@ class MenuBar(wx.MenuBar):
931 tools_menu.AppendMenu(-1, _(u"Segmentation"), segmentation_menu) 937 tools_menu.AppendMenu(-1, _(u"Segmentation"), segmentation_menu)
932 tools_menu.AppendMenu(-1, _(u"Surface"), surface_menu) 938 tools_menu.AppendMenu(-1, _(u"Surface"), surface_menu)
933 939
934 -  
935 #View 940 #View
936 -  
937 self.view_menu = view_menu = wx.Menu() 941 self.view_menu = view_menu = wx.Menu()
938 view_menu.Append(const.ID_VIEW_INTERPOLATED, _(u'Interpolated slices'), "", wx.ITEM_CHECK) 942 view_menu.Append(const.ID_VIEW_INTERPOLATED, _(u'Interpolated slices'), "", wx.ITEM_CHECK)
939 943
@@ -1378,8 +1382,11 @@ class ObjectToolBar(AuiToolBar): @@ -1378,8 +1382,11 @@ class ObjectToolBar(AuiToolBar):
1378 const.STATE_SPIN, const.STATE_ZOOM_SL, 1382 const.STATE_SPIN, const.STATE_ZOOM_SL,
1379 const.STATE_ZOOM, 1383 const.STATE_ZOOM,
1380 const.STATE_MEASURE_DISTANCE, 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 self.__init_items() 1390 self.__init_items()
1384 self.__bind_events() 1391 self.__bind_events()
1385 self.__bind_events_wx() 1392 self.__bind_events_wx()
@@ -1431,6 +1438,10 @@ class ObjectToolBar(AuiToolBar): @@ -1431,6 +1438,10 @@ class ObjectToolBar(AuiToolBar):
1431 path = os.path.join(d, "measure_angle_original.png") 1438 path = os.path.join(d, "measure_angle_original.png")
1432 BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) 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 #path = os.path.join(d, "tool_annotation_original.png") 1445 #path = os.path.join(d, "tool_annotation_original.png")
1435 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) 1446 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1436 1447
@@ -1456,6 +1467,10 @@ class ObjectToolBar(AuiToolBar): @@ -1456,6 +1467,10 @@ class ObjectToolBar(AuiToolBar):
1456 path = os.path.join(d, "measure_angle.png") 1467 path = os.path.join(d, "measure_angle.png")
1457 BMP_ANGLE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) 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 #path = os.path.join(d, "tool_annotation.png") 1474 #path = os.path.join(d, "tool_annotation.png")
1460 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG) 1475 #BMP_ANNOTATE = wx.Bitmap(path, wx.BITMAP_TYPE_PNG)
1461 1476
@@ -1502,6 +1517,20 @@ class ObjectToolBar(AuiToolBar): @@ -1502,6 +1517,20 @@ class ObjectToolBar(AuiToolBar):
1502 wx.NullBitmap, 1517 wx.NullBitmap,
1503 short_help_string = _("Measure angle"), 1518 short_help_string = _("Measure angle"),
1504 kind = wx.ITEM_CHECK) 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 #self.AddLabelTool(const.STATE_ANNOTATE, 1534 #self.AddLabelTool(const.STATE_ANNOTATE,
1506 # "", 1535 # "",
1507 # shortHelp = _("Add annotation"), 1536 # shortHelp = _("Add annotation"),
invesalius/gui/widgets/canvas_renderer.py 0 → 100644
@@ -0,0 +1,1233 @@ @@ -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 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 2
3 import math 3 import math
4 -import numpy 4 +import numpy as np
5 5
6 def calculate_distance(p1, p2): 6 def calculate_distance(p1, p2):
7 """ 7 """
@@ -15,20 +15,67 @@ def calculate_distance(p1, p2): @@ -15,20 +15,67 @@ def calculate_distance(p1, p2):
15 """ 15 """
16 return math.sqrt(sum([(j-i)**2 for i,j in zip(p1, p2)])) 16 return math.sqrt(sum([(j-i)**2 for i,j in zip(p1, p2)]))
17 17
  18 +
18 def calculate_angle(v1, v2): 19 def calculate_angle(v1, v2):
19 """ 20 """
20 Calculates the angle formed between vector v1 and v2. 21 Calculates the angle formed between vector v1 and v2.
21 22
22 >>> calculate_angle((0, 1), (1, 0)) 23 >>> calculate_angle((0, 1), (1, 0))
23 90.0 24 90.0
24 - 25 +
25 >>> calculate_angle((1, 0), (0, 1)) 26 >>> calculate_angle((1, 0), (0, 1))
26 90.0 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 angle = math.degrees(math.acos(cos_)) 30 angle = math.degrees(math.acos(cos_))
30 return angle 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 if __name__ == '__main__': 79 if __name__ == '__main__':
33 import doctest 80 import doctest
34 doctest.testmod() 81 doctest.testmod()
invesalius/project.py
@@ -192,17 +192,7 @@ class Project(with_metaclass(Singleton, object)): @@ -192,17 +192,7 @@ class Project(with_metaclass(Singleton, object)):
192 d = self.measurement_dict 192 d = self.measurement_dict
193 for i in d: 193 for i in d:
194 m = d[i] 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 return measures 196 return measures
207 197
208 def SavePlistProject(self, dir_, filename, compress=False): 198 def SavePlistProject(self, dir_, filename, compress=False):
@@ -346,7 +336,10 @@ class Project(with_metaclass(Singleton, object)): @@ -346,7 +336,10 @@ class Project(with_metaclass(Singleton, object)):
346 measurements = plistlib.readPlist(os.path.join(dirpath, 336 measurements = plistlib.readPlist(os.path.join(dirpath,
347 project["measurements"])) 337 project["measurements"]))
348 for index in measurements: 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 measure.Load(measurements[index]) 343 measure.Load(measurements[index])
351 self.measurement_dict[int(index)] = measure 344 self.measurement_dict[int(index)] = measure
352 345