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