Hi there,
I am developing an application based on acquiring a STL mesh with a laser line sensor. The mesh is used to plan a robot path to perform some operations based on an interaction with the user. I mean, once the mesh is acquired by the sensor it should appear on a graphical user interface where the user has to select with the mouse a region of the mesh where the robot should perform the operations.
So my questions are multiple:
-are there some examples available showing how to pick cells of a mesh inside a polygon drawn with a mouse? I would like to replicate what happens with software like Meshlab, where you select an area and you delete all the points of the area
-is vtk compatible with GUI libraries such as tkinter or Qt? I mean, the mesh should appear inside a canvas of a window.
Thanks a lot, sorry but I am new to VTK and I cannot find examples about this topic.
Anyone could help me, please?
Rendering/Core/Testing/Cxx/TestPolygonSelection.cxx shows how its done.
Yup, VTK is compatible with tkinter from python and Qt from both C++ and Python.
You can inherit from vtkInteractorStyleDrawPolygon
. It is not possible in Python, but a friend re-implemented the class in Python. I used this as starting point for making trimming tool for vtkPolyData
. I have attached a fully working examples.
#!/usr/bin/env python3
import numpy as np
import vtk
class InteractorStyleMesh(vtk.vtkInteractorStyle):
"""
InteractorStyleDrawPolygon is a re-write of the vtk.vtk class of the same name in python.
The python class vtk.vtk.vtkInteractorStyleDrawPolygon is not a full wrap of the underlying
++ class. For example, vtk.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 vtk.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 vtk.vtkVector2i& StartPos, const vtk.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.polyData = None
self.actor = None
self.placer = None
self.selectedActor = vtk.vtkActor()
self.selectedMapper = vtk.vtkDataSetMapper()
self.selectedActor.SetMapper(self.selectedMapper)
self.AddObserver("MouseMoveEvent", self.OnMouseMove)
self.AddObserver("LeftButtonPressEvent", self.OnLeftButtonDown)
self.AddObserver("LeftButtonReleaseEvent", self.OnLeftButtonUp)
self.AddObserver("RightButtonPressEvent", self.OnRightButtonDown)
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()
self.cylinder = vtk.vtkSphere()
self.cylinder.SetRadius(4.0)
self.extractPolyDataGeometry = vtk.vtkExtractPolyDataGeometry()
self.extractPolyDataGeometry.SetImplicitFunction(self.cylinder)
self.selectedMapper.ScalarVisibilityOff()
self.selectedActor.GetProperty().SetColor(
colors.GetColor3d("Tomato"))
self.selectedActor.GetProperty().SetPointSize(5)
self.selectedActor.GetProperty().SetRepresentationToWireframe()
self.lastCellId = -1
self.appendPolyData = vtk.vtkAppendPolyData()
self.cleanPolyData = vtk.vtkCleanPolyData()
self.cleanPolyData.SetInputConnection(self.appendPolyData.GetOutputPort())
self.selectedMapper.SetInputConnection(self.cleanPolyData.GetOutputPort())
def SetPolyData(self, polyData):
self.polyData = polyData
self.extractPolyDataGeometry.SetInputData(self.polyData)
self.Modified()
def SetActor(self, actor):
self.actor = actor
self.placer = vtk.vtkPolygonalSurfacePointPlacer()
self.placer.AddProp(self.actor)
self.placer.GetPolys().AddItem(self.polyData)
self.Modified()
def OnRightButtonDown(self, obj, event):
if self.renderer.HasViewProp(self.selectedActor):
self.renderer.RemoveActor(self.selectedActor)
self.appendPolyData.SetInputData(None)
self.cleanPolyData.SetInputConnection(self.appendPolyData.GetOutputPort())
self.renderer.GetRenderWindow().Render()
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.Internal.Clear()
self.Internal.AddPoint(self.StartPosition[0], self.StartPosition[1])
if (self.polyData is not None):
X, Y = self.interactor.GetEventPosition()
displayPos = [float(X), float(Y)]
worldPos = [0,0,0]
worldOrient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
if (not self.placer.ComputeWorldPosition(self.renderer, displayPos, worldPos, worldOrient)):
return
lastId = self.placer.GetCellPicker().GetCellId()
if (lastId == self.lastCellId):
self.interactor.GetRenderWindow().Render()
return
self.lastCellId = lastId
self.cylinder.SetCenter(worldPos)
self.cylinder.Modified()
self.extractPolyDataGeometry.Update()
self.appendPolyData.AddInputData(self.extractPolyDataGeometry.GetOutput())
if not self.renderer.HasViewProp(self.selectedActor):
self.renderer.AddActor(self.selectedActor)
self.interactor.GetRenderWindow().Render()
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
self.Moving = False
output = self.cleanPolyData.GetOutput()
self.appendPolyData.SetInputData(None)
self.appendPolyData.SetInputData(output)
self.interactor.GetRenderWindow().Render()
self.InvokeEvent(vtk.vtkCommand.SelectionChangedEvent)
self.InvokeEvent(vtk.vtkCommand.EndPickEvent)
self.InvokeEvent(vtk.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
# Criteria for enough movement
if np.linalg.norm(lastPoint-newPoint) > 10:
# Movement is sufficient
self.Internal.AddPoint(*newPoint)
X, Y = self.interactor.GetEventPosition()
displayPos = [float(X), float(Y)]
worldPos = [0,0,0]
worldOrient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
if (not self.placer.ComputeWorldPosition(self.renderer, displayPos, worldPos, worldOrient)):
return
lastId = self.placer.GetCellPicker().GetCellId()
if (lastId == self.lastCellId):
return
self.lastCellId = lastId
self.cylinder.SetCenter(worldPos)
self.cylinder.Modified()
self.extractPolyDataGeometry.Update()
# Test appending
newData = vtk.vtkPolyData()
newData.DeepCopy(self.extractPolyDataGeometry.GetOutput())
self.appendPolyData.AddInputData(newData)#self.extractPolyDataGeometry.GetOutput())
print(self.appendPolyData.GetOutput().GetNumberOfPoints())
print(self.cleanPolyData.GetOutput().GetNumberOfPoints())
self.interactor.GetRenderWindow().Render()
# Append polydata or add new implicit function
# Call parent function
#super().OnMouseMove()
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
source = vtk.vtkSphereSource()
source.SetPhiResolution(42)
source.SetThetaResolution(80)
source.SetPhiResolution(84)
source.SetThetaResolution(160)
source.SetRadius(20)
source.Update()
polyData0 = source.GetOutput()
transformFilter = vtk.vtkTransformPolyDataFilter()
transform = vtk.vtkTransform()
transform.RotateX(90)
transform.Modified()
transformFilter.SetTransform(transform)
transformFilter.SetInputData(polyData0)
transformFilter.Update()
polyData = transformFilter.GetOutput()
colors = vtk.vtkNamedColors()
idFilter = vtk.vtkIdFilter()
idFilter.SetInputData(polyData)
idFilter.SetCellIdsArrayName("OriginalIds")
idFilter.SetPointIdsArrayName("OriginalIds")
idFilter.Update()
# This is needed to convert the ouput of vtk.vtkIdFilter (vtkDataSet) back to
# vtk.vtkPolyData
surfaceFilter = vtk.vtkDataSetSurfaceFilter()
surfaceFilter.SetInputConnection(idFilter.GetOutputPort())
surfaceFilter.Update()
inputData = surfaceFilter.GetOutput()
# Create a mapper and actor
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(polyData)
mapper.ScalarVisibilityOff()
actor = vtk.vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetPointSize(5)
actor.GetProperty().SetDiffuseColor(colors.GetColor3d("Peacock"))
# Visualize
renderer = vtk.vtkRenderer()
renderer.UseHiddenLineRemovalOn()
renderWindow = vtk.vtkRenderWindow()
renderWindow.AddRenderer(renderer)
renderWindow.SetSize(640, 480)
renderWindow.SetWindowName("HighlightSelection")
areaPicker = vtk.vtkAreaPicker()
renderWindowInteractor = vtk.vtkRenderWindowInteractor()
renderWindowInteractor.SetPicker(areaPicker)
renderWindowInteractor.SetRenderWindow(renderWindow)
renderer.AddActor(actor)
renderer.SetBackground(colors.GetColor3d("Tan"))
renderWindow.Render()
style = InteractorStyleMesh(None, renderWindowInteractor, renderer)
style.SetPolyData(inputData)
style.SetActor(actor)
renderWindowInteractor.SetInteractorStyle(style)
renderWindowInteractor.Start()
The picking is used for drawing an overlay in the example. This can later be used for coloring, removing part of the polydata or whatever you like. Remember to build the indexer for the polydata before modifying it.