From a2350755deb300f540a1088388803108d1b714f1 Mon Sep 17 00:00:00 2001 From: tfmoraes Date: Tue, 1 Nov 2011 13:25:45 +0000 Subject: [PATCH] Improved cursor_actors, better handlings and more accurate --- invesalius/data/cursor_actors.py | 472 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- invesalius/data/imagedata_utils.py | 2 -- invesalius/data/slice_.py | 24 ++++++++++++++++-------- invesalius/data/slice_data.py | 7 ++++--- invesalius/data/viewer_slice.py | 29 ++++++++++++++++++++--------- 5 files changed, 336 insertions(+), 198 deletions(-) diff --git a/invesalius/data/cursor_actors.py b/invesalius/data/cursor_actors.py index 6d1ed94..7fd9f93 100644 --- a/invesalius/data/cursor_actors.py +++ b/invesalius/data/cursor_actors.py @@ -17,127 +17,174 @@ # detalhes. #-------------------------------------------------------------------------- -from math import * +import math import numpy import vtk -import wx.lib.pubsub as ps +import imagedata_utils from project import Project import constants as const -import utils -class CursorCircle: - # TODO: Think and try to change this class to an actor - # CursorCircleActor(vtk.vtkActor) +from vtk.util import numpy_support + +ORIENTATION = {'AXIAL': 2, + 'CORONAL': 1, + 'SAGITAL': 0} + +def to_vtk(n_array, spacing, slice_number, orientation): + """ + It transforms a numpy array into a vtkImageData. + """ + # TODO Merge this function with imagedata_utils.to_vtk to eliminate + # duplicated code + try: + dz, dy, dx = n_array.shape + except ValueError: + dy, dx = n_array.shape + dz = 1 + + v_image = numpy_support.numpy_to_vtk(n_array.flat) + + if orientation == 'AXIAL': + extent = (0, dx -1, 0, dy -1, slice_number, slice_number + dz - 1) + elif orientation == 'SAGITAL': + extent = (slice_number, slice_number + dx - 1, 0, dy - 1, 0, dz - 1) + elif orientation == 'CORONAL': + extent = (0, dx - 1, slice_number, slice_number + dy - 1, 0, dz - 1) + + image = vtk.vtkImageData() + image.SetOrigin(0, 0, 0) + image.SetSpacing(spacing) + image.SetNumberOfScalarComponents(1) + image.SetDimensions(dx, dy, dz) + image.SetExtent(extent) + image.SetScalarType(numpy_support.get_vtk_array_type(n_array.dtype)) + image.AllocateScalars() + image.Update() + image.GetCellData().SetScalars(v_image) + image.GetPointData().SetScalars(v_image) + image.Update() + + image_copy = vtk.vtkImageData() + image_copy.DeepCopy(image) + image_copy.Update() + + return image_copy + +class CursorBase(object): def __init__(self): self.colour = (0.0, 0.0, 1.0) self.opacity = 1 - self.radius = 15.0 - #self.position = (0.5,0.5, 1) - self.points = [] + self.size = 15.0 self.orientation = "AXIAL" self.spacing = (1, 1, 1) - - self.mapper = vtk.vtkPolyDataMapper() - self.actor = vtk.vtkActor() - self.property = vtk.vtkProperty() - - self.__build_actor() - self.__calculate_area_pixels() - - def __build_actor(self): - """ - Function to plot the circle - """ - print "Building circle cursor", self.orientation - r = self.radius - t = 0 - - self.posc_a = 0 - self.posc_b = 0 - - self.segment = vtk.vtkAppendPolyData() - - self.xa = self.posc_a + r * cos(t) - self.ya = self.posc_a + r * sin(t) - - while(t <= 2 * pi): - self.GenerateCicleSegment(t) - t = t + 0.05 - - self.GenerateCicleSegment(0) - - self.mapper.SetInputConnection(self.segment.GetOutputPort()) - self.actor.SetMapper(self.mapper) - self.actor.GetProperty().SetOpacity(self.opacity) - self.actor.GetProperty().SetColor(self.colour) - self.actor.PickableOff() - - def GenerateCicleSegment(self, t): - """ - Generate cicle segment - """ - x = self.posc_a + self.radius * cos(t) - y = self.posc_b + self.radius * sin(t) - - ls = vtk.vtkLineSource() - ls.SetPoint1(self.xa, self.ya, 0) - ls.SetPoint2(x, y, 0) - - self.segment.AddInput(ls.GetOutput()) - self.xa, self.ya = x, y - - def __calculate_area_pixels(self): - """ - Return the cursor's pixels. - """ - radius = self.radius - if self.orientation == 'AXIAL': - sx = self.spacing[0] - sy = self.spacing[1] - elif self.orientation == 'CORONAL': - sx = self.spacing[0] - sy = self.spacing[2] - elif self.orientation == 'SAGITAL': - sx = self.spacing[1] - sy = self.spacing[2] - - y,x = numpy.ogrid[-radius/sy:+radius/sy, - -radius/sx:+radius/sx] - - index = (y*sy)**2 + (x*sx)**2 <= radius**2 - self.points = index + if vtk.vtkVersion().GetVTKVersion() > '5.8.0': + self.mapper = vtk.vtkImageSliceMapper() + cursor_property = vtk.vtkImageProperty() + cursor_property.SetInterpolationTypeToNearest() + self.actor = vtk.vtkImageSlice() + self.actor.SetMapper(self.mapper) + self.actor.SetProperty(cursor_property) + else: + self.actor = vtk.vtkImageActor() + self.mapper = None + self._build_actor() + self._calculate_area_pixels() def SetSize(self, diameter): - radius = self.radius = diameter/2.0 - #self.disk.SetInnerRadius(radius-1) # filled = self.radius - #self.disk.SetOuterRadius(radius) # filled = 0 - self.__build_actor() - self.__calculate_area_pixels() + self.radius = diameter/2.0 + self._build_actor() + self._calculate_area_pixels() def SetColour(self, colour): self.colour = colour - self.actor.GetProperty().SetColor(colour) + self._build_actor() def SetOrientation(self, orientation): self.orientation = orientation - proj = Project() - orig_orien = proj.original_orientation - if orientation == "CORONAL": - self.actor.RotateX(90) - if orientation == "SAGITAL": - self.actor.RotateY(90) + self._build_actor() + self._calculate_area_pixels() def SetPosition(self, position): - self.position = position - self.actor.SetPosition(position) + # Overriding SetPosition method because in rectangles with odd + # dimensions there is no half position. + px, py, pz = position + sx, sy, sz = self.spacing + tx = self.actor.GetXRange()[1] - self.actor.GetXRange()[0] + ty = self.actor.GetYRange()[1] - self.actor.GetYRange()[0] + tz = self.actor.GetZRange()[1] - self.actor.GetZRange()[0] + + if self.orientation == 'AXIAL': + if self.points.shape[0] % 2: + y = py - ty / 2.0 + else: + y = py - ty / 2.0 + self.spacing[1] / 2.0 + + if self.points.shape[1] % 2: + x = px - tx / 2.0 + else: + x = px - tx / 2.0 + self.spacing[0] / 2.0 + z = pz + + if self.mapper: + x += sx / 2.0 + y += sy / 2.0 + + elif self.orientation == 'CORONAL': + if self.points.shape[0] % 2: + z = pz - tz / 2.0 + else: + z = pz - tz / 2.0 + self.spacing[2] / 2.0 + + if self.points.shape[1] % 2: + x = px - tx / 2.0 + else: + x = px - tx / 2.0 + self.spacing[0] / 2.0 + y = py + + if self.mapper: + x += sx / 2.0 + z += sz / 2.0 - def SetEditionPosition(self, position): - self.edition_position = position + elif self.orientation == 'SAGITAL': + # height shape is odd + if self.points.shape[1] % 2: + y = py - ty / 2.0 + else: + y = py - ty / 2.0 + self.spacing[1] / 2.0 + + if self.points.shape[0] % 2: + z = pz - tz / 2.0 + else: + z = pz - tz / 2.0 + self.spacing[2] / 2.0 + x = px + + if self.mapper: + y += sy / 2.0 + z += sz / 2.0 + + else: + if self.points.shape[0] % 2: + y = py - ty / 2.0 + else: + y = py - ty / 2.0 + self.spacing[1] / 2.0 + + if self.points.shape[1] % 2: + x = px - tx / 2.0 + else: + x = px - tx / 2.0 + self.spacing[0] / 2.0 + z = pz + + if self.mapper: + x += sx / 2.0 + y += sy / 2.0 + + self.actor.SetPosition(x, y, z) def SetSpacing(self, spacing): self.spacing = spacing - self.__calculate_area_pixels() + self._build_actor() + self._calculate_area_pixels() def Show(self, value=1): if value: @@ -148,90 +195,104 @@ class CursorCircle: def GetPixels(self): return self.points + def _build_actor(self): + pass + + def _calculate_area_pixels(self): + pass -class CursorRectangle: + def _set_colour(self, imagedata, colour): + scalar_range = int(imagedata.GetScalarRange()[1]) + r, g, b = colour + + # map scalar values into colors + lut_mask = vtk.vtkLookupTable() + lut_mask.SetNumberOfColors(256) + lut_mask.SetHueRange(const.THRESHOLD_HUE_RANGE) + lut_mask.SetSaturationRange(1, 1) + lut_mask.SetValueRange(0, 255) + lut_mask.SetRange(0, 255) + lut_mask.SetNumberOfTableValues(256) + lut_mask.SetTableValue(0, 0, 0, 0, 0.0) + lut_mask.SetTableValue(1.0, 1-r, 1-g, 1-b, 0.50) + lut_mask.SetRampToLinear() + lut_mask.Build() + + # map the input image through a lookup table + img_colours_mask = vtk.vtkImageMapToColors() + img_colours_mask.SetLookupTable(lut_mask) + img_colours_mask.SetOutputFormatToRGBA() + img_colours_mask.SetInput(imagedata) + img_colours_mask.Update() + + return img_colours_mask.GetOutput() + + +class CursorCircle(CursorBase): + # TODO: Think and try to change this class to an actor + # CursorCircleActor(vtk.vtkActor) def __init__(self): - - self.colour = (0.0, 0.0, 1.0) - self.opacity = 1 - - self.x_length = 30 - self.y_length = 30 - self.radius = 15 - - self.dimension = (self.x_length, self.y_length) - self.position = (0 ,0) - self.orientation = "AXIAL" - self.spacing = (1, 1, 1) - - self.__build_actor() - self.__calculate_area_pixels() - - def SetSize(self, size): - self.x_length = size - self.y_length = size - self.radius = size / 2 - retangle = self.retangle - retangle.SetXLength(size) - retangle.SetYLength(size) - self.__calculate_area_pixels() - - def SetOrientation(self, orientation): - self.orientation = orientation - - def SetColour(self, colour): - self.colour = colour - self.actor.GetProperty().SetColor(colour) + self.radius = 15.0 + super(CursorCircle, self).__init__() - def SetOrientation(self, orientation): - self.orientation = orientation - proj = Project() - orig_orien = proj.original_orientation - if orientation == "CORONAL": - self.actor.RotateX(90) - if orientation == "SAGITAL": - self.actor.RotateY(90) + def _build_actor(self): + """ + Function to plot the circle + """ + print "Building circle cursor", self.orientation + r = self.radius + sx, sy, sz = self.spacing + if self.orientation == 'AXIAL': + xi = math.floor(-r/sx) + xf = math.ceil(r/sx) + 1 + yi = math.floor(-r/sy) + yf = math.ceil(r/sy) + 1 + zi = 0 + zf = 1 + elif self.orientation == 'CORONAL': + xi = math.floor(-r/sx) + xf = math.ceil(r/sx) + 1 + yi = 0 + yf = 1 + zi = math.floor(-r/sz) + zf = math.ceil(r/sz) + 1 + elif self.orientation == 'SAGITAL': + xi = 0 + xf = 1 + yi = math.floor(-r/sy) + yf = math.ceil(r/sy) + 1 + zi = math.floor(-r/sz) + zf = math.ceil(r/sz) + 1 - def SetPosition(self, position): - x,y,z = position - self.position = position - self.actor.SetPosition(x,y,z) - - def SetEditionPosition(self, position): - self.edition_position = position + z,y,x = numpy.ogrid[zi:zf,yi:yf, xi:xf] - def SetSpacing(self, spacing): - self.spacing = spacing + circle_m = (z*sz)**2 + (y*sy)**2 + (x*sx)**2 <= r**2 + circle_i = to_vtk(circle_m.astype('uint8'), + self.spacing, 0, self.orientation) + circle_ci = self._set_colour(circle_i, self.colour) - def Show(self, value=1): - if value: - self.actor.VisibilityOn() + if self.mapper is None: + self.actor.SetInput(circle_ci) + self.actor.InterpolateOff() + self.actor.PickableOff() + self.actor.SetDisplayExtent(circle_ci.GetExtent()) else: - self.actor.VisibilityOff() + self.mapper.SetInput(circle_ci) + self.mapper.BorderOn() + + self.mapper.SetOrientation(ORIENTATION[self.orientation]) - def __build_actor(self): + print '====================================' + print self.orientation + print circle_ci.GetSpacing() + print xi, xf, yi, yf, zi, zf + print '====================================' + + def _calculate_area_pixels(self): """ - Function to plot the Retangle + Return the cursor's pixels. """ - print "Building rectangle cursor", self.orientation - mapper = vtk.vtkPolyDataMapper() - self.retangle = vtk.vtkCubeSource() - self.actor = actor = vtk.vtkActor() - - prop = vtk.vtkProperty() - prop.SetRepresentationToWireframe() - self.actor.SetProperty(prop) - - mapper.SetInput(self.retangle.GetOutput()) - actor.SetPosition(self.position[0] - self.x_length,\ - self.position[1] - self.y_length, 1) - - actor.SetMapper(mapper) - actor.GetProperty().SetOpacity(self.opacity) - actor.GetProperty().SetColor(self.colour) - actor.SetVisibility(0) - - def __calculate_area_pixels(self): + r = self.radius if self.orientation == 'AXIAL': sx = self.spacing[0] sy = self.spacing[1] @@ -241,12 +302,71 @@ class CursorRectangle: elif self.orientation == 'SAGITAL': sx = self.spacing[1] sy = self.spacing[2] - shape = (self.y_length/sy, self.x_length/sx) - self.points = numpy.empty(shape, dtype='bool') - self.points.fill(True) - def GetPixels(self): + xi = math.floor(-r/sx) + xf = math.ceil(r/sx) + 1 + yi = math.floor(-r/sy) + yf = math.ceil(r/sy) + 1 + + y,x = numpy.ogrid[yi:yf, xi:xf] + + print "AREA", x + + index = (y*sy)**2 + (x*sx)**2 <= r**2 + self.points = index + + +class CursorRectangle(CursorBase): + def __init__(self): + self.radius = 15.0 + super(CursorRectangle, self).__init__() + + + def _build_actor(self): """ - Return the points of the rectangle + Function to plot the Retangle """ - return self.points + print "Building rectangle cursor", self.orientation + r = self.radius + sx, sy, sz = self.spacing + if self.orientation == 'AXIAL': + x = math.floor(2*r/sx) + y = math.floor(2*r/sy) + z = 1 + elif self.orientation == 'CORONAL': + x = math.floor(r/sx) + y = 1 + z = math.floor(r/sz) + elif self.orientation == 'SAGITAL': + x = 1 + y = math.floor(r/sy) + z = math.floor(r/sz) + + rectangle_m = numpy.ones((z, y, x), dtype='uint8') + rectangle_i = to_vtk(rectangle_m, self.spacing, 0, self.orientation) + rectangle_ci = self._set_colour(rectangle_i, self.colour) + + if self.mapper is None: + self.actor.SetInput(rectangle_ci) + self.actor.InterpolateOff() + self.actor.PickableOff() + self.actor.SetDisplayExtent(rectangle_ci.GetExtent()) + else: + self.mapper.SetInput(rectangle_ci) + self.mapper.BorderOn() + self.mapper.SetOrientation(ORIENTATION[self.orientation]) + + def _calculate_area_pixels(self): + r = self.radius + sx, sy, sz = self.spacing + if self.orientation == 'AXIAL': + x = math.floor(2*r/sx) + y = math.floor(2*r/sy) + elif self.orientation == 'CORONAL': + x = math.floor(r/sx) + y = math.floor(r/sz) + elif self.orientation == 'SAGITAL': + x = math.floor(r/sy) + y = math.floor(r/sz) + + self.points = numpy.ones((y, x), dtype='bool') diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index 68ce7af..a6c3f4d 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -545,5 +545,3 @@ def analyze2mmap(analyze): matrix.flush() return matrix, temp_file - - diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index e82ddf2..32b83f4 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -294,8 +294,8 @@ class Slice(object): sx = self.spacing[0] sy = self.spacing[2] elif orientation == 'SAGITAL': - sx = self.spacing[1] - sy = self.spacing[2] + sx = self.spacing[2] + sy = self.spacing[1] else: if orientation == 'AXIAL': @@ -309,15 +309,17 @@ class Slice(object): py = position / mask.shape[1] px = position % mask.shape[1] elif orientation == 'SAGITAL': - sx = self.spacing[1] - sy = self.spacing[2] + sx = self.spacing[2] + sy = self.spacing[1] py = position / mask.shape[1] px = position % mask.shape[1] - xi = px - math.ceil(radius/sx) - xf = px + math.ceil(radius/sx) - yi = py - math.ceil(radius/sy) - yf = py + math.ceil(radius/sy) + cx = index.shape[1] / 2 + 1 + cy = index.shape[0] / 2 + 1 + xi = px - index.shape[1] + cx + xf = xi + index.shape[1] + yi = py - index.shape[0] + cy + yf = yi + index.shape[0] if yi < 0: index = index[abs(yi):,:] @@ -341,6 +343,12 @@ class Slice(object): roi_m = mask[yi:yf,xi:xf] roi_i = image[yi:yf, xi:xf] + print + print"IMAGE", roi_m.shape + print "BRUSH", index.shape + print "IMAGE[BRUSH]", roi_m[index].shape + print + if operation == const.BRUSH_THRESH: # It's a trick to make points between threshold gets value 254 # (1 * 253 + 1) and out ones gets value 1 (0 * 253 + 1). diff --git a/invesalius/data/slice_data.py b/invesalius/data/slice_data.py index e07cf74..d7ffa75 100644 --- a/invesalius/data/slice_data.py +++ b/invesalius/data/slice_data.py @@ -38,6 +38,7 @@ class SliceData(object): self.number = 0 self.orientation = 'AXIAL' self.renderer = None + self.overlay_renderer = None self.__create_text() self.__create_box() @@ -131,8 +132,8 @@ class SliceData(object): def SetCursor(self, cursor): if self.cursor: - self.renderer.RemoveActor(self.cursor.actor) - self.renderer.AddActor(cursor.actor) + self.overlay_renderer.RemoveActor(self.cursor.actor) + self.overlay_renderer.AddActor(cursor.actor) self.cursor = cursor def SetNumber(self, number): @@ -166,7 +167,7 @@ class SliceData(object): self.line_r.SetPoint2((xf, yf, 0)) def Hide(self): - self.renderer.RemoveActor(self.actor) + self.overlay_renderer.RemoveActor(self.actor) self.renderer.RemoveActor(self.text.actor) def Show(self): diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index c8aa42f..befeb4e 100755 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -725,13 +725,13 @@ class Viewer(wx.Panel): self.pick.Pick(mouse_x, mouse_y, 0, render) coord = self.get_coordinate_cursor() - slice_data.cursor.SetPosition(coord) - slice_data.cursor.SetEditionPosition( - self.get_coordinate_cursor_edition(slice_data)) - self.__update_cursor_position(slice_data, coord) + position = self.slice_data.actor.GetInput().FindPoint(coord) + + if position != -1: + coord = self.slice_data.actor.GetInput().GetPoint(position) + slice_data.cursor.SetPosition(coord) cursor = self.slice_data.cursor - position = self.slice_data.actor.GetInput().FindPoint(coord) radius = cursor.radius if position < 0: @@ -771,9 +771,7 @@ class Viewer(wx.Panel): if position != -1: coord = self.slice_data.actor.GetInput().GetPoint(position) slice_data.cursor.SetPosition(coord) - slice_data.cursor.SetEditionPosition( - self.get_coordinate_cursor_edition(slice_data)) - self.__update_cursor_position(slice_data, coord) + #self.__update_cursor_position(slice_data, coord) if (self.left_pressed): cursor = self.slice_data.cursor @@ -1280,12 +1278,25 @@ class Viewer(wx.Panel): def create_slice_window(self): renderer = vtk.vtkRenderer() + renderer.SetLayer(0) + cam = renderer.GetActiveCamera() + + overlay_renderer = vtk.vtkRenderer() + overlay_renderer.SetLayer(1) + overlay_renderer.SetActiveCamera(cam) + overlay_renderer.SetInteractive(0) + + + self.interactor.GetRenderWindow().SetNumberOfLayers(2) + self.interactor.GetRenderWindow().AddRenderer(overlay_renderer) self.interactor.GetRenderWindow().AddRenderer(renderer) + actor = vtk.vtkImageActor() actor.InterpolateOff() slice_data = sd.SliceData() slice_data.SetOrientation(self.orientation) slice_data.renderer = renderer + slice_data.overlay_renderer = overlay_renderer slice_data.actor = actor slice_data.SetBorderStyle(sd.BORDER_ALL) renderer.AddActor(actor) @@ -1301,7 +1312,7 @@ class Viewer(wx.Panel): self.cam.SetFocalPoint(0, 0, 0) self.cam.SetViewUp(const.SLICE_POSITION[orig_orien][0][self.orientation]) self.cam.SetPosition(const.SLICE_POSITION[orig_orien][1][self.orientation]) - self.cam.ComputeViewPlaneNormal() + #self.cam.ComputeViewPlaneNormal() #self.cam.OrthogonalizeViewUp() self.cam.ParallelProjectionOn() -- libgit2 0.21.2