Skip to content

Commit

Permalink
ENH: make boolean operations more robust by automatic retries and inp…
Browse files Browse the repository at this point in the history
…ut meshes triangulation features

Fixes zippy84/vtkbool#81
  • Loading branch information
mauigna06 committed Aug 9, 2024
1 parent 6f3ee57 commit b726771
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 30 deletions.
201 changes: 179 additions & 22 deletions CombineModels/CombineModels.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ def setup(self):
self.ui.operationDifferenceRadioButton.connect("toggled(bool)", lambda toggled, op="difference": self.operationButtonToggled(op))
self.ui.operationDifference2RadioButton.connect("toggled(bool)", lambda toggled, op="difference2": self.operationButtonToggled(op))

self.ui.triangulateInputsCheckBox.connect("stateChanged(int)", self.updateParameterNodeFromGUI)

# Spin Boxes
self.ui.numberOfRetriesSpinBox.valueChanged.connect(self.updateParameterNodeFromGUI)
self.ui.translateRandomlySpinBox.valueChanged.connect(self.updateParameterNodeFromGUI)

# Buttons
self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
self.ui.toggleVisibilityButton.connect('clicked(bool)', self.onToggleVisibilityButton)
Expand Down Expand Up @@ -198,6 +204,24 @@ def updateGUIFromParameterNode(self, caller=None, event=None):

self.ui.toggleVisibilityButton.enabled = (self._parameterNode.GetNodeReference("OutputModel") is not None)

# translate randomly order of magnitude (value is negative by default)
translateRandomly_order_abs_val = int(self._parameterNode.GetParameter("translateRandomly"))
self.ui.translateRandomlySpinBox.value = translateRandomly_order_abs_val

numberOfRetries = int(self._parameterNode.GetParameter("numberOfRetries"))
self.ui.numberOfRetriesSpinBox.value = numberOfRetries
if numberOfRetries > 0:
self.ui.numberOfRetriesSpinBox.toolTip = "Model B will be randomized if operation fails"
self.ui.translateRandomlySpinBox.enabled = True
self.ui.translateRandomlySpinBox.toolTip = "If the operation fails, it will retry with a random translation of" + \
f" {10**-translateRandomly_order_abs_val}mm"
else:
self.ui.numberOfRetriesSpinBox.toolTip = "No retries will be done"
self.ui.translateRandomlySpinBox.enabled = False
self.ui.translateRandomlySpinBox.toolTip = "Set a number of retries different than 0"

self.ui.triangulateInputsCheckBox.checked = self._parameterNode.GetParameter("triangulateInputs") == "True"

# All the GUI updates are done
self._updatingGUIFromParameterNode = False

Expand All @@ -216,6 +240,14 @@ def updateParameterNodeFromGUI(self, caller=None, event=None):
self._parameterNode.SetNodeReferenceID("InputModelB", self.ui.inputModelBSelector.currentNodeID)
self._parameterNode.SetNodeReferenceID("OutputModel", self.ui.outputModelSelector.currentNodeID)

self._parameterNode.SetParameter("numberOfRetries", str(self.ui.numberOfRetriesSpinBox.value))
self._parameterNode.SetParameter("translateRandomly", str(self.ui.translateRandomlySpinBox.value))

if self.ui.triangulateInputsCheckBox.checked:
self._parameterNode.SetParameter("triangulateInputs", "True")
else:
self._parameterNode.SetParameter("triangulateInputs", "False")

self._parameterNode.EndModify(wasModified)

def operationButtonToggled(self, operation):
Expand All @@ -237,7 +269,11 @@ def onApplyButton(self):
self._parameterNode.GetNodeReference("InputModelA"),
self._parameterNode.GetNodeReference("InputModelB"),
self._parameterNode.GetNodeReference("OutputModel"),
self._parameterNode.GetParameter("Operation"))
self._parameterNode.GetParameter("Operation"),
int(self._parameterNode.GetParameter("numberOfRetries")),
int(self._parameterNode.GetParameter("translateRandomly")),
self._parameterNode.GetParameter("triangulateInputs") == "True"
)

except Exception as e:
slicer.util.errorDisplay("Failed to compute results: "+str(e))
Expand Down Expand Up @@ -285,15 +321,31 @@ def setDefaultParameters(self, parameterNode):
"""
if not parameterNode.GetParameter("Operation"):
parameterNode.SetParameter("Operation", "union")

def process(self, inputModelA, inputModelB, outputModel, operation):
if not parameterNode.GetParameter("numberOfRetries"):
parameterNode.SetParameter("numberOfRetries", "2")
if not parameterNode.GetParameter("translateRandomly"):
parameterNode.SetParameter("translateRandomly", "4")

def process(
self,
inputModelA,
inputModelB,
outputModel,
operation,
numberOfRetries = 2,
translateRandomly = 4,
triangulateInputs = True
):
"""
Run the processing algorithm.
Can be used without GUI widget.
:param inputModelA: first input model node
:param inputModelB: second input model node
:param outputModel: result model node, if empty then a new output node will be created
:param operation: union, intersection, difference, difference2
:param numberOfRetries: number of retries if operation fails
:param translateRandomly: order of magnitude of the random translation
:param triangulateInputs: triangulate input models before boolean operation
"""

if not inputModelA or not inputModelB or not outputModel:
Expand All @@ -318,31 +370,136 @@ def process(self, inputModelA, inputModelB, outputModel, operation):
else:
raise ValueError("Invalid operation: "+operation)

if inputModelA.GetParentTransformNode() == outputModel.GetParentTransformNode():
combine.SetInputConnection(0, inputModelA.GetPolyDataConnection())
else:
transformToOutput = vtk.vtkGeneralTransform()
slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(inputModelA.GetParentTransformNode(), outputModel.GetParentTransformNode(), transformToOutput)
transformer = vtk.vtkTransformPolyDataFilter()
transformer.SetTransform(transformToOutput)
transformer.SetInputConnection(inputModelA.GetPolyDataConnection())
combine.SetInputConnection(0, transformer.GetOutputPort())

if inputModelB.GetParentTransformNode() == outputModel.GetParentTransformNode():
combine.SetInputConnection(1, inputModelB.GetPolyDataConnection())
else:
transformToOutput = vtk.vtkGeneralTransform()
slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(inputModelB.GetParentTransformNode(), outputModel.GetParentTransformNode(), transformToOutput)
transformer = vtk.vtkTransformPolyDataFilter()
transformer.SetTransform(transformToOutput)
transformer.SetInputConnection(inputModelB.GetPolyDataConnection())
combine.SetInputConnection(1, transformer.GetOutputPort())
transformToOutput = vtk.vtkGeneralTransform()
slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(inputModelA.GetParentTransformNode(), outputModel.GetParentTransformNode(), transformToOutput)
if transformToOutput is None:
transformToOutput = vtk.vtkTransform()
transformerA = vtk.vtkTransformPolyDataFilter()
transformerA.SetTransform(transformToOutput)
transformerA.SetInputData(inputModelA.GetPolyData())
transformerA.Update()
combine.SetInputData(0, transformerA.GetOutput())

transformToOutput = vtk.vtkGeneralTransform()
slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(inputModelB.GetParentTransformNode(), outputModel.GetParentTransformNode(), transformToOutput)
if transformToOutput is None:
transformToOutput = vtk.vtkTransform()
preTransformerB = vtk.vtkTransformPolyDataFilter()
preTransformerB.SetTransform(transformToOutput)
preTransformerB.SetInputData(inputModelB.GetPolyData())
preTransformerB.Update()
identityTransform = vtk.vtkTransform()
transformerB = vtk.vtkTransformPolyDataFilter()
transformerB.SetTransform(identityTransform)
transformerB.SetInputData(preTransformerB.GetOutput())
transformerB.Update()
combine.SetInputData(1, transformerB.GetOutput())

# These parameters might be useful to expose:
# combine.MergeRegsOn() # default off
# combine.DecPolysOff() # default on
combine.Update()

collisionDetectionFilter = vtk.vtkCollisionDetectionFilter()
collisionDetectionFilter.SetInputData(0, transformerA.GetOutput())
collisionDetectionFilter.SetInputData(1, transformerB.GetOutput())
identityMatrix = vtk.vtkMatrix4x4()
collisionDetectionFilter.SetMatrix(0,identityMatrix)
collisionDetectionFilter.SetMatrix(1,identityMatrix)
collisionDetectionFilter.SetCollisionModeToFirstContact()

for retry in range(numberOfRetries+1):
if (
operation == 'union'
):
if combine.GetOutput().GetNumberOfPoints() != 0:
# succeess
break
else:
if retry == 0:
# check if the models are already intersecting
collisionDetectionFilter.Update()
if collisionDetectionFilter.GetOutput().GetNumberOfPoints() == 0:
# models do not touch so we append them
appendFilter = vtk.vtkAppendPolyData()
appendFilter.AddInputData(transformerA.GetOutput())
appendFilter.AddInputData(transformerB.GetOutput())
appendFilter.Update()
combine = appendFilter
break

if (
operation == 'intersection'
):
if combine.GetOutput().GetNumberOfPoints() != 0:
# succeess
break
else:
if retry == 0:
# check if the models are already intersecting
collisionDetectionFilter.Update()
if collisionDetectionFilter.GetOutput().GetNumberOfPoints() == 0:
# models do not touch so we return an empty model
break

if (
operation == 'difference'
):
if combine.GetOutput().GetNumberOfPoints() != 0:
# succeess
break
else:
if retry == 0:
# check if the models are already intersecting
collisionDetectionFilter.Update()
if collisionDetectionFilter.GetOutput().GetNumberOfPoints() == 0:
# models do not touch so we return modelA
combine = transformerA
break

if (
operation == 'difference2'
):
if combine.GetOutput().GetNumberOfPoints() != 0:
# succeess
break
else:
if retry == 0:
# check if the models are already intersecting
collisionDetectionFilter.Update()
if collisionDetectionFilter.GetOutput().GetNumberOfPoints() == 0:
# models do not touch so we return modelA
combine = transformerB
break

if retry == 0 and triangulateInputs:
# in case inputs are not triangulated, triangulate them
triangulatedInputModelA = vtk.vtkTriangleFilter()
triangulatedInputModelA.SetInputData(inputModelA.GetPolyData())
triangulatedInputModelA.Update()
transformerA.SetInputData(triangulatedInputModelA.GetOutput())
transformerA.Update()
triangulatedInputModelB = vtk.vtkTriangleFilter()
triangulatedInputModelB.SetInputData(inputModelB.GetPolyData())
triangulatedInputModelB.Update()
preTransformerB.SetInputData(triangulatedInputModelB.GetOutput())
preTransformerB.Update()
transformerB.SetInputData(preTransformerB.GetOutput())

# retry with random translation if boolean operation fails
logging.info(f"Retrying boolean operation with random translation (retry {retry+1})")
transform = vtk.vtkTransform()
unitaryVector = [vtk.vtkMath.Random()-0.5 for _ in range(3)]
vtk.vtkMath.Normalize(unitaryVector)
import numpy as np
translationVector = np.array(unitaryVector) * (10**-translateRandomly)
transform.Translate(translationVector)
transformerB.SetTransform(transform)
transformerB.Update()
# recalculate the boolean operation
combine.SetInputData(1, transformerB.GetOutput())
combine.Update()

outputModel.SetAndObservePolyData(combine.GetOutput())
outputModel.CreateDefaultDisplayNodes()
# The filter creates a few scalars, don't show them by default, as they would be somewhat distracting
Expand Down
87 changes: 79 additions & 8 deletions CombineModels/Resources/UI/CombineModels.ui
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>279</width>
<height>372</height>
<width>350</width>
<height>582</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
Expand Down Expand Up @@ -199,6 +199,77 @@
</layout>
</widget>
</item>
<item>
<widget class="ctkCollapsibleButton" name="advancedCollapsibleButton">
<property name="text">
<string>Advanced</string>
</property>
<property name="collapsed">
<bool>true</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Number of retries:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="numberOfRetriesSpinBox">
<property name="toolTip">
<string/>
</property>
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Translate
randomly:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="translateRandomlySpinBox">
<property name="suffix">
<string>mm</string>
</property>
<property name="prefix">
<string>1e-</string>
</property>
<property name="maximum">
<number>5</number>
</property>
<property name="value">
<number>4</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Triangulate
inputs:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="triangulateInputsCheckBox">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="applyButton">
<property name="enabled">
Expand Down Expand Up @@ -241,12 +312,6 @@
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ctkCollapsibleButton</class>
<extends>QWidget</extends>
<header>ctkCollapsibleButton.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>qMRMLNodeComboBox</class>
<extends>QWidget</extends>
Expand All @@ -258,6 +323,12 @@
<header>qMRMLWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ctkCollapsibleButton</class>
<extends>QWidget</extends>
<header>ctkCollapsibleButton.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
Expand Down

0 comments on commit b726771

Please sign in to comment.