Skip to content

Instantly share code, notes, and snippets.

@mhogg
Created September 9, 2020 04:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhogg/4863cf0b91e2f85a56d52b8a6981e325 to your computer and use it in GitHub Desktop.
Save mhogg/4863cf0b91e2f85a56d52b8a6981e325 to your computer and use it in GitHub Desktop.
import numpy as np
import vtkmodules.all as vtk
from vtkmodules.util import numpy_support as nps
from vtkmodules.vtkCommonCore import vtkCommand
class InteractorStyleDrawPolygon(vtk.vtkInteractorStyle):
"""
InteractorStyleDrawPolygon is a re-write of the vtk class of the same name in python.
The python class vtk.vtkInteractorStyleDrawPolygon is not a full wrap of the underlying
++ class. For example, vtkInteractorStyleDrawPolygon doesn't have a method for
'GetPolygonPoints' in Python. It's in the header, source and documentation, but not python.
Reports are this is because it returns a vtkVector2i, a template based generic class, which
is difficult to wrap.
References:
- http://vtk.1045678.n5.nabble.com/Missing-method-for-vtkInteractorStyleDrawPolygon-td5747100.html
- https://gitlab.kitware.com/vtk/vtk/blob/master/Interaction/Style/vtkInteractorStyleDrawPolygon.h
- https://gitlab.kitware.com/vtk/vtk/-/blob/master/Interaction/Style/vtkInteractorStyleDrawPolygon.cxx
"""
class vtkInternal(object):
"""
Class internal to InteractorStyleDrawPolygon to help with drawing the polygon
"""
def __init__(self, parent=None):
super().__init__()
self._points = []
@property
def points(self):
return np.array(self._points)
def AddPoint(self, x, y):
self._points.append([x,y])
def GetPoint(self, index):
if index < 0 or index > len(self._points)-1: return
return np.array(self._points[index])
def GetNumberOfPoints(self):
return len(self._points)
def Clear(self):
self._points = []
def DrawPixels(self, StartPos, EndPos, pixels, size):
# C++ args: const vtkVector2i& StartPos, const vtkVector2i& EndPos, unsigned char* pixels, const int* size
# NOTE: ^ operator = bitwise exclusive OR. Same in C++ and Python
length = int(round(np.linalg.norm(StartPos-EndPos)))
if length == 0: return
x1, y1 = StartPos
x2, y2 = EndPos
x = [int(round(v)) for v in np.linspace(x1,x2,length)]
y = [int(round(v)) for v in np.linspace(y1,y2,length)]
indices = np.array([row * size[0] + col for col,row in zip(x,y)])
pixels[indices] = 255 ^ pixels[indices]
def __init__(self, parent=None, interactor=None, renderer=None):
self.parent = parent
self.interactor = interactor
self.renderer = renderer
self.AddObserver("MouseMoveEvent", self.OnMouseMove)
self.AddObserver("LeftButtonPressEvent", self.OnLeftButtonDown)
self.AddObserver("LeftButtonReleaseEvent", self.OnLeftButtonUp)
self.setup()
def setup(self):
self.Internal = self.vtkInternal()
self.StartPosition = np.zeros(2, dtype=np.int32)
self.EndPosition = np.zeros(2, dtype=np.int32)
self.Moving = False
self.DrawPolygonPixels = True
self.PixelArray = vtk.vtkUnsignedCharArray()
def DrawPolygon(self):
"""
Draw the polygon defined by the mouse move points
"""
tmpPixelArray = vtk.vtkUnsignedCharArray()
tmpPixelArray.DeepCopy(self.PixelArray)
pixels = nps.vtk_to_numpy(tmpPixelArray)
renWin = self.interactor.GetRenderWindow()
size = renWin.GetSize()
# Draw each line segment
for i in range(self.Internal.GetNumberOfPoints()-1):
a = self.Internal.GetPoint(i)
b = self.Internal.GetPoint(i+1)
self.Internal.DrawPixels(a, b, pixels, size)
# Draw a line from the end to the start
if (self.Internal.GetNumberOfPoints() >= 3):
start = self.Internal.GetPoint(0)
end = self.Internal.GetPoint(self.Internal.GetNumberOfPoints()-1)
self.Internal.DrawPixels(start, end, pixels, size)
# NOTE: In SetPixelData, must add 0 as 7th variable (in C++ it has a default
# value of 0, but not in Python)
# NOTE: SetPixelData takes a long time to run, particularly if the screen is
# maximised, which increases the number of pixels
renWin.SetPixelData(0, 0, size[0]-1, size[1]-1, pixels.flatten(), 0, 0)
renWin.Frame()
def DrawPolygonPixelsOn(self):
self.DrawPolygonPixels = True
def DrawPolygonPixelsOff(self):
self.DrawPolygonPixels = False
def GetPolygonPoints(self):
"""
Return the polygon points as a numpy array
NOTE: This function is not available in wrapped Python vtk
"""
return self.Internal.points
def OnLeftButtonDown(self, obj, event):
"""
Left mouse button press event
"""
if self.interactor is None: return
self.Moving = True
renWin = self.interactor.GetRenderWindow()
eventPos = self.interactor.GetEventPosition()
self.StartPosition[0], self.StartPosition[1] = eventPos[0], eventPos[1]
self.EndPosition = self.StartPosition
self.PixelArray.Initialize()
self.PixelArray.SetNumberOfComponents(3)
size = renWin.GetSize()
self.PixelArray.SetNumberOfTuples(size[0] * size[1])
self.pixels = None
# Note: In GetPixelData, must add 0 as 7th variable (in C++ it has a default
# value of 0, but not in Python)
renWin.GetPixelData(0, 0, size[0]-1, size[1]-1, 1, self.PixelArray, 0)
self.Internal.Clear()
self.Internal.AddPoint(self.StartPosition[0], self.StartPosition[1])
self.InvokeEvent(vtk.vtkCommand.StartInteractionEvent)
# Call parent function
#super().OnLeftButtonDown()
def OnLeftButtonUp(self, obj, event):
"""
Left mouse button release event
When LMB is released, a EndPickEvent and EndInteractionEvent are emitted
NOTE: This is different to the C++ class, which emits a SelectionChangedEvent
instead of an EndPickEvent
"""
if self.interactor is None or not self.Moving: return
if self.DrawPolygonPixels:
renWin = self.interactor.GetRenderWindow()
size = renWin.GetSize()
pixels = nps.vtk_to_numpy(self.PixelArray)
renWin.SetPixelData(0, 0, size[0]-1, size[1]-1, pixels.flatten(), 0, 0)
renWin.Frame()
self.Moving = False
self.InvokeEvent(vtkCommand.SelectionChangedEvent)
self.InvokeEvent(vtkCommand.EndPickEvent)
self.InvokeEvent(vtkCommand.EndInteractionEvent)
# Call parent function
#super().OnLeftButtonUp()
def OnMouseMove(self, obj, event):
"""
On mouse move event
"""
if self.interactor is None or not self.Moving: return
# Get lastest mouse position
eventPos = self.interactor.GetEventPosition()
self.EndPosition[0], self.EndPosition[1] = eventPos[0], eventPos[1]
size = self.interactor.GetRenderWindow().GetSize()
if self.EndPosition[0] > size[0]-1: self.EndPosition[0] = size[0]-1
if self.EndPosition[0] < 0: self.EndPosition[0] = 0
if self.EndPosition[1] > size[1]-1: self.EndPosition[1] = size[1]-1
if self.EndPosition[1] < 0: self.EndPosition[1] = 0
# Update the polygon to include the lastest mouse position
lastPoint = self.Internal.GetPoint(self.Internal.GetNumberOfPoints()-1)
newPoint = self.EndPosition
if np.linalg.norm(lastPoint-newPoint) > 10:
self.Internal.AddPoint(*newPoint)
if self.DrawPolygonPixels:
self.DrawPolygon()
# Call parent function
#super().OnMouseMove()
def SetDrawPolygonPixels(self, drawPolygonPixels):
self.DrawPolygonPixels = drawPolygonPixels
def __str__(self):
"""
Replaces PrintSelf in C++ class
"""
indent = 2*' '
s = super().__str__().rstrip()+'\n'
s += f'{indent}Moving : {self.Moving}\n'
s += f'{indent}DrawPolygonPixels: {self.DrawPolygonPixels}\n'
s += f'{indent}StartPosition: {self.StartPosition[0]}, {self.StartPosition[1]}\n'
s += f'{indent}EndPosition: {self.EndPosition[0]}, {self.EndPosition[1]}\n'
return s
@yangyang117
Copy link

hi, thank you for your sharing of your code. Can you show how to use this style? I use it by
interactorStyle1 = InteractorStyleDrawPolygon()
but it will not work. I see in the init part, there are parent=None, interactor=None, renderer=None, how to define them?
before use it, show define an interactor ?

@yangyang117
Copy link

I try to use the vtkInteractorStyle as the interactor ,but get the error
AttributeError: 'vtkmodules.vtkRenderingCore.vtkInteractorStyle' object has no attribute 'GetEventPosition'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment