diff --git a/CombineModels/CombineModels.py b/CombineModels/CombineModels.py
index 70afac3..f220c43 100644
--- a/CombineModels/CombineModels.py
+++ b/CombineModels/CombineModels.py
@@ -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)
@@ -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
@@ -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):
@@ -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))
@@ -285,8 +321,21 @@ 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.
@@ -294,6 +343,9 @@ def process(self, inputModelA, inputModelB, outputModel, operation):
: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:
@@ -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
diff --git a/CombineModels/Resources/UI/CombineModels.ui b/CombineModels/Resources/UI/CombineModels.ui
index 2278914..306931b 100644
--- a/CombineModels/Resources/UI/CombineModels.ui
+++ b/CombineModels/Resources/UI/CombineModels.ui
@@ -6,8 +6,8 @@
0
0
- 279
- 372
+ 350
+ 582
@@ -199,6 +199,77 @@
+ -
+
+
+ Advanced
+
+
+ true
+
+
+
-
+
+
+ Number of retries:
+
+
+
+ -
+
+
+
+
+
+ 5
+
+
+ 2
+
+
+
+ -
+
+
+ Translate
+randomly:
+
+
+
+ -
+
+
+ mm
+
+
+ 1e-
+
+
+ 5
+
+
+ 4
+
+
+
+ -
+
+
+ Triangulate
+inputs:
+
+
+
+ -
+
+
+
+
+
+
+
+
+
-
@@ -241,12 +312,6 @@
-
- ctkCollapsibleButton
- QWidget
-
- 1
-
qMRMLNodeComboBox
QWidget
@@ -258,6 +323,12 @@
1
+
+ ctkCollapsibleButton
+ QWidget
+
+ 1
+