Delete control points within a ROI

Hi all,

I would like to develop a Python script that allows the deletion of multiple control points within a ROI. Specifically, it would be great to mimic the behavior of the Scissors tool from the Segment Editor module.


Example: I want to delete all the control points inside a selected circle on the image.

Any suggestions on how to approach this?

Many thanks,

This functionality already exists in SlicerMorph extension. It is a plugin called MarkupEditor.

Hi, thank you very much! I’m running some tests with this tool. However, the selection of points isn’t accurate when there’s overlap between points or with a curve. Additionally, sometimes points outside the ROI get selected.

The steps I’m following are:

Pick points with curve → Set selection → draw ROI → Edit selected points → Delete points


See example here.

I also noticed that it only takes into account the points that are visible in the 3D view, which becomes a problem when the view is zoomed in on a specific region.

Yes, I observed similar issues occasionally as well. But often drawing another curve and choosing “Add to Selection” fixes it, so we didn’t spend too much debugging it.

You are welcome to take look and improve if you have the bandwidth.

1 Like

Yes, that’s a requirement for our use case, in which we often have points on a model. If you don’t restrict it to visible points only, it will select points on the back side of the model too.

1 Like

I’ve reviewed the code, and for our specific purpose, we need to ensure that points outside the view are not selected, and that the ROI restricts the selection similarly to how the scissors tool works in Segment Editor. To achieve this, I modified the logic class in the MarkupEditor.py file (look for comments marked “CHANGED”):

  1. Removed if pointVisible: in the selection option.
  2. Added a check to determine if a point is outside the 3D view; if so, it is marked as unselected.
class MarkupEditorLogic(ScriptedLoadableModuleLogic):
    def __init__(self):
        ScriptedLoadableModuleLogic.__init__(self)
        self.observerTags = {}

    def editMarkups(self, selectOption, fiducialsNode, curveNode, viewNode):
        layoutManager = slicer.app.layoutManager()
        threeDWidget = layoutManager.viewWidget(viewNode)
        threeDView = threeDWidget.threeDView()
        aspectRatio = threeDWidget.width / threeDWidget.height
        className = "vtkMRMLMarkupsDisplayableManager"
        markupsDisplayableManager = threeDView.displayableManagerByClassName(className)
        fiducialsWidget =  markupsDisplayableManager.GetWidget(fiducialsNode.GetDisplayNode())
        fiducialsRepresentation =  fiducialsWidget.GetRepresentation()

        cameraNode = slicer.modules.cameras.logic().GetViewActiveCameraNode(viewNode)
        camera = cameraNode.GetCamera()
        raswToXYZW = vtk.vtkMatrix4x4()
        modelView = camera.GetModelViewTransformMatrix()
        projection = camera.GetProjectionTransformMatrix(aspectRatio, 1, 100)  # near/far are arbitrary
        raswToXYZW.Multiply4x4(projection, modelView, raswToXYZW)

        def rasToColumnRow(ras):
            rasw = *ras, 1
            xyzw = raswToXYZW.MultiplyPoint(rasw)
            x, y = [xyzw[0], xyzw[1]]
            if viewNode.GetRenderMode() == viewNode.Perspective:
                x, y = [x / xyzw[3], y / xyzw[3]]
            column = (x + 1) / 2 * threeDWidget.width
            row = (1 - (y + 1) / 2) * threeDWidget.height
            return column, row

        selectionPolygon = qt.QPolygonF()
        for index in range(curveNode.GetNumberOfControlPoints()):
            ras = [0] * 3
            curveNode.GetNthControlPointPositionWorld(index, ras)
            column, row = rasToColumnRow(ras)
            point = qt.QPointF(column, row)
            selectionPolygon.push_back(point)

        pickImage = qt.QImage(threeDWidget.width, threeDWidget.height, qt.QImage().Format_ARGB32)
        backgroundColor = qt.QColor("#ffffffff")
        pickImage.fill(backgroundColor)

        painter = qt.QPainter()
        painter.begin(pickImage)
        painterPath = qt.QPainterPath()
        painterPath.addPolygon(selectionPolygon)
        brush = qt.QBrush(qt.Qt.SolidPattern)
        painter.setBrush(brush)
        painter.drawPath(painterPath)
        painter.end()

        debugging = False
        if debugging:
            pixmap = qt.QPixmap().fromImage(pickImage)
            slicer.modules.label = qt.QLabel()
            slicer.modules.label.setPixmap(pixmap)
            slicer.modules.label.show()

        visibleCount = 0
        pickedCount = 0
        for index in range(fiducialsNode.GetNumberOfControlPoints()):
            pointVisible = fiducialsRepresentation.GetNthControlPointViewVisibility(index)
            if pointVisible:
                visibleCount += 1

            ras = [0] * 3
            fiducialsNode.GetNthControlPointPositionWorld(index, ras)
            column, row = rasToColumnRow(ras)

            # CHANGED: check if the point is inside the 3D view
            if column < 0 or column >= pickImage.width() or row < 0 or row >= pickImage.height():
                fiducialsNode.SetNthControlPointSelected(index, False)
                continue  # we do not process this point

            # If the point is inside the 3D view, we check if it is inside
            # the polygon
            pickColor = pickImage.pixelColor(column, row)
            picked = (pickColor != backgroundColor)
            
            if picked:
                pickedCount += 1

            # CHANGED: we do not use pointVisible here, so overlapping points are selected
            if selectOption == "set":
                fiducialsNode.SetNthControlPointSelected(index, picked)
            elif selectOption == "add":
                if picked:
                    fiducialsNode.SetNthControlPointSelected(index, True)
            elif selectOption == "unset":
                if picked:
                    fiducialsNode.SetNthControlPointSelected(index, False)
            else:
                logging.error(f"Unknown selectOption {selectOption}")
    


1 Like