Commit 595672743516cd0b26e1df1e9ccc4496323e8587
1 parent
159f813a
Exists in
master
and in
68 other branches
ADD: Edit mask pixel tool (erase) in default mode - not working
Showing
4 changed files
with
195 additions
and
9 deletions
Show diff stats
.gitattributes
@@ -73,6 +73,7 @@ invesalius/.svnignore -text | @@ -73,6 +73,7 @@ invesalius/.svnignore -text | ||
73 | invesalius/constants.py -text | 73 | invesalius/constants.py -text |
74 | invesalius/control.py -text | 74 | invesalius/control.py -text |
75 | invesalius/data/__init__.py -text | 75 | invesalius/data/__init__.py -text |
76 | +invesalius/data/cursor_actors.py -text | ||
76 | invesalius/data/editor.py -text | 77 | invesalius/data/editor.py -text |
77 | invesalius/data/imagedata_utils.py -text | 78 | invesalius/data/imagedata_utils.py -text |
78 | invesalius/data/mask.py -text | 79 | invesalius/data/mask.py -text |
@@ -0,0 +1,111 @@ | @@ -0,0 +1,111 @@ | ||
1 | +from math import * | ||
2 | + | ||
3 | +import vtk | ||
4 | + | ||
5 | +class CursorCircle: | ||
6 | + # TODO: Think and try to change this class to an actor | ||
7 | + # CursorCircleActor(vtk.vtkActor) | ||
8 | + | ||
9 | + def __init__(self): | ||
10 | + | ||
11 | + self.colour = (0.0, 0.0, 1.0) | ||
12 | + self.opacity = 1 | ||
13 | + self.radius = 20 | ||
14 | + self.position = (0 ,0, 1) | ||
15 | + self.points = [] | ||
16 | + self.orientation = "AXIAL" | ||
17 | + | ||
18 | + self.mapper = vtk.vtkPolyDataMapper() | ||
19 | + self.disk = vtk.vtkDiskSource() | ||
20 | + self.actor = vtk.vtkActor() | ||
21 | + | ||
22 | + self.__build_actor() | ||
23 | + self.__calculate_area_pixels() | ||
24 | + | ||
25 | + def __build_actor(self): | ||
26 | + """ | ||
27 | + Function to plot the circle | ||
28 | + """ | ||
29 | + | ||
30 | + disk = self.disk | ||
31 | + disk.SetInnerRadius(self.radius) | ||
32 | + disk.SetOuterRadius(0) # filled | ||
33 | + disk.SetRadialResolution(50) | ||
34 | + disk.SetCircumferentialResolution(50) | ||
35 | + | ||
36 | + mapper = self.mapper | ||
37 | + mapper.SetInput(disk.GetOutput()) | ||
38 | + | ||
39 | + actor = self.actor | ||
40 | + actor.SetMapper(mapper) | ||
41 | + actor.GetProperty().SetOpacity(self.opacity) | ||
42 | + actor.GetProperty().SetColor(self.colour) | ||
43 | + actor.SetPosition(self.position) | ||
44 | + actor.SetVisibility(1) | ||
45 | + actor.PickableOff() | ||
46 | + | ||
47 | + def __calculate_area_pixels(self): | ||
48 | + """ | ||
49 | + Return the cursor's pixels. | ||
50 | + This method scans the circle line by line. | ||
51 | + Extracted equation. | ||
52 | + http://www.mathopenref.com/chord.html | ||
53 | + """ | ||
54 | + xc = 0 | ||
55 | + yc = 0 | ||
56 | + z = 0 | ||
57 | + self.pixel_list = [] | ||
58 | + radius = int(self.radius) | ||
59 | + for i in xrange(int(yc - radius), int(yc + radius)): | ||
60 | + # distance from the line to the circle's center | ||
61 | + d = yc - i | ||
62 | + # line size | ||
63 | + line = sqrt(round(radius ** 2) - round(d ** 2)) * 2 | ||
64 | + # line initial x | ||
65 | + xi = int(xc - line/2) | ||
66 | + # line final | ||
67 | + xf = int(line/2 + xc) | ||
68 | + yi = i | ||
69 | + for k in xrange(xi,xf): | ||
70 | + self.pixel_list.append((k, yi)) | ||
71 | + | ||
72 | + def SetSize(self, radius): | ||
73 | + self.radius = radius | ||
74 | + disk.SetInnerRadius(radius) | ||
75 | + self.__calculate_area_pixels() | ||
76 | + | ||
77 | + def SetColour(self, colour): | ||
78 | + self.actor.GetProperty().SetColor(self.colour) | ||
79 | + | ||
80 | + def SetOrientation(self, orientation): | ||
81 | + self.orientation = orientation | ||
82 | + | ||
83 | + if orientation == "CORONAL": | ||
84 | + self.actor.RotateX(90) | ||
85 | + | ||
86 | + if orientation == "SAGITAL": | ||
87 | + self.actor.RotateY(90) | ||
88 | + | ||
89 | + def SetPosition(self, position): | ||
90 | + | ||
91 | + #if self.orientation == "AXIAL": | ||
92 | + # z = 1 | ||
93 | + #elif self.orientation == "CORONAL": | ||
94 | + # y = 1 | ||
95 | + #elif self.orientation == "SAGITAL": | ||
96 | + # x = 1 | ||
97 | + self.position = position | ||
98 | + self.actor.SetPosition(position) | ||
99 | + | ||
100 | + def GetPixels(self): | ||
101 | + px, py, pz = self.position | ||
102 | + orient = self.orientation | ||
103 | + for pixel_0,pixel_1 in self.pixel_list: | ||
104 | + # The position of the pixels in this list is relative (based only on | ||
105 | + # the area, and not the cursor position). | ||
106 | + # Let's calculate the absolute position | ||
107 | + # TODO: Optimize this!!!! | ||
108 | + absolute_pixel = {"AXIAL": (px + pixel_0, py + pixel_1, pz), | ||
109 | + "CORONAL": (px + pixel_0, py, pz + pixel_1), | ||
110 | + "SAGITAL": (px, py + pixel_0, pz + pixel_1)} | ||
111 | + yield absolute_pixel[orient] |
invesalius/data/slice_.py
@@ -33,6 +33,12 @@ class Slice(object): | @@ -33,6 +33,12 @@ class Slice(object): | ||
33 | 'Update cursor position in slice') | 33 | 'Update cursor position in slice') |
34 | ps.Publisher().subscribe(self.ShowMask, 'Show mask') | 34 | ps.Publisher().subscribe(self.ShowMask, 'Show mask') |
35 | ps.Publisher().subscribe(self.ChangeMaskName, 'Change mask name') | 35 | ps.Publisher().subscribe(self.ChangeMaskName, 'Change mask name') |
36 | + ps.Publisher().subscribe(self.EraseMaskPixel, 'Erase mask pixel') | ||
37 | + | ||
38 | + | ||
39 | + def EraseMaskPixel(self, pubsub_evt): | ||
40 | + position = pubsub_evt.data | ||
41 | + self.ErasePixel(position) | ||
36 | 42 | ||
37 | def ChangeMaskName(self, pubsub_evt): | 43 | def ChangeMaskName(self, pubsub_evt): |
38 | index, name = pubsub_evt.data | 44 | index, name = pubsub_evt.data |
@@ -118,6 +124,8 @@ class Slice(object): | @@ -118,6 +124,8 @@ class Slice(object): | ||
118 | ps.Publisher().sendMessage('Select mask name in combo', mask_index) | 124 | ps.Publisher().sendMessage('Select mask name in combo', mask_index) |
119 | ps.Publisher().sendMessage('Update slice viewer') | 125 | ps.Publisher().sendMessage('Update slice viewer') |
120 | 126 | ||
127 | + | ||
128 | + | ||
121 | def ChangeCurrentMaskColour(self, colour, update=True): | 129 | def ChangeCurrentMaskColour(self, colour, update=True): |
122 | # This is necessary because wx events are calling this before it was created | 130 | # This is necessary because wx events are calling this before it was created |
123 | if self.current_mask: | 131 | if self.current_mask: |
@@ -358,27 +366,31 @@ class Slice(object): | @@ -358,27 +366,31 @@ class Slice(object): | ||
358 | # thresh_min, thresh_max = evt.data | 366 | # thresh_min, thresh_max = evt.data |
359 | # self.current_mask.edition_threshold_range = thresh_min, thresh_max | 367 | # self.current_mask.edition_threshold_range = thresh_min, thresh_max |
360 | 368 | ||
361 | - def ErasePixel(self, x, y, z): | 369 | + def ErasePixel(self, position): |
362 | """ | 370 | """ |
363 | Delete pixel, based on x, y and z position coordinates. | 371 | Delete pixel, based on x, y and z position coordinates. |
364 | """ | 372 | """ |
365 | - colour = imagedata.GetScalarRange()[0] | ||
366 | - self.imagedata.SetScalarComponentFromDouble(x, y, z, 0, colour) | ||
367 | - self.imagedata.Update() | 373 | + x, y, z = position |
374 | + imagedata = self.current_mask.imagedata | ||
375 | + colour = imagedata.GetScalarRange()[0]# - 1 # Important to effect erase | ||
376 | + imagedata.SetScalarComponentFromDouble(x, y, z, 0, colour) | ||
377 | + imagedata.Update() | ||
368 | 378 | ||
369 | def DrawPixel(self, x, y, z, colour=None): | 379 | def DrawPixel(self, x, y, z, colour=None): |
370 | """ | 380 | """ |
371 | Draw pixel, based on x, y and z position coordinates. | 381 | Draw pixel, based on x, y and z position coordinates. |
372 | """ | 382 | """ |
383 | + imagedata = self.current_mask.imagedata | ||
373 | if colour is None: | 384 | if colour is None: |
374 | colour = imagedata.GetScalarRange()[1] | 385 | colour = imagedata.GetScalarRange()[1] |
375 | - self.imagedata.SetScalarComponentFromDouble(x, y, z, 0, colour) | 386 | + imagedata.SetScalarComponentFromDouble(x, y, z, 0, colour) |
376 | 387 | ||
377 | def EditPixelBasedOnThreshold(self, x, y, z): | 388 | def EditPixelBasedOnThreshold(self, x, y, z): |
378 | """ | 389 | """ |
379 | Erase or draw pixel based on edition threshold range. | 390 | Erase or draw pixel based on edition threshold range. |
380 | """ | 391 | """ |
381 | - pixel_colour = self.imagedata.GetScalarComponentAsDouble(x, y, z, 0) | 392 | + |
393 | + pixel_colour = imagedata.GetScalarComponentAsDouble(x, y, z, 0) | ||
382 | thresh_min, thresh_max = self.current_mask.edition_threshold_range | 394 | thresh_min, thresh_max = self.current_mask.edition_threshold_range |
383 | 395 | ||
384 | if (pixel_colour >= thresh_min) and (pixel_colour <= thresh_max): | 396 | if (pixel_colour >= thresh_min) and (pixel_colour <= thresh_max): |
invesalius/data/viewer_slice.py
@@ -26,6 +26,7 @@ import wx.lib.pubsub as ps | @@ -26,6 +26,7 @@ import wx.lib.pubsub as ps | ||
26 | import data.slice_ as sl | 26 | import data.slice_ as sl |
27 | import constants as const | 27 | import constants as const |
28 | import project | 28 | import project |
29 | +import cursor_actors as ca | ||
29 | 30 | ||
30 | class Viewer(wx.Panel): | 31 | class Viewer(wx.Panel): |
31 | 32 | ||
@@ -36,7 +37,7 @@ class Viewer(wx.Panel): | @@ -36,7 +37,7 @@ class Viewer(wx.Panel): | ||
36 | self.SetBackgroundColour(colour) | 37 | self.SetBackgroundColour(colour) |
37 | 38 | ||
38 | # Interactor aditional style | 39 | # Interactor aditional style |
39 | - self.modes = [] | 40 | + self.modes = ['DEFAULT'] |
40 | self.mouse_pressed = 0 | 41 | self.mouse_pressed = 0 |
41 | 42 | ||
42 | self.__init_gui() | 43 | self.__init_gui() |
@@ -83,7 +84,7 @@ class Viewer(wx.Panel): | @@ -83,7 +84,7 @@ class Viewer(wx.Panel): | ||
83 | self.cam = ren.GetActiveCamera() | 84 | self.cam = ren.GetActiveCamera() |
84 | self.ren = ren | 85 | self.ren = ren |
85 | 86 | ||
86 | - self.AppendMode('DEFAULT') | 87 | + self.AppendMode('EDITOR') |
87 | 88 | ||
88 | def AppendMode(self, mode): | 89 | def AppendMode(self, mode): |
89 | 90 | ||
@@ -115,6 +116,19 @@ class Viewer(wx.Panel): | @@ -115,6 +116,19 @@ class Viewer(wx.Panel): | ||
115 | # Bind event | 116 | # Bind event |
116 | style.AddObserver(event, | 117 | style.AddObserver(event, |
117 | action[mode][event]) | 118 | action[mode][event]) |
119 | + | ||
120 | + # Insert cursor | ||
121 | + cursor = ca.CursorCircle() | ||
122 | + cursor.SetOrientation(self.orientation) | ||
123 | + coordinates = {"SAGITAL": [self.slice_number, 0, 0], | ||
124 | + "CORONAL": [0, self.slice_number, 0], | ||
125 | + "AXIAL": [0, 0, self.slice_number]} | ||
126 | + cursor.SetPosition(coordinates[self.orientation]) | ||
127 | + self.ren.AddActor(cursor.actor) | ||
128 | + self.ren.Render() | ||
129 | + | ||
130 | + self.cursor = cursor | ||
131 | + | ||
118 | 132 | ||
119 | 133 | ||
120 | def OnMouseClick(self, obj, evt_vtk): | 134 | def OnMouseClick(self, obj, evt_vtk): |
@@ -129,9 +143,15 @@ class Viewer(wx.Panel): | @@ -129,9 +143,15 @@ class Viewer(wx.Panel): | ||
129 | print "Edit pixel region based on origin:", coord | 143 | print "Edit pixel region based on origin:", coord |
130 | 144 | ||
131 | def OnBrushMove(self, obj, evt_vtk): | 145 | def OnBrushMove(self, obj, evt_vtk): |
132 | - coord = self.GetCoordinate() | 146 | + coord = self.GetCoordinateCursor() |
147 | + self.cursor.SetPosition(coord) | ||
148 | + self.ren.Render() | ||
133 | if self.mouse_pressed: | 149 | if self.mouse_pressed: |
134 | print "Edit pixel region based on origin:", coord | 150 | print "Edit pixel region based on origin:", coord |
151 | + pixels = self.cursor.GetPixels() | ||
152 | + for coord in pixels: | ||
153 | + ps.Publisher().sendMessage('Erase mask pixel', coord) | ||
154 | + self.interactor.Render() | ||
135 | 155 | ||
136 | def OnCrossMove(self, obj, evt_vtk): | 156 | def OnCrossMove(self, obj, evt_vtk): |
137 | coord = self.GetCoordinate() | 157 | coord = self.GetCoordinate() |
@@ -187,6 +207,48 @@ class Viewer(wx.Panel): | @@ -187,6 +207,48 @@ class Viewer(wx.Panel): | ||
187 | #print "New coordinate: ", coord | 207 | #print "New coordinate: ", coord |
188 | 208 | ||
189 | return coord | 209 | return coord |
210 | + | ||
211 | + | ||
212 | + def GetCoordinateCursor(self): | ||
213 | + | ||
214 | + # Find position | ||
215 | + mouse_x, mouse_y = self.interactor.GetEventPosition() | ||
216 | + self.pick.Pick(mouse_x, mouse_y, 0, self.ren) | ||
217 | + x, y, z = self.pick.GetPickPosition() | ||
218 | + | ||
219 | + # First we fix the position origin, based on vtkActor bounds | ||
220 | + bounds = self.actor.GetBounds() | ||
221 | + bound_xi, bound_xf, bound_yi, bound_yf, bound_zi, bound_zf = bounds | ||
222 | + x = float(x - bound_xi) | ||
223 | + y = float(y - bound_yi) | ||
224 | + z = float(z - bound_zi) | ||
225 | + | ||
226 | + # Then we fix the porpotion, based on vtkImageData spacing | ||
227 | + #spacing_x, spacing_y, spacing_z = self.imagedata.GetSpacing() | ||
228 | + #x = x/spacing_x | ||
229 | + #y = y/spacing_y | ||
230 | + #z = z/spacing_z | ||
231 | + | ||
232 | + # Based on the current orientation, we define 3D position | ||
233 | + coordinates = {"SAGITAL": [self.slice_number, y, z], | ||
234 | + "CORONAL": [x, self.slice_number, z], | ||
235 | + "AXIAL": [x, y, self.slice_number]} | ||
236 | + coord = [int(coord) for coord in coordinates[self.orientation]] | ||
237 | + | ||
238 | + # According to vtkImageData extent, we limit min and max value | ||
239 | + # If this is not done, a VTK Error occurs when mouse is pressed outside | ||
240 | + # vtkImageData extent | ||
241 | + #extent = self.imagedata.GetWholeExtent() | ||
242 | + #extent_min = extent[0], extent[2], extent[4] | ||
243 | + #extent_max = extent[1], extent[3], extent[5] | ||
244 | + #for index in xrange(3): | ||
245 | + # if coord[index] > extent_max[index]: | ||
246 | + # coord[index] = extent_max[index] | ||
247 | + # elif coord[index] < extent_min[index]: | ||
248 | + # coord[index] = extent_min[index] | ||
249 | + #print "New coordinate: ", coord | ||
250 | + | ||
251 | + return coord | ||
190 | 252 | ||
191 | def __bind_events(self): | 253 | def __bind_events(self): |
192 | ps.Publisher().subscribe(self.LoadImagedata, 'Load slice to viewer') | 254 | ps.Publisher().subscribe(self.LoadImagedata, 'Load slice to viewer') |