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 | 73 | invesalius/constants.py -text |
74 | 74 | invesalius/control.py -text |
75 | 75 | invesalius/data/__init__.py -text |
76 | +invesalius/data/cursor_actors.py -text | |
76 | 77 | invesalius/data/editor.py -text |
77 | 78 | invesalius/data/imagedata_utils.py -text |
78 | 79 | invesalius/data/mask.py -text | ... | ... |
... | ... | @@ -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 | 33 | 'Update cursor position in slice') |
34 | 34 | ps.Publisher().subscribe(self.ShowMask, 'Show mask') |
35 | 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 | 43 | def ChangeMaskName(self, pubsub_evt): |
38 | 44 | index, name = pubsub_evt.data |
... | ... | @@ -118,6 +124,8 @@ class Slice(object): |
118 | 124 | ps.Publisher().sendMessage('Select mask name in combo', mask_index) |
119 | 125 | ps.Publisher().sendMessage('Update slice viewer') |
120 | 126 | |
127 | + | |
128 | + | |
121 | 129 | def ChangeCurrentMaskColour(self, colour, update=True): |
122 | 130 | # This is necessary because wx events are calling this before it was created |
123 | 131 | if self.current_mask: |
... | ... | @@ -358,27 +366,31 @@ class Slice(object): |
358 | 366 | # thresh_min, thresh_max = evt.data |
359 | 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 | 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 | 379 | def DrawPixel(self, x, y, z, colour=None): |
370 | 380 | """ |
371 | 381 | Draw pixel, based on x, y and z position coordinates. |
372 | 382 | """ |
383 | + imagedata = self.current_mask.imagedata | |
373 | 384 | if colour is None: |
374 | 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 | 388 | def EditPixelBasedOnThreshold(self, x, y, z): |
378 | 389 | """ |
379 | 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 | 394 | thresh_min, thresh_max = self.current_mask.edition_threshold_range |
383 | 395 | |
384 | 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 | 26 | import data.slice_ as sl |
27 | 27 | import constants as const |
28 | 28 | import project |
29 | +import cursor_actors as ca | |
29 | 30 | |
30 | 31 | class Viewer(wx.Panel): |
31 | 32 | |
... | ... | @@ -36,7 +37,7 @@ class Viewer(wx.Panel): |
36 | 37 | self.SetBackgroundColour(colour) |
37 | 38 | |
38 | 39 | # Interactor aditional style |
39 | - self.modes = [] | |
40 | + self.modes = ['DEFAULT'] | |
40 | 41 | self.mouse_pressed = 0 |
41 | 42 | |
42 | 43 | self.__init_gui() |
... | ... | @@ -83,7 +84,7 @@ class Viewer(wx.Panel): |
83 | 84 | self.cam = ren.GetActiveCamera() |
84 | 85 | self.ren = ren |
85 | 86 | |
86 | - self.AppendMode('DEFAULT') | |
87 | + self.AppendMode('EDITOR') | |
87 | 88 | |
88 | 89 | def AppendMode(self, mode): |
89 | 90 | |
... | ... | @@ -115,6 +116,19 @@ class Viewer(wx.Panel): |
115 | 116 | # Bind event |
116 | 117 | style.AddObserver(event, |
117 | 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 | 134 | def OnMouseClick(self, obj, evt_vtk): |
... | ... | @@ -129,9 +143,15 @@ class Viewer(wx.Panel): |
129 | 143 | print "Edit pixel region based on origin:", coord |
130 | 144 | |
131 | 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 | 149 | if self.mouse_pressed: |
134 | 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 | 156 | def OnCrossMove(self, obj, evt_vtk): |
137 | 157 | coord = self.GetCoordinate() |
... | ... | @@ -187,6 +207,48 @@ class Viewer(wx.Panel): |
187 | 207 | #print "New coordinate: ", coord |
188 | 208 | |
189 | 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 | 253 | def __bind_events(self): |
192 | 254 | ps.Publisher().subscribe(self.LoadImagedata, 'Load slice to viewer') | ... | ... |