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') | ... | ... |