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