Picking cells inside polygon drawn with the mouse

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.