From d47e74fecb69c3cc1ea42b1b7316151db9a5b56c Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 24 Jan 2018 14:02:27 -0500 Subject: [PATCH 1/5] ENH: added Elastix registration (issue #334) * SliceTrackerRegistrationLogic expects registration algorithm as a parameter in order to be interchangeable * IRegistrationAlgorithm is the interface that needs to be implemented in order to add new registration algorithm * new configuration entries * adapted SliceTrackerRegistration widget and cli * gentle handling if preferred algorithm doesn't exist with giving notification and using fallback algorithm (default: BRAINS) TODO: use own parameter files needs to possible --- SliceTracker/Resources/default.cfg | 4 + SliceTracker/SliceTrackerRegistration.py | 254 +++++----------- .../algorithms/registration.py | 277 ++++++++++++++++++ .../SliceTrackerUtils/configuration.py | 6 + SliceTracker/SliceTrackerUtils/session.py | 46 +-- 5 files changed, 384 insertions(+), 203 deletions(-) create mode 100644 SliceTracker/SliceTrackerUtils/algorithms/registration.py diff --git a/SliceTracker/Resources/default.cfg b/SliceTracker/Resources/default.cfg index 28ced28..5f44abf 100644 --- a/SliceTracker/Resources/default.cfg +++ b/SliceTracker/Resources/default.cfg @@ -1,6 +1,10 @@ [ZFrame Registration] class: OpenSourceZFrameRegistration +[Prostate Registration] +Algorithm: BRAINSRegistration +Fallback: BRAINSRegistration + [Series Descriptions] PLANNING_IMAGE: ^(.*?)((a|A)(x|X))+(.*?)((t|T)(2))+ COVER_PROSTATE: COVER PROSTATE diff --git a/SliceTracker/SliceTrackerRegistration.py b/SliceTracker/SliceTrackerRegistration.py index e31bc7b..9c64175 100644 --- a/SliceTracker/SliceTrackerRegistration.py +++ b/SliceTracker/SliceTrackerRegistration.py @@ -1,11 +1,14 @@ import argparse, sys, os, logging import qt, slicer from slicer.ScriptedLoadableModule import * -from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin, ModuleWidgetMixin -from SliceTrackerUtils.sessionData import * -from SliceTrackerUtils.constants import SliceTrackerConstants + +from SlicerDevelopmentToolboxUtils.mixins import ModuleWidgetMixin from SlicerDevelopmentToolboxUtils.decorators import onReturnProcessEvents +from SliceTrackerUtils.constants import SliceTrackerConstants +from SliceTrackerUtils.sessionData import RegistrationResult +import SliceTrackerUtils.algorithms.registration as registration + class SliceTrackerRegistration(ScriptedLoadableModule): @@ -14,7 +17,7 @@ def __init__(self, parent): self.parent.title = "SliceTracker Registration" self.parent.categories = ["Radiology"] self.parent.dependencies = ["SlicerDevelopmentToolbox"] - self.parent.contributors = ["Peter Behringer (SPL), Christian Herz (SPL), Andriy Fedorov (SPL)"] + self.parent.contributors = ["Christian Herz (SPL), Peter Behringer (SPL), Andriy Fedorov (SPL)"] self.parent.helpText = """ SliceTracker Registration facilitates support of MRI-guided targeted prostate biopsy. """ self.parent.acknowledgementText = """Surgical Planning Laboratory, Brigham and Women's Hospital, Harvard Medical School, Boston, USA This work was supported in part by the National @@ -37,7 +40,8 @@ class SliceTrackerRegistrationWidget(ScriptedLoadableModuleWidget, ModuleWidgetM def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) - self.logic = SliceTrackerRegistrationLogic() + self.registrationAlgorithm = None + self.counter = 1 def setup(self): ScriptedLoadableModuleWidget.setup(self) @@ -59,27 +63,26 @@ def setup(self): self.fiducialSelector = self.createComboBox(nodeTypes=["vtkMRMLMarkupsFiducialNode", ""], noneEnabled=True, showChildNodeTypes=False, selectNodeUponCreation=False, toolTip="Select the Targets") - self.initialTransformSelector = self.createComboBox(nodeTypes=["vtkMRMLTransformNode", "vtkMRMLBSplineTransformNode", - "vtkMRMLLinearTransformNode", ""], - noneEnabled=True, - showChildNodeTypes=False, selectNodeUponCreation=False, - toolTip="Select the initial transform") + + self.algorithmSelector = qt.QComboBox() + self.algorithmSelector.addItems(registration.__algorithms__.keys()) + self.applyRegistrationButton = self.createButton("Run Registration") self.registrationGroupBoxLayout.addRow("Moving Image Volume: ", self.movingVolumeSelector) self.registrationGroupBoxLayout.addRow("Moving Label Volume: ", self.movingLabelSelector) self.registrationGroupBoxLayout.addRow("Fixed Image Volume: ", self.fixedVolumeSelector) self.registrationGroupBoxLayout.addRow("Fixed Label Volume: ", self.fixedLabelSelector) - self.registrationGroupBoxLayout.addRow("Initial Transform: ", self.initialTransformSelector) self.registrationGroupBoxLayout.addRow("Targets: ", self.fiducialSelector) + self.registrationGroupBoxLayout.addRow("Algorithm:", self.algorithmSelector) self.registrationGroupBoxLayout.addRow(self.applyRegistrationButton) self.layout.addWidget(self.registrationGroupBox) self.layout.addStretch() self.setupConnections() - self.updateButton() + self.onAlgorithmSelected(0) def setupConnections(self): self.applyRegistrationButton.clicked.connect(self.runRegistration) - self.movingVolumeSelector.connect('currentNodeChanged(bool)', self.updateButton) + self.algorithmSelector.currentIndexChanged.connect(self.onAlgorithmSelected) self.movingVolumeSelector.connect('currentNodeChanged(bool)', self.updateButton) self.fixedVolumeSelector.connect('currentNodeChanged(bool)', self.updateButton) self.fixedLabelSelector.connect('currentNodeChanged(bool)', self.updateButton) @@ -99,7 +102,17 @@ def updateButton(self): self.yellowCompositeNode.SetBackgroundVolumeID(self.fixedVolumeSelector.currentNode().GetID()) if self.fixedLabelSelector.currentNode(): self.yellowCompositeNode.SetLabelVolumeID(self.fixedLabelSelector.currentNode().GetID()) - self.applyRegistrationButton.enabled = self.isRegistrationPossible() + self.applyRegistrationButton.enabled = self.isRegistrationPossible() and self.registrationAlgorithm is not None + + def onAlgorithmSelected(self, index): + text = self.algorithmSelector.itemText(index) + algorithm = registration.__algorithms__[text] + if algorithm.isAlgorithmAvailable(): + self.registrationAlgorithm = algorithm + else: + logging.info("Selected algorithm {} seems not to be available due to missing dependencies".format(text)) + self.registrationAlgorithm = None + self.updateButton() def isRegistrationPossible(self): return self.movingVolumeSelector.currentNode() and self.fixedVolumeSelector.currentNode() and \ @@ -108,19 +121,19 @@ def isRegistrationPossible(self): def runRegistration(self): logging.debug("Starting Registration") self.progress = self.createProgressDialog(value=1, maximum=4) - parameterNode = slicer.vtkMRMLScriptedModuleNode() - parameterNode.SetAttribute('FixedImageNodeID', self.fixedVolumeSelector.currentNode().GetID()) - parameterNode.SetAttribute('FixedLabelNodeID', self.fixedLabelSelector.currentNode().GetID()) - parameterNode.SetAttribute('MovingImageNodeID', self.movingVolumeSelector.currentNode().GetID()) - parameterNode.SetAttribute('MovingLabelNodeID', self.movingLabelSelector.currentNode().GetID()) - if self.fiducialSelector.currentNode(): - parameterNode.SetAttribute('TargetsNodeID', self.fiducialSelector.currentNode().GetID()) - if self.initialTransformSelector.currentNode(): - parameterNode.SetAttribute('InitialTransformNodeID', self.initialTransformSelector.currentNode().GetID()) - self.logic.runReRegistration(parameterNode, progressCallback=self.updateProgressBar) - else: - self.logic.run(parameterNode, progressCallback=self.updateProgressBar) + + logic = SliceTrackerRegistrationLogic(self.registrationAlgorithm()) + + parameterNode = logic.initializeParameterNode(self.fixedVolumeSelector.currentNode(), + self.fixedLabelSelector.currentNode(), + self.movingVolumeSelector.currentNode(), + self.movingLabelSelector.currentNode(), + self.fiducialSelector.currentNode()) + + logic.run(parameterNode, result=RegistrationResult("{}: RegistrationResult".format(str(self.counter))), + progressCallback=self.updateProgressBar) self.progress.close() + self.counter += 1 @onReturnProcessEvents def updateProgressBar(self, **kwargs): @@ -130,165 +143,33 @@ def updateProgressBar(self, **kwargs): setattr(self.progress, key, value) -class SliceTrackerRegistrationLogic(ScriptedLoadableModuleLogic, ModuleLogicMixin): +class SliceTrackerRegistrationLogic(ScriptedLoadableModuleLogic): - counter = 1 + @staticmethod + def initializeParameterNode(fixedVolume, fixedLabel, movingVolume, movingLabel, targets=None): + parameterNode = slicer.vtkMRMLScriptedModuleNode() + parameterNode.SetAttribute('FixedImageNodeID', fixedVolume.GetID()) + parameterNode.SetAttribute('FixedLabelNodeID', fixedLabel.GetID()) + parameterNode.SetAttribute('MovingImageNodeID', movingVolume.GetID()) + parameterNode.SetAttribute('MovingLabelNodeID', movingLabel.GetID()) + if targets: + parameterNode.SetAttribute('TargetsNodeID', targets.GetID()) + return parameterNode - def __init__(self): + def __init__(self, algorithm): ScriptedLoadableModuleLogic.__init__(self) - self.registrationResult = None - - def _processParameterNode(self, parameterNode): - if not self.registrationResult: - self.registrationResult = RegistrationResult("01: RegistrationResult") - result = self.registrationResult - result.volumes.fixed = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('FixedImageNodeID')) - result.labels.fixed = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('FixedLabelNodeID')) - result.labels.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingLabelNodeID')) - movingVolume = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) - result.volumes.moving = self.volumesLogic.CloneVolume(slicer.mrmlScene, movingVolume, - "temp-movingVolume_" + str(self.counter)) - self.counter += 1 + self.registrationAlgorithm = algorithm - logging.debug("Fixed Image Name: %s" % result.volumes.fixed.GetName()) - logging.debug("Fixed Label Name: %s" % result.labels.fixed.GetName()) - logging.debug("Moving Image Name: %s" % movingVolume.GetName()) - logging.debug("Moving Label Name: %s" % result.labels.moving.GetName()) - initialTransform = parameterNode.GetAttribute('InitialTransformNodeID') - if initialTransform: - initialTransform = slicer.mrmlScene.GetNodeByID(initialTransform) - logging.debug("Initial Registration Name: %s" % initialTransform.GetName()) - return result - - def run(self, parameterNode, progressCallback=None): - self.progressCallback = progressCallback - result = self._processParameterNode(parameterNode) - - registrationTypes = ['rigid', 'affine', 'bSpline'] - self.createVolumeAndTransformNodes(registrationTypes, prefix=str(result.seriesNumber), suffix=result.suffix) - - self.doRigidRegistration(movingBinaryVolume=result.labels.moving, initializeTransformMode="useCenterOfROIAlign") - self.doAffineRegistration() - self.doBSplineRegistration(initialTransform=result.transforms.affine) - - targetsNodeID = parameterNode.GetAttribute('TargetsNodeID') - if targetsNodeID: - result.targets.original = slicer.mrmlScene.GetNodeByID(targetsNodeID) - self.transformTargets(registrationTypes, result.targets.original, str(result.seriesNumber), suffix=result.suffix) - result.volumes.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) - - def runReRegistration(self, parameterNode, progressCallback=None): - logging.debug("Starting Re-Registration") - - self.progressCallback = progressCallback - - self._processParameterNode(parameterNode) - result = self.registrationResult - - registrationTypes = ['rigid', 'bSpline'] - self.createVolumeAndTransformNodes(registrationTypes, prefix=str(result.seriesNumber), suffix=result.suffix) - initialTransform = parameterNode.GetAttribute('InitialTransformNodeID') - - if initialTransform: - initialTransform = slicer.mrmlScene.GetNodeByID(initialTransform) - - # TODO: label value should be delivered by parameterNode - self.dilateMask(result.labels.fixed, dilateValue=1) - self.doRigidRegistration(movingBinaryVolume=result.labels.moving, - initialTransform=initialTransform if initialTransform else None) - self.doBSplineRegistration(initialTransform=result.transforms.rigid, useScaleVersor3D=True, useScaleSkewVersor3D=True, - useAffine=True) - - targetsNodeID = parameterNode.GetAttribute('TargetsNodeID') - if targetsNodeID: - result.targets.original = slicer.mrmlScene.GetNodeByID(targetsNodeID) - self.transformTargets(registrationTypes, result.originalTargets, str(result.seriesNumber), suffix=result.suffix) - result.movingVolume = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) - - def createVolumeAndTransformNodes(self, registrationTypes, prefix, suffix=""): - for regType in registrationTypes: - self.registrationResult.setVolume(regType, self.createScalarVolumeNode(prefix + '-VOLUME-' + regType + suffix)) - transformName = prefix + '-TRANSFORM-' + regType + suffix - transform = self.createBSplineTransformNode(transformName) if regType == 'bSpline' \ - else self.createLinearTransformNode(transformName) - self.registrationResult.setTransform(regType, transform) - - def transformTargets(self, registrations, targets, prefix, suffix=""): - if targets: - for registration in registrations: - name = prefix + '-TARGETS-' + registration + suffix - clone = self.cloneFiducialAndTransform(name, targets, self.registrationResult.getTransform(registration)) - clone.SetLocked(True) - self.registrationResult.setTargets(registration, clone) - - def cloneFiducialAndTransform(self, cloneName, originalTargets, transformNode): - tfmLogic = slicer.modules.transforms.logic() - clonedTargets = self.cloneFiducials(originalTargets, cloneName) - clonedTargets.SetAndObserveTransformNodeID(transformNode.GetID()) - tfmLogic.hardenTransform(clonedTargets) - return clonedTargets - - def doRigidRegistration(self, **kwargs): - self.updateProgress(labelText='\nRigid registration', value=2) - paramsRigid = {'fixedVolume': self.registrationResult.volumes.fixed, - 'movingVolume': self.registrationResult.volumes.moving, - 'fixedBinaryVolume': self.registrationResult.labels.fixed, - 'outputTransform': self.registrationResult.transforms.rigid.GetID(), - 'outputVolume': self.registrationResult.volumes.rigid.GetID(), - 'maskProcessingMode': "ROI", - 'useRigid': True} - for key, value in kwargs.iteritems(): - paramsRigid[key] = value - slicer.cli.run(slicer.modules.brainsfit, None, paramsRigid, wait_for_completion=True) - self.registrationResult.cmdArguments += "Rigid Registration Parameters: %s" % str(paramsRigid) + "\n\n" - - def doAffineRegistration(self): - self.updateProgress(labelText='\nAffine registration', value=2) - paramsAffine = {'fixedVolume': self.registrationResult.volumes.fixed, - 'movingVolume': self.registrationResult.volumes.moving, - 'fixedBinaryVolume': self.registrationResult.labels.fixed, - 'movingBinaryVolume': self.registrationResult.labels.moving, - 'outputTransform': self.registrationResult.transforms.affine.GetID(), - 'outputVolume': self.registrationResult.volumes.affine.GetID(), - 'maskProcessingMode': "ROI", - 'useAffine': True, - 'initialTransform': self.registrationResult.transforms.rigid} - slicer.cli.run(slicer.modules.brainsfit, None, paramsAffine, wait_for_completion=True) - self.registrationResult.cmdArguments += "Affine Registration Parameters: %s" % str(paramsAffine) + "\n\n" - - def doBSplineRegistration(self, initialTransform, **kwargs): - self.updateProgress(labelText='\nBSpline registration', value=3) - paramsBSpline = {'fixedVolume': self.registrationResult.volumes.fixed, - 'movingVolume': self.registrationResult.volumes.moving, - 'outputVolume': self.registrationResult.volumes.bSpline.GetID(), - 'bsplineTransform': self.registrationResult.transforms.bSpline.GetID(), - 'fixedBinaryVolume': self.registrationResult.labels.fixed, - 'movingBinaryVolume': self.registrationResult.labels.moving, - 'useROIBSpline': True, - 'useBSpline': True, - 'splineGridSize': "3,3,3", - 'maskProcessing': "ROI", - 'minimumStepLength': "0.005", - 'maximumStepLength': "0.2", - 'costFunctionConvergenceFactor': "1.00E+09", - 'maskProcessingMode': "ROI", - 'initialTransform': initialTransform} - for key, value in kwargs.iteritems(): - paramsBSpline[key] = value - - slicer.cli.run(slicer.modules.brainsfit, None, paramsBSpline, wait_for_completion=True) - self.registrationResult.cmdArguments += "BSpline Registration Parameters: %s" % str(paramsBSpline) + "\n\n" - - self.updateProgress(labelText='\nCompleted registration', value=4) - - def updateProgress(self, **kwargs): - if self.progressCallback: - self.progressCallback(**kwargs) + def run(self, parameterNode, result, progressCallback=None): + self.registrationAlgorithm.run(parameterNode, result, progressCallback) + + def getResult(self): + return self.registrationAlgorithm.registrationResult def main(argv): try: - parser = argparse.ArgumentParser(description="Slicetracker Registration") + parser = argparse.ArgumentParser(description="SliceTracker Registration") parser.add_argument("-fl", "--fixed-label", dest="fixed_label", metavar="PATH", default="-", required=True, help="Fixed label to be used for registration") parser.add_argument("-ml", "--moving-label", dest="moving_label", metavar="PATH", default="-", required=True, @@ -301,6 +182,9 @@ def main(argv): required=False, help="Initial rigid transform for re-registration") parser.add_argument("-o", "--output-directory", dest="output_directory", metavar="PATH", default="-", required=False, help="Output directory for registration result") + parser.add_argument("-al", "--algorithm", dest="algorithm", metavar="PATH", default="BRAINS", + choices=registration.__algorithms__.keys(), required=False, + help="Algorithm to be used for registration (default: %(default)s)") args = parser.parse_args(argv) @@ -313,17 +197,17 @@ def main(argv): success, fixedVolume = slicer.util.loadVolume(args.fixed_volume, returnNode=True) success, movingVolume = slicer.util.loadVolume(args.moving_volume, returnNode=True) - parameterNode = slicer.vtkMRMLScriptedModuleNode() - parameterNode.SetAttribute('FixedImageNodeID', fixedVolume.GetID()) - parameterNode.SetAttribute('FixedLabelNodeID', fixedLabel.GetID()) - parameterNode.SetAttribute('MovingImageNodeID', movingVolume.GetID()) - parameterNode.SetAttribute('MovingLabelNodeID', movingLabel.GetID()) + algorithm = registration.__algorithms__[args.algorithm] + + if not algorithm.isAlgorithmAvailable(): + raise RuntimeError("Registration algorithm {} cannot be executed due to missing dependencies.".format(args.algorithm)) - logic = SliceTrackerRegistrationLogic() - logic.run(parameterNode) + logic = SliceTrackerRegistrationLogic(algorithm()) + parameterNode = logic.initializeParameterNode(fixedVolume, fixedLabel, movingVolume, movingLabel) + logic.run(parameterNode, result=RegistrationResult("01: RegistrationResult")) if args.output_directory != "-": - logic.registrationResult.save(args.output_directory) + logic.getResult().save(args.output_directory) except Exception, e: print e diff --git a/SliceTracker/SliceTrackerUtils/algorithms/registration.py b/SliceTracker/SliceTrackerUtils/algorithms/registration.py new file mode 100644 index 0000000..dc64f92 --- /dev/null +++ b/SliceTracker/SliceTrackerUtils/algorithms/registration.py @@ -0,0 +1,277 @@ +from abc import abstractmethod, ABCMeta +import logging +import slicer + +from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin + + +class IRegistrationAlgorithm(ModuleLogicMixin): + + __metaclass__ = ABCMeta + + NAME = None + + @staticmethod + def isAlgorithmAvailable(): + raise NotImplementedError + + def __init__(self): + self.registrationResult = None + self.progressCallback = None + if not self.NAME: + raise NotImplementedError("Concrete implementations of {} need to have a NAME assigned".format(self.__class__.__name__)) + + @abstractmethod + def run(self, parameterNode, registrationResult, progressCallback=None): + pass + + def _processParameterNode(self, parameterNode): + result = self.registrationResult + result.volumes.fixed = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('FixedImageNodeID')) + result.labels.fixed = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('FixedLabelNodeID')) + result.labels.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingLabelNodeID')) + movingVolume = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) + result.volumes.moving = self.volumesLogic.CloneVolume(slicer.mrmlScene, movingVolume, + "{}-temp-movingVolume".format(movingVolume.GetName())) + + logging.debug("Fixed Image Name: %s" % result.volumes.fixed.GetName()) + logging.debug("Fixed Label Name: %s" % result.labels.fixed.GetName()) + logging.debug("Moving Image Name: %s" % movingVolume.GetName()) + logging.debug("Moving Label Name: %s" % result.labels.moving.GetName()) + initialTransform = parameterNode.GetAttribute('InitialTransformNodeID') + if initialTransform: + initialTransform = slicer.mrmlScene.GetNodeByID(initialTransform) + logging.debug("Initial Registration Name: %s" % initialTransform.GetName()) + + def createVolumeAndTransformNodes(self, registrationTypes, prefix, suffix="", + deformableRegistrationNodeClass=slicer.vtkMRMLBSplineTransformNode): + for regType in registrationTypes: + self.registrationResult.setVolume(regType, self.createScalarVolumeNode(prefix + '-VOLUME-' + regType + suffix)) + transformName = prefix + '-TRANSFORM-' + regType + suffix + transform = self.createNode(deformableRegistrationNodeClass, transformName) if regType == 'bSpline' \ + else self.createLinearTransformNode(transformName) + self.registrationResult.setTransform(regType, transform) + + def transformTargets(self, registrations, targets, prefix, suffix=""): + if targets: + for registration in registrations: + name = prefix + '-TARGETS-' + registration + suffix + clone = self.cloneFiducialAndTransform(name, targets, self.registrationResult.getTransform(registration)) + clone.SetLocked(True) + self.registrationResult.setTargets(registration, clone) + + def cloneFiducialAndTransform(self, cloneName, originalTargets, transformNode): + tfmLogic = slicer.modules.transforms.logic() + clonedTargets = self.cloneFiducials(originalTargets, cloneName) + clonedTargets.SetAndObserveTransformNodeID(transformNode.GetID()) + tfmLogic.hardenTransform(clonedTargets) + return clonedTargets + + def updateProgress(self, **kwargs): + if self.progressCallback: + self.progressCallback(**kwargs) + + +class BRAINSRegistration(IRegistrationAlgorithm): + + NAME = "BRAINSFit" + + @staticmethod + def isAlgorithmAvailable(): + return hasattr(slicer.modules, "brainsfit") + + def __init__(self): + super(BRAINSRegistration, self).__init__() + + def run(self, parameterNode, result, progressCallback=None): + self.progressCallback = progressCallback + self.registrationResult = result + self._processParameterNode(parameterNode) + + registrationTypes = ['rigid', 'affine', 'bSpline'] + self.createVolumeAndTransformNodes(registrationTypes, prefix=str(result.seriesNumber), suffix=result.suffix) + + self.__runRigidRegistration() + self.__runAffineRegistration() + self.__runBSplineRegistration() + + targetsNodeID = parameterNode.GetAttribute('TargetsNodeID') + if targetsNodeID: + result.targets.original = slicer.mrmlScene.GetNodeByID(targetsNodeID) + self.transformTargets(registrationTypes, result.targets.original, str(result.seriesNumber), suffix=result.suffix) + slicer.mrmlScene.RemoveNode(result.volumes.moving) + result.volumes.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) + return result + + def __runRigidRegistration(self): + self.updateProgress(labelText='\nRigid registration', value=2) + reg = BRAINSRigidRegistration(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, + self.registrationResult.labels.fixed, self.registrationResult.labels.moving, + self.registrationResult.transforms.rigid, self.registrationResult.volumes.rigid) + reg.run() + self.registrationResult.cmdArguments += reg.params + + def __runAffineRegistration(self): + self.updateProgress(labelText='\nAffine registration', value=2) + reg = BRAINSAffineRegistration(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, + self.registrationResult.labels.fixed, self.registrationResult.labels.moving, + self.registrationResult.transforms.affine, self.registrationResult.volumes.affine, + self.registrationResult.transforms.rigid) + reg.run() + self.registrationResult.cmdArguments += reg.params + + def __runBSplineRegistration(self): + self.updateProgress(labelText='\nBSpline registration', value=3) + + reg = BRAINSBSplineRegistration(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, + self.registrationResult.labels.fixed, self.registrationResult.labels.moving, + self.registrationResult.transforms.bSpline, self.registrationResult.volumes.bSpline, + self.registrationResult.transforms.affine) + reg.run() + self.registrationResult.cmdArguments += reg.params + + self.updateProgress(labelText='\nCompleted registration', value=4) + + +class IBRAINSRegistrationType(object): + + __metaclass__ = ABCMeta + + def __init__(self, fixedVolume, movingVolume, fixedLabel, movingLabel, outputTransform, outputVolume, + initialTransform=None): + self.fixedVolume = fixedVolume + self.movingVolume = movingVolume + self.fixedLabel = fixedLabel + self.movingLabel = movingLabel + self.outputTransform = outputTransform + self.outputVolume = outputVolume + self.initialTransform = initialTransform + self.params = None + + @abstractmethod + def run(self): + pass + + def getGeneralParams(self): + return {'fixedVolume': self.fixedVolume, + 'movingVolume': self.movingVolume, + 'fixedBinaryVolume': self.fixedLabel, + 'movingBinaryVolume': self.movingLabel, + 'outputTransform': self.outputTransform.GetID(), + 'outputVolume': self.outputVolume.GetID()} + + +class BRAINSRigidRegistration(IBRAINSRegistrationType): + + def run(self): + params = {'maskProcessingMode': "ROI", + 'initializeTransformMode': "useCenterOfROIAlign", + 'useRigid': True} + params.update(self.getGeneralParams()) + slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) + self.params = "Rigid Registration Parameters: %s" % str(params) + "\n\n" + + +class BRAINSAffineRegistration(IBRAINSRegistrationType): + + def run(self): + params = {'maskProcessingMode': "ROI", + 'useAffine': True, + 'initialTransform': self.initialTransform} + params.update(self.getGeneralParams()) + slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) + self.params = "Affine Registration Parameters: %s" % str(params) + "\n\n" + + +class BRAINSBSplineRegistration(IBRAINSRegistrationType): + + def run(self): + params = {'useROIBSpline': True, + 'useBSpline': True, + 'splineGridSize': "3,3,3", + 'maskProcessing': "ROI", + 'minimumStepLength': "0.005", + 'maximumStepLength': "0.2", + 'costFunctionConvergenceFactor': "1.00E+09", + 'maskProcessingMode': "ROI", + 'initialTransform': self.initialTransform} + params.update(self.getGeneralParams()) + slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) + self.params = "BSpline Registration Parameters: %s" % str(params) + "\n\n" + + +class ElastixRegistration(IRegistrationAlgorithm): + + NAME = "Elastix" + + @staticmethod + def isAlgorithmAvailable(): + try: + from Elastix import ElastixLogic + except ImportError: + return False + return True + + def hardenTransform(self, nodes, transform): + tfmLogic = slicer.modules.transforms.logic() + for node in nodes: + node.SetAndObserveTransformNodeID(transform.GetID()) + tfmLogic.hardenTransform(node) + + def run(self, parameterNode, result, progressCallback=None): + self.progressCallback = progressCallback + self.registrationResult = result + self._processParameterNode(parameterNode) + + registrationTypes = ['rigid', 'bSpline'] + self.createVolumeAndTransformNodes(registrationTypes, prefix=str(result.seriesNumber), suffix=result.suffix, + deformableRegistrationNodeClass=slicer.vtkMRMLTransformNode) + + self.__runRigidBRAINSRegistration() + self.hardenTransform(nodes=[self.registrationResult.volumes.moving, self.registrationResult.labels.moving], + transform=self.registrationResult.transforms.rigid) + self.__runElastixRegistration() + + self.hardenTransform(nodes=[self.registrationResult.transforms.bSpline], + transform=self.registrationResult.transforms.rigid) + + self.registrationResult.transforms.rigid.Inverse() + self.hardenTransform(nodes=[self.registrationResult.volumes.moving, self.registrationResult.labels.moving], + transform=self.registrationResult.transforms.rigid) + self.registrationResult.transforms.rigid.Inverse() + + targetsNodeID = parameterNode.GetAttribute('TargetsNodeID') + if targetsNodeID: + result.targets.original = slicer.mrmlScene.GetNodeByID(targetsNodeID) + self.transformTargets(registrationTypes, result.targets.original, str(result.seriesNumber), suffix=result.suffix) + slicer.mrmlScene.RemoveNode(result.volumes.moving) + result.volumes.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) + + def __runRigidBRAINSRegistration(self): + self.updateProgress(labelText='\nRigid registration', value=2) + + rigidRegistration = BRAINSRigidRegistration(self.registrationResult.volumes.fixed, + self.registrationResult.volumes.moving, + self.registrationResult.labels.fixed, + self.registrationResult.labels.moving, + self.registrationResult.transforms.rigid, + self.registrationResult.volumes.rigid) + rigidRegistration.run() + + self.registrationResult.cmdArguments += rigidRegistration.params + + def __runElastixRegistration(self): + self.updateProgress(labelText='\nElastix registration', value=3) + from Elastix import ElastixLogic, RegistrationPresets_ParameterFilenames + logic = ElastixLogic() + parameterFileNames = logic.getRegistrationPresets()[0][RegistrationPresets_ParameterFilenames] + print RegistrationPresets_ParameterFilenames + logic.registerVolumes(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, + parameterFileNames, self.registrationResult.volumes.bSpline, + self.registrationResult.transforms.bSpline, + self.registrationResult.labels.fixed, self.registrationResult.labels.moving) + self.updateProgress(labelText='\nCompleted registration', value=4) + + +__algorithms__ = {'BRAINS':BRAINSRegistration, 'Elastix':ElastixRegistration} + diff --git a/SliceTracker/SliceTrackerUtils/configuration.py b/SliceTracker/SliceTrackerUtils/configuration.py index 08358ed..6748166 100644 --- a/SliceTracker/SliceTrackerUtils/configuration.py +++ b/SliceTracker/SliceTrackerUtils/configuration.py @@ -18,6 +18,12 @@ def loadConfiguration(self): if not self.getSetting("ZFrame_Registration_Class_Name"): self.setSetting("ZFrame_Registration_Class_Name", config.get('ZFrame Registration', 'class')) + if not self.getSetting("Prostate_Registration_Algorithm"): + self.setSetting("Prostate_Registration_Algorithm", config.get('Prostate Registration', 'Algorithm')) + + if not self.getSetting("Prostate_Registration_Fallback"): + self.setSetting("Prostate_Registration_Fallback", config.get('Prostate Registration', 'Fallback')) + if not self.getSetting("PLANNING_IMAGE"): self.setSetting("PLANNING_IMAGE", config.get('Series Descriptions', 'PLANNING_IMAGE')) if not self.getSetting("COVER_PROSTATE"): diff --git a/SliceTracker/SliceTrackerUtils/session.py b/SliceTracker/SliceTrackerUtils/session.py index a3f3b50..76afe1f 100644 --- a/SliceTracker/SliceTrackerUtils/session.py +++ b/SliceTracker/SliceTrackerUtils/session.py @@ -1,4 +1,5 @@ import os, logging +import sys import vtk, ctk, ast import qt import xml @@ -26,6 +27,7 @@ from SliceTrackerRegistration import SliceTrackerRegistrationLogic from steps.plugins.segmentationValidator import SliceTrackerSegmentationValidatorPlugin +from .algorithms.registration import * @singleton @@ -198,7 +200,6 @@ def setSelectedTarget(self, info): def __init__(self): StepBasedSession.__init__(self) - self.registrationLogic = SliceTrackerRegistrationLogic() self.seriesTypeManager = SeriesTypeManager() self.seriesTypeManager.addEventObserver(self.seriesTypeManager.SeriesTypeManuallyAssignedEvent, lambda caller, event: self.invokeEvent(self.SeriesTypeManuallyAssignedEvent)) @@ -673,7 +674,6 @@ def generateNameAndCreateRegistrationResult(self, fixedVolume): name, suffix = self.getRegistrationResultNameAndGeneratedSuffix(fixedVolume.GetName()) result = self.data.createResult(name + suffix) result.suffix = suffix - self.registrationLogic.registrationResult = result return result def applyInitialRegistration(self, retryMode, segmentationData, progressCallback=None): @@ -682,8 +682,10 @@ def applyInitialRegistration(self, retryMode, segmentationData, progressCallback self.runBRAINSResample(inputVolume=self.fixedLabel, referenceVolume=self.fixedVolume, outputVolume=self.fixedLabel) - self._runRegistration(self.fixedVolume, self.fixedLabel, self.movingVolume, - self.movingLabel, self.movingTargets, segmentationData, progressCallback) + parameterNode = SliceTrackerRegistrationLogic.initializeParameterNode(self.fixedVolume, self.fixedLabel, + self.movingVolume, self.movingLabel, + self.movingTargets) + self._runRegistration(parameterNode, segmentationData, progressCallback) def applyRegistration(self, progressCallback=None): @@ -692,7 +694,6 @@ def applyRegistration(self, progressCallback=None): lastApprovedTfm = self.data.getMostRecentApprovedTransform() initialTransform = lastApprovedTfm if lastApprovedTfm else lastRigidTfm - fixedLabel = self.volumesLogic.CreateAndAddLabelVolume(slicer.mrmlScene, self.currentSeriesVolume, self.currentSeriesVolume.GetName() + '-label') @@ -700,30 +701,39 @@ def applyRegistration(self, progressCallback=None): outputVolume=fixedLabel, warpTransform=initialTransform) self.dilateMask(fixedLabel, dilateValue=self.segmentedLabelValue) - self._runRegistration(self.currentSeriesVolume, fixedLabel, coverProstateRegResult.volumes.fixed, - coverProstateRegResult.labels.fixed, coverProstateRegResult.targets.approved, None, - progressCallback) - - def _runRegistration(self, fixedVolume, fixedLabel, movingVolume, movingLabel, targets, segmentationData, - progressCallback): + parameterNode = SliceTrackerRegistrationLogic.initializeParameterNode(self.currentSeriesVolume, fixedLabel, + coverProstateRegResult.volumes.fixed, + coverProstateRegResult.labels.fixed, + coverProstateRegResult.targets.approved) + self._runRegistration(parameterNode, None, progressCallback) + + def _runRegistration(self, parameterNode, segmentationData, progressCallback): + fixedVolume = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('FixedImageNodeID')) result = self.generateNameAndCreateRegistrationResult(fixedVolume) result.receivedTime = self.seriesTimeStamps[result.name.replace(result.suffix, "")] if segmentationData: result.segmentationData = segmentationData - parameterNode = slicer.vtkMRMLScriptedModuleNode() - parameterNode.SetAttribute('FixedImageNodeID', fixedVolume.GetID()) - parameterNode.SetAttribute('FixedLabelNodeID', fixedLabel.GetID()) - parameterNode.SetAttribute('MovingImageNodeID', movingVolume.GetID()) - parameterNode.SetAttribute('MovingLabelNodeID', movingLabel.GetID()) - parameterNode.SetAttribute('TargetsNodeID', targets.GetID()) result.startTime = self.getTime() - self.registrationLogic.run(parameterNode, progressCallback=progressCallback) + registrationLogic = self.getRegistrationLogic() + registrationLogic.run(parameterNode, result, progressCallback=progressCallback) result.endTime = self.getTime() + self.addTargetsToMRMLScene(result) if self.seriesTypeManager.isCoverProstate(self.currentSeries) and self.temporaryIntraopTargets: self.addTemporaryTargetsToResult(result) self.invokeEvent(self.InitiateEvaluationEvent) + def getRegistrationLogic(self): + preferred = self.getSetting("Prostate_Registration_Algorithm") + fallback = self.getSetting("Prostate_Registration_Fallback") + registrationClass = getattr(sys.modules[__name__], preferred) + if not registrationClass.isAlgorithmAvailable(): + registrationClass = getattr(sys.modules[__name__], fallback) + if not registrationClass.isAlgorithmAvailable(): + raise RuntimeError("Neither preferred registration algorithm: {}, nor fallback registration algorithm {}" + "are available. Make sure to install required dependencies".format(preferred, fallback)) + return SliceTrackerRegistrationLogic(registrationClass()) + def addTargetsToMRMLScene(self, result): targetNodes = result.targets.asDict() for regType in RegistrationTypeData.RegistrationTypes: From 0ba87b82cbce2ec1ae2396632122d42adb82b800 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 24 Jan 2018 14:36:37 -0500 Subject: [PATCH 2/5] ENH: added SliceTracker registration class diagram --- puml/SliceTrackerRegistration.puml | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 puml/SliceTrackerRegistration.puml diff --git a/puml/SliceTrackerRegistration.puml b/puml/SliceTrackerRegistration.puml new file mode 100644 index 0000000..f7c009e --- /dev/null +++ b/puml/SliceTrackerRegistration.puml @@ -0,0 +1,66 @@ +@startuml + +class SliceTrackerRegistrationLogic { + + IRegistrationAlgorithm algorithm; + -- + + run() +} + +IRegistrationAlgorithm <-- SliceTrackerRegistrationLogic::algorithm: has_a + + +SliceTrackerRegistrationLogic <-- SliceTrackerRegistrationWidget: uses +SliceTrackerRegistrationLogic <-- CLI: uses + + +interface IRegistrationAlgorithm { + + NAME: None + + {static} isAlgorithmAvailable() + + {abstract} run(parameterNode, registrationResult, progressCallback=None) + # _processParameterNode(parameterNode) + + updateProgress(**kwargs) +} + +class BRAINRegistration { + + NAME="BRAINSFit" + + isAlgorithmAvailable() + + run(self, parameterNode, result, progressCallback=None) + - __runRigidRegistration() + - __runAffineRegistration() + - __runBSplineRegistration() +} + +class ElastixRegistration { + + NAME="Elastix" + + isAlgorithmAvailable() + + run(self, parameterNode, result, progressCallback=None) + - __runRigidBRAINSRegistration() + - __runElastixRegistration() +} + +BRAINSRigidRegistration <-- ElastixRegistration::__runRigidBRAINSRegistration: uses + +BRAINSRigidRegistration <-- BRAINRegistration::__runRigidRegistration: uses +BRAINSAffineRegistration <-- BRAINRegistration::__runAffineRegistration: uses +BRAINSBSplineRegistration <-- BRAINRegistration::__runBSplineRegistration: uses + +IRegistrationAlgorithm <|-- BRAINRegistration: is_a +IRegistrationAlgorithm <|-- ElastixRegistration: is_a + +interface IBRAINSRegistrationType{ + + fixedVolume + + movingVolume + + fixedLabel + + movingLabel + + outputTransform + + outputVolume + + initialTransform=None + + {abstract} run() + + getGeneralParams() +} + +IBRAINSRegistrationType <|-- BRAINSRigidRegistration : is_a +IBRAINSRegistrationType <|-- BRAINSAffineRegistration : is_a +IBRAINSRegistrationType <|-- BRAINSBSplineRegistration : is_a + +@enduml \ No newline at end of file From 768f6ddec3baef9cffcf9612502fc77e27f2d663 Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 24 Jan 2018 16:54:12 -0500 Subject: [PATCH 3/5] ENH: renamed interface to ImageRegistrationTool (issue #334) --- SliceTracker/Resources/default.cfg | 2 +- SliceTracker/SliceTrackerRegistration.py | 42 +++++++++---------- .../algorithms/registration.py | 14 +++---- .../SliceTrackerUtils/configuration.py | 4 +- SliceTracker/SliceTrackerUtils/session.py | 13 +++--- puml/SliceTrackerRegistration.puml | 23 ++++++---- 6 files changed, 52 insertions(+), 46 deletions(-) diff --git a/SliceTracker/Resources/default.cfg b/SliceTracker/Resources/default.cfg index 5f44abf..24e99b1 100644 --- a/SliceTracker/Resources/default.cfg +++ b/SliceTracker/Resources/default.cfg @@ -2,7 +2,7 @@ class: OpenSourceZFrameRegistration [Prostate Registration] -Algorithm: BRAINSRegistration +Tool: BRAINSRegistration Fallback: BRAINSRegistration [Series Descriptions] diff --git a/SliceTracker/SliceTrackerRegistration.py b/SliceTracker/SliceTrackerRegistration.py index 9c64175..ec4e676 100644 --- a/SliceTracker/SliceTrackerRegistration.py +++ b/SliceTracker/SliceTrackerRegistration.py @@ -40,7 +40,7 @@ class SliceTrackerRegistrationWidget(ScriptedLoadableModuleWidget, ModuleWidgetM def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) - self.registrationAlgorithm = None + self.imageRegistrationTool = None self.counter = 1 def setup(self): @@ -64,8 +64,8 @@ def setup(self): showChildNodeTypes=False, selectNodeUponCreation=False, toolTip="Select the Targets") - self.algorithmSelector = qt.QComboBox() - self.algorithmSelector.addItems(registration.__algorithms__.keys()) + self.imageRegistrationToolSelector = qt.QComboBox() + self.imageRegistrationToolSelector.addItems(registration.__tools__.keys()) self.applyRegistrationButton = self.createButton("Run Registration") self.registrationGroupBoxLayout.addRow("Moving Image Volume: ", self.movingVolumeSelector) @@ -73,16 +73,16 @@ def setup(self): self.registrationGroupBoxLayout.addRow("Fixed Image Volume: ", self.fixedVolumeSelector) self.registrationGroupBoxLayout.addRow("Fixed Label Volume: ", self.fixedLabelSelector) self.registrationGroupBoxLayout.addRow("Targets: ", self.fiducialSelector) - self.registrationGroupBoxLayout.addRow("Algorithm:", self.algorithmSelector) + self.registrationGroupBoxLayout.addRow("Image Registration Tool:", self.imageRegistrationToolSelector) self.registrationGroupBoxLayout.addRow(self.applyRegistrationButton) self.layout.addWidget(self.registrationGroupBox) self.layout.addStretch() self.setupConnections() - self.onAlgorithmSelected(0) + self.onImageRegistrationToolSelected(0) def setupConnections(self): self.applyRegistrationButton.clicked.connect(self.runRegistration) - self.algorithmSelector.currentIndexChanged.connect(self.onAlgorithmSelected) + self.imageRegistrationToolSelector.currentIndexChanged.connect(self.onImageRegistrationToolSelected) self.movingVolumeSelector.connect('currentNodeChanged(bool)', self.updateButton) self.fixedVolumeSelector.connect('currentNodeChanged(bool)', self.updateButton) self.fixedLabelSelector.connect('currentNodeChanged(bool)', self.updateButton) @@ -102,16 +102,16 @@ def updateButton(self): self.yellowCompositeNode.SetBackgroundVolumeID(self.fixedVolumeSelector.currentNode().GetID()) if self.fixedLabelSelector.currentNode(): self.yellowCompositeNode.SetLabelVolumeID(self.fixedLabelSelector.currentNode().GetID()) - self.applyRegistrationButton.enabled = self.isRegistrationPossible() and self.registrationAlgorithm is not None + self.applyRegistrationButton.enabled = self.isRegistrationPossible() and self.imageRegistrationTool is not None - def onAlgorithmSelected(self, index): - text = self.algorithmSelector.itemText(index) - algorithm = registration.__algorithms__[text] - if algorithm.isAlgorithmAvailable(): - self.registrationAlgorithm = algorithm + def onImageRegistrationToolSelected(self, index): + text = self.imageRegistrationToolSelector.itemText(index) + imageRegistrationTool = registration.__tools__[text] + if imageRegistrationTool.isToolAvailable(): + self.imageRegistrationTool = imageRegistrationTool else: logging.info("Selected algorithm {} seems not to be available due to missing dependencies".format(text)) - self.registrationAlgorithm = None + self.imageRegistrationTool = None self.updateButton() def isRegistrationPossible(self): @@ -122,7 +122,7 @@ def runRegistration(self): logging.debug("Starting Registration") self.progress = self.createProgressDialog(value=1, maximum=4) - logic = SliceTrackerRegistrationLogic(self.registrationAlgorithm()) + logic = SliceTrackerRegistrationLogic(self.imageRegistrationTool()) parameterNode = logic.initializeParameterNode(self.fixedVolumeSelector.currentNode(), self.fixedLabelSelector.currentNode(), @@ -158,13 +158,13 @@ def initializeParameterNode(fixedVolume, fixedLabel, movingVolume, movingLabel, def __init__(self, algorithm): ScriptedLoadableModuleLogic.__init__(self) - self.registrationAlgorithm = algorithm + self.imageRegistrationTool = algorithm def run(self, parameterNode, result, progressCallback=None): - self.registrationAlgorithm.run(parameterNode, result, progressCallback) + self.imageRegistrationTool.run(parameterNode, result, progressCallback) def getResult(self): - return self.registrationAlgorithm.registrationResult + return self.imageRegistrationTool.registrationResult def main(argv): @@ -183,7 +183,7 @@ def main(argv): parser.add_argument("-o", "--output-directory", dest="output_directory", metavar="PATH", default="-", required=False, help="Output directory for registration result") parser.add_argument("-al", "--algorithm", dest="algorithm", metavar="PATH", default="BRAINS", - choices=registration.__algorithms__.keys(), required=False, + choices=registration.__tools__.keys(), required=False, help="Algorithm to be used for registration (default: %(default)s)") args = parser.parse_args(argv) @@ -197,12 +197,12 @@ def main(argv): success, fixedVolume = slicer.util.loadVolume(args.fixed_volume, returnNode=True) success, movingVolume = slicer.util.loadVolume(args.moving_volume, returnNode=True) - algorithm = registration.__algorithms__[args.algorithm] + imageRegistrationTool = registration.__tools__[args.algorithm] - if not algorithm.isAlgorithmAvailable(): + if not imageRegistrationTool.isToolAvailable(): raise RuntimeError("Registration algorithm {} cannot be executed due to missing dependencies.".format(args.algorithm)) - logic = SliceTrackerRegistrationLogic(algorithm()) + logic = SliceTrackerRegistrationLogic(imageRegistrationTool()) parameterNode = logic.initializeParameterNode(fixedVolume, fixedLabel, movingVolume, movingLabel) logic.run(parameterNode, result=RegistrationResult("01: RegistrationResult")) diff --git a/SliceTracker/SliceTrackerUtils/algorithms/registration.py b/SliceTracker/SliceTrackerUtils/algorithms/registration.py index dc64f92..a6263a7 100644 --- a/SliceTracker/SliceTrackerUtils/algorithms/registration.py +++ b/SliceTracker/SliceTrackerUtils/algorithms/registration.py @@ -5,14 +5,14 @@ from SlicerDevelopmentToolboxUtils.mixins import ModuleLogicMixin -class IRegistrationAlgorithm(ModuleLogicMixin): +class ImageRegistrationTool(ModuleLogicMixin): __metaclass__ = ABCMeta NAME = None @staticmethod - def isAlgorithmAvailable(): + def isToolAvailable(): raise NotImplementedError def __init__(self): @@ -72,12 +72,12 @@ def updateProgress(self, **kwargs): self.progressCallback(**kwargs) -class BRAINSRegistration(IRegistrationAlgorithm): +class BRAINSRegistration(ImageRegistrationTool): NAME = "BRAINSFit" @staticmethod - def isAlgorithmAvailable(): + def isToolAvailable(): return hasattr(slicer.modules, "brainsfit") def __init__(self): @@ -200,12 +200,12 @@ def run(self): self.params = "BSpline Registration Parameters: %s" % str(params) + "\n\n" -class ElastixRegistration(IRegistrationAlgorithm): +class ElastixRegistration(ImageRegistrationTool): NAME = "Elastix" @staticmethod - def isAlgorithmAvailable(): + def isToolAvailable(): try: from Elastix import ElastixLogic except ImportError: @@ -273,5 +273,5 @@ def __runElastixRegistration(self): self.updateProgress(labelText='\nCompleted registration', value=4) -__algorithms__ = {'BRAINS':BRAINSRegistration, 'Elastix':ElastixRegistration} +__tools__ = {'BRAINS':BRAINSRegistration, 'Elastix':ElastixRegistration} diff --git a/SliceTracker/SliceTrackerUtils/configuration.py b/SliceTracker/SliceTrackerUtils/configuration.py index 6748166..fa68014 100644 --- a/SliceTracker/SliceTrackerUtils/configuration.py +++ b/SliceTracker/SliceTrackerUtils/configuration.py @@ -18,8 +18,8 @@ def loadConfiguration(self): if not self.getSetting("ZFrame_Registration_Class_Name"): self.setSetting("ZFrame_Registration_Class_Name", config.get('ZFrame Registration', 'class')) - if not self.getSetting("Prostate_Registration_Algorithm"): - self.setSetting("Prostate_Registration_Algorithm", config.get('Prostate Registration', 'Algorithm')) + if not self.getSetting("Prostate_Registration_Tool"): + self.setSetting("Prostate_Registration_Tool", config.get('Prostate Registration', 'Tool')) if not self.getSetting("Prostate_Registration_Fallback"): self.setSetting("Prostate_Registration_Fallback", config.get('Prostate Registration', 'Fallback')) diff --git a/SliceTracker/SliceTrackerUtils/session.py b/SliceTracker/SliceTrackerUtils/session.py index 76afe1f..6058207 100644 --- a/SliceTracker/SliceTrackerUtils/session.py +++ b/SliceTracker/SliceTrackerUtils/session.py @@ -724,15 +724,16 @@ def _runRegistration(self, parameterNode, segmentationData, progressCallback): self.invokeEvent(self.InitiateEvaluationEvent) def getRegistrationLogic(self): - preferred = self.getSetting("Prostate_Registration_Algorithm") + preferred = self.getSetting("Prostate_Registration_Tool") fallback = self.getSetting("Prostate_Registration_Fallback") - registrationClass = getattr(sys.modules[__name__], preferred) - if not registrationClass.isAlgorithmAvailable(): - registrationClass = getattr(sys.modules[__name__], fallback) - if not registrationClass.isAlgorithmAvailable(): + registrationToolClass = getattr(sys.modules[__name__], preferred) + if not registrationToolClass.isToolAvailable(): + registrationToolClass = getattr(sys.modules[__name__], fallback) + if not registrationToolClass.isToolAvailable(): raise RuntimeError("Neither preferred registration algorithm: {}, nor fallback registration algorithm {}" "are available. Make sure to install required dependencies".format(preferred, fallback)) - return SliceTrackerRegistrationLogic(registrationClass()) + logging.info("Preferred registration algorithm: {} not available. Falling back to {}. ".format(preferred, fallback)) + return SliceTrackerRegistrationLogic(registrationToolClass()) def addTargetsToMRMLScene(self, result): targetNodes = result.targets.asDict() diff --git a/puml/SliceTrackerRegistration.puml b/puml/SliceTrackerRegistration.puml index f7c009e..0a29688 100644 --- a/puml/SliceTrackerRegistration.puml +++ b/puml/SliceTrackerRegistration.puml @@ -1,29 +1,34 @@ @startuml +!include RegistrationResults.puml + class SliceTrackerRegistrationLogic { - + IRegistrationAlgorithm algorithm; + + ImageRegistrationTool imageRegistrationTool; -- + run() } -IRegistrationAlgorithm <-- SliceTrackerRegistrationLogic::algorithm: has_a - +ImageRegistrationTool <.. SliceTrackerRegistrationLogic::imageRegistrationTool: has_a SliceTrackerRegistrationLogic <-- SliceTrackerRegistrationWidget: uses SliceTrackerRegistrationLogic <-- CLI: uses -interface IRegistrationAlgorithm { +interface ImageRegistrationTool { + NAME: None - + {static} isAlgorithmAvailable() + + registrationResult: RegistrationResult + + {static} isToolAvailable() + {abstract} run(parameterNode, registrationResult, progressCallback=None) # _processParameterNode(parameterNode) + updateProgress(**kwargs) } +RegistrationResult <.. ImageRegistrationTool::registrationResult: has_a + + class BRAINRegistration { + NAME="BRAINSFit" - + isAlgorithmAvailable() + + isToolAvailable() + run(self, parameterNode, result, progressCallback=None) - __runRigidRegistration() - __runAffineRegistration() @@ -32,7 +37,7 @@ class BRAINRegistration { class ElastixRegistration { + NAME="Elastix" - + isAlgorithmAvailable() + + isToolAvailable() + run(self, parameterNode, result, progressCallback=None) - __runRigidBRAINSRegistration() - __runElastixRegistration() @@ -44,8 +49,8 @@ BRAINSRigidRegistration <-- BRAINRegistration::__runRigidRegistration: uses BRAINSAffineRegistration <-- BRAINRegistration::__runAffineRegistration: uses BRAINSBSplineRegistration <-- BRAINRegistration::__runBSplineRegistration: uses -IRegistrationAlgorithm <|-- BRAINRegistration: is_a -IRegistrationAlgorithm <|-- ElastixRegistration: is_a +ImageRegistrationTool <|-- BRAINRegistration: is_a +ImageRegistrationTool <|-- ElastixRegistration: is_a interface IBRAINSRegistrationType{ + fixedVolume From 23d05f068df32b138b192d605caf69a80019458e Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Wed, 24 Jan 2018 21:47:44 -0500 Subject: [PATCH 4/5] ENH: changed CLI parameter and added parameter files (issue #334) * only using Parameters_BSpline.txt for now --- .../Parameters_Affine.txt | 155 ++++++++++++++++ .../Parameters_BSpline.txt | 169 ++++++++++++++++++ .../Parameters_Rigid.txt | 155 ++++++++++++++++ SliceTracker/SliceTrackerRegistration.py | 8 +- .../algorithms/registration.py | 15 +- 5 files changed, 494 insertions(+), 8 deletions(-) create mode 100644 SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Affine.txt create mode 100644 SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt create mode 100644 SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt diff --git a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Affine.txt b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Affine.txt new file mode 100644 index 0000000..ae3e92b --- /dev/null +++ b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Affine.txt @@ -0,0 +1,155 @@ +// Example parameter file for affine registration +// C-style comments: // + +// The internal pixel type, used for internal computations +// Leave to float in general. +// NB: this is not the type of the input images! The pixel +// type of the input images is automatically read from the +// images themselves. +// This setting can be changed to "short" to save some memory +// in case of very large 3D images. +(FixedInternalImagePixelType "float") +(MovingInternalImagePixelType "float") + +// The dimensions of the fixed and moving image +// NB: This has to be specified by the user. The dimension of +// the images is currently NOT read from the images. +// Also note that some other settings may have to specified +// for each dimension separately. +(MovingImageDimension 3) + +// Specify whether you want to take into account the so-called +// direction cosines of the images. Recommended: true. +// In some cases, the direction cosines of the image are corrupt, +// due to image format conversions for example. In that case, you +// may want to set this option to "false". +(UseDirectionCosines "true") + +// **************** Main Components ************************** + +// The following components should usually be left as they are: +(Registration "MultiResolutionRegistration") +(Interpolator "BSplineInterpolator") +(ResampleInterpolator "FinalBSplineInterpolator") +(Resampler "DefaultResampler") + +// These may be changed to Fixed/MovingSmoothingImagePyramid. +// See the manual. +(FixedImagePyramid "FixedRecursiveImagePyramid") +(MovingImagePyramid "MovingRecursiveImagePyramid") + +// The following components are most important: +// The optimizer AdaptiveStochasticGradientDescent (ASGD) works +// quite ok in general. The Transform and Metric are important +// and need to be chosen careful for each application. See manual. +(Optimizer "AdaptiveStochasticGradientDescent") +(Transform "AffineTransform") +(Metric "AdvancedMattesMutualInformation") + +// ***************** Transformation ************************** + +// Scales the affine matrix elements compared to the translations, to make +// sure they are in the same range. In general, it's best to +// use automatic scales estimation: +(AutomaticScalesEstimation "true") + +// Automatically guess an initial translation by aligning the +// geometric centers of the fixed and moving. +(AutomaticTransformInitialization "true") + +// Whether transforms are combined by composition or by addition. +// In generally, Compose is the best option in most cases. +// It does not influence the results very much. +(HowToCombineTransforms "Compose") + +// ******************* Similarity measure ********************* + +// Number of grey level bins in each resolution level, +// for the mutual information. 16 or 32 usually works fine. +// You could also employ a hierarchical strategy: +//(NumberOfHistogramBins 16 32 64) +(NumberOfHistogramBins 32) + +// If you use a mask, this option is important. +// If the mask serves as region of interest, set it to false. +// If the mask indicates which pixels are valid, then set it to true. +// If you do not use a mask, the option doesn't matter. +(ErodeMask "false") + +// ******************** Multiresolution ********************** + +// The number of resolutions. 1 Is only enough if the expected +// deformations are small. 3 or 4 mostly works fine. For large +// images and large deformations, 5 or 6 may even be useful. +(NumberOfResolutions 4) + +// The downsampling/blurring factors for the image pyramids. +// By default, the images are downsampled by a factor of 2 +// compared to the next resolution. +// So, in 2D, with 4 resolutions, the following schedule is used: +//(ImagePyramidSchedule 8 8 4 4 2 2 1 1 ) +// And in 3D: +//(ImagePyramidSchedule 8 8 8 4 4 4 2 2 2 1 1 1 ) +// You can specify any schedule, for example: +//(ImagePyramidSchedule 4 4 4 3 2 1 1 1 ) +// Make sure that the number of elements equals the number +// of resolutions times the image dimension. + +// ******************* Optimizer **************************** + +// Maximum number of iterations in each resolution level: +// 200-500 works usually fine for affine registration. +// For more robustness, you may increase this to 1000-2000. +(MaximumNumberOfIterations 250) + +// The step size of the optimizer, in mm. By default the voxel size is used. +// which usually works well. In case of unusual high-resolution images +// (eg histology) it is necessary to increase this value a bit, to the size +// of the "smallest visible structure" in the image: +//(MaximumStepLength 1.0) + +// **************** Image sampling ********************** + +// Number of spatial samples used to compute the mutual +// information (and its derivative) in each iteration. +// With an AdaptiveStochasticGradientDescent optimizer, +// in combination with the two options below, around 2000 +// samples may already suffice. +(NumberOfSpatialSamples 2048) + +// Refresh these spatial samples in every iteration, and select +// them randomly. See the manual for information on other sampling +// strategies. +(NewSamplesEveryIteration "true") +(ImageSampler "Random") + +// ************* Interpolation and Resampling **************** + +// Order of B-Spline interpolation used during registration/optimisation. +// It may improve accuracy if you set this to 3. Never use 0. +// An order of 1 gives linear interpolation. This is in most +// applications a good choice. +(BSplineInterpolationOrder 1) + +// Order of B-Spline interpolation used for applying the final +// deformation. +// 3 gives good accuracy; recommended in most cases. +// 1 gives worse accuracy (linear interpolation) +// 0 gives worst accuracy, but is appropriate for binary images +// (masks, segmentations); equivalent to nearest neighbor interpolation. +(FinalBSplineInterpolationOrder 3) + +//Default pixel value for pixels that come from outside the picture: +(DefaultPixelValue 0) + +// Choose whether to generate the deformed moving image. +// You can save some time by setting this to false, if you are +// only interested in the final (nonrigidly) deformed moving image +// for example. +(WriteResultImage "true") + +// The pixel type and format of the resulting deformed moving image +(ResultImagePixelType "short") +(ResultImageFormat "mhd") + + diff --git a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt new file mode 100644 index 0000000..2b97d43 --- /dev/null +++ b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt @@ -0,0 +1,169 @@ +// Example parameter file for B-spline registration +// C-style comments: // + +// The internal pixel type, used for internal computations +// Leave to float in general. +// NB: this is not the type of the input images! The pixel +// type of the input images is automatically read from the +// images themselves. +// This setting can be changed to "short" to save some memory +// in case of very large 3D images. +(FixedInternalImagePixelType "float") +(MovingInternalImagePixelType "float") + +// The dimensions of the fixed and moving image +// NB: This has to be specified by the user. The dimension of +// the images is currently NOT read from the images. +// Also note that some other settings may have to specified +// for each dimension separately. +(MovingImageDimension 3) + +// Specify whether you want to take into account the so-called +// direction cosines of the images. Recommended: true. +// In some cases, the direction cosines of the image are corrupt, +// due to image format conversions for example. In that case, you +// may want to set this option to "false". +(UseDirectionCosines "true") + +// **************** Main Components ************************** + +// The following components should usually be left as they are: +(Registration "MultiResolutionRegistration") +(Interpolator "BSplineInterpolator") +(ResampleInterpolator "FinalBSplineInterpolator") +(Resampler "DefaultResampler") + +// These may be changed to Fixed/MovingSmoothingImagePyramid. +// See the manual. +(FixedImagePyramid "FixedRecursiveImagePyramid") +(MovingImagePyramid "MovingRecursiveImagePyramid") + +// The following components are most important: +// The optimizer AdaptiveStochasticGradientDescent (ASGD) works +// quite ok in general. The Transform and Metric are important +// and need to be chosen careful for each application. See manual. +(Optimizer "AdaptiveStochasticGradientDescent") +(Transform "BSplineTransform") +(Metric "AdvancedMattesMutualInformation") + +// ***************** Transformation ************************** + +// The control point spacing of the bspline transformation in +// the finest resolution level. Can be specified for each +// dimension differently. Unit: mm. +// The lower this value, the more flexible the deformation. +// Low values may improve the accuracy, but may also cause +// unrealistic deformations. This is a very important setting! +// We recommend tuning it for every specific application. It is +// difficult to come up with a good 'default' value. +(FinalGridSpacingInPhysicalUnits 16) + +// Alternatively, the grid spacing can be specified in voxel units. +// To do that, uncomment the following line and comment/remove +// the FinalGridSpacingInPhysicalUnits definition. +//(FinalGridSpacingInVoxels 16) + +// By default the grid spacing is halved after every resolution, +// such that the final grid spacing is obtained in the last +// resolution level. You can also specify your own schedule, +// if you uncomment the following line: +//(GridSpacingSchedule 4.0 4.0 2.0 1.0) +// This setting can also be supplied per dimension. + +// Whether transforms are combined by composition or by addition. +// In generally, Compose is the best option in most cases. +// It does not influence the results very much. +(HowToCombineTransforms "Compose") + +// ******************* Similarity measure ********************* + +// Number of grey level bins in each resolution level, +// for the mutual information. 16 or 32 usually works fine. +// You could also employ a hierarchical strategy: +//(NumberOfHistogramBins 16 32 64) +(NumberOfHistogramBins 32) + +// If you use a mask, this option is important. +// If the mask serves as region of interest, set it to false. +// If the mask indicates which pixels are valid, then set it to true. +// If you do not use a mask, the option doesn't matter. +(ErodeMask "false") + +// ******************** Multiresolution ********************** + +// The number of resolutions. 1 Is only enough if the expected +// deformations are small. 3 or 4 mostly works fine. For large +// images and large deformations, 5 or 6 may even be useful. +(NumberOfResolutions 4) + +// The downsampling/blurring factors for the image pyramids. +// By default, the images are downsampled by a factor of 2 +// compared to the next resolution. +// So, in 2D, with 4 resolutions, the following schedule is used: +//(ImagePyramidSchedule 8 8 4 4 2 2 1 1 ) +// And in 3D: +//(ImagePyramidSchedule 8 8 8 4 4 4 2 2 2 1 1 1 ) +// You can specify any schedule, for example: +//(ImagePyramidSchedule 4 4 4 3 2 1 1 1 ) +// Make sure that the number of elements equals the number +// of resolutions times the image dimension. + +// ******************* Optimizer **************************** + +// Maximum number of iterations in each resolution level: +// 200-2000 works usually fine for nonrigid registration. +// The more, the better, but the longer computation time. +// This is an important parameter! +(MaximumNumberOfIterations 500) + +// The step size of the optimizer, in mm. By default the voxel size is used. +// which usually works well. In case of unusual high-resolution images +// (eg histology) it is necessary to increase this value a bit, to the size +// of the "smallest visible structure" in the image: +//(MaximumStepLength 1.0) + +// **************** Image sampling ********************** + +// Number of spatial samples used to compute the mutual +// information (and its derivative) in each iteration. +// With an AdaptiveStochasticGradientDescent optimizer, +// in combination with the two options below, around 2000 +// samples may already suffice. +(NumberOfSpatialSamples 2048) + +// Refresh these spatial samples in every iteration, and select +// them randomly. See the manual for information on other sampling +// strategies. +(NewSamplesEveryIteration "true") +(ImageSampler "Random") + +// ************* Interpolation and Resampling **************** + +// Order of B-Spline interpolation used during registration/optimisation. +// It may improve accuracy if you set this to 3. Never use 0. +// An order of 1 gives linear interpolation. This is in most +// applications a good choice. +(BSplineInterpolationOrder 1) + +// Order of B-Spline interpolation used for applying the final +// deformation. +// 3 gives good accuracy; recommended in most cases. +// 1 gives worse accuracy (linear interpolation) +// 0 gives worst accuracy, but is appropriate for binary images +// (masks, segmentations); equivalent to nearest neighbor interpolation. +(FinalBSplineInterpolationOrder 3) + +//Default pixel value for pixels that come from outside the picture: +(DefaultPixelValue 0) + +// Choose whether to generate the deformed moving image. +// You can save some time by setting this to false, if you are +// not interested in the final deformed moving image, but only +// want to analyze the deformation field for example. +(WriteResultImage "true") + +// The pixel type and format of the resulting deformed moving image +(ResultImagePixelType "short") +(ResultImageFormat "mhd") + + diff --git a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt new file mode 100644 index 0000000..43fd118 --- /dev/null +++ b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt @@ -0,0 +1,155 @@ +// Example parameter file for rotation registration +// C-style comments: // + +// The internal pixel type, used for internal computations +// Leave to float in general. +// NB: this is not the type of the input images! The pixel +// type of the input images is automatically read from the +// images themselves. +// This setting can be changed to "short" to save some memory +// in case of very large 3D images. +(FixedInternalImagePixelType "float") +(MovingInternalImagePixelType "float") + +// The dimensions of the fixed and moving image +// NB: This has to be specified by the user. The dimension of +// the images is currently NOT read from the images. +// Also note that some other settings may have to specified +// for each dimension separately. +(MovingImageDimension 3) + +// Specify whether you want to take into account the so-called +// direction cosines of the images. Recommended: true. +// In some cases, the direction cosines of the image are corrupt, +// due to image format conversions for example. In that case, you +// may want to set this option to "false". +(UseDirectionCosines "true") + +// **************** Main Components ************************** + +// The following components should usually be left as they are: +(Registration "MultiResolutionRegistration") +(Interpolator "BSplineInterpolator") +(ResampleInterpolator "FinalBSplineInterpolator") +(Resampler "DefaultResampler") + +// These may be changed to Fixed/MovingSmoothingImagePyramid. +// See the manual. +(FixedImagePyramid "FixedRecursiveImagePyramid") +(MovingImagePyramid "MovingRecursiveImagePyramid") + +// The following components are most important: +// The optimizer AdaptiveStochasticGradientDescent (ASGD) works +// quite ok in general. The Transform and Metric are important +// and need to be chosen careful for each application. See manual. +(Optimizer "AdaptiveStochasticGradientDescent") +(Transform "EulerTransform") +(Metric "AdvancedMattesMutualInformation") + +// ***************** Transformation ************************** + +// Scales the rotations compared to the translations, to make +// sure they are in the same range. In general, it's best to +// use automatic scales estimation: +(AutomaticScalesEstimation "true") + +// Automatically guess an initial translation by aligning the +// geometric centers of the fixed and moving. +(AutomaticTransformInitialization "true") + +// Whether transforms are combined by composition or by addition. +// In generally, Compose is the best option in most cases. +// It does not influence the results very much. +(HowToCombineTransforms "Compose") + +// ******************* Similarity measure ********************* + +// Number of grey level bins in each resolution level, +// for the mutual information. 16 or 32 usually works fine. +// You could also employ a hierarchical strategy: +//(NumberOfHistogramBins 16 32 64) +(NumberOfHistogramBins 32) + +// If you use a mask, this option is important. +// If the mask serves as region of interest, set it to false. +// If the mask indicates which pixels are valid, then set it to true. +// If you do not use a mask, the option doesn't matter. +(ErodeMask "false") + +// ******************** Multiresolution ********************** + +// The number of resolutions. 1 Is only enough if the expected +// deformations are small. 3 or 4 mostly works fine. For large +// images and large deformations, 5 or 6 may even be useful. +(NumberOfResolutions 4) + +// The downsampling/blurring factors for the image pyramids. +// By default, the images are downsampled by a factor of 2 +// compared to the next resolution. +// So, in 2D, with 4 resolutions, the following schedule is used: +//(ImagePyramidSchedule 8 8 4 4 2 2 1 1 ) +// And in 3D: +//(ImagePyramidSchedule 8 8 8 4 4 4 2 2 2 1 1 1 ) +// You can specify any schedule, for example: +//(ImagePyramidSchedule 4 4 4 3 2 1 1 1 ) +// Make sure that the number of elements equals the number +// of resolutions times the image dimension. + +// ******************* Optimizer **************************** + +// Maximum number of iterations in each resolution level: +// 200-500 works usually fine for rigid registration. +// For more robustness, you may increase this to 1000-2000. +(MaximumNumberOfIterations 250) + +// The step size of the optimizer, in mm. By default the voxel size is used. +// which usually works well. In case of unusual high-resolution images +// (eg histology) it is necessary to increase this value a bit, to the size +// of the "smallest visible structure" in the image: +//(MaximumStepLength 1.0) + +// **************** Image sampling ********************** + +// Number of spatial samples used to compute the mutual +// information (and its derivative) in each iteration. +// With an AdaptiveStochasticGradientDescent optimizer, +// in combination with the two options below, around 2000 +// samples may already suffice. +(NumberOfSpatialSamples 2048) + +// Refresh these spatial samples in every iteration, and select +// them randomly. See the manual for information on other sampling +// strategies. +(NewSamplesEveryIteration "true") +(ImageSampler "Random") + +// ************* Interpolation and Resampling **************** + +// Order of B-Spline interpolation used during registration/optimisation. +// It may improve accuracy if you set this to 3. Never use 0. +// An order of 1 gives linear interpolation. This is in most +// applications a good choice. +(BSplineInterpolationOrder 1) + +// Order of B-Spline interpolation used for applying the final +// deformation. +// 3 gives good accuracy; recommended in most cases. +// 1 gives worse accuracy (linear interpolation) +// 0 gives worst accuracy, but is appropriate for binary images +// (masks, segmentations); equivalent to nearest neighbor interpolation. +(FinalBSplineInterpolationOrder 3) + +//Default pixel value for pixels that come from outside the picture: +(DefaultPixelValue 0) + +// Choose whether to generate the deformed moving image. +// You can save some time by setting this to false, if you are +// only interested in the final (nonrigidly) deformed moving image +// for example. +(WriteResultImage "true") + +// The pixel type and format of the resulting deformed moving image +(ResultImagePixelType "short") +(ResultImageFormat "mhd") + + diff --git a/SliceTracker/SliceTrackerRegistration.py b/SliceTracker/SliceTrackerRegistration.py index ec4e676..be568f1 100644 --- a/SliceTracker/SliceTrackerRegistration.py +++ b/SliceTracker/SliceTrackerRegistration.py @@ -182,9 +182,9 @@ def main(argv): required=False, help="Initial rigid transform for re-registration") parser.add_argument("-o", "--output-directory", dest="output_directory", metavar="PATH", default="-", required=False, help="Output directory for registration result") - parser.add_argument("-al", "--algorithm", dest="algorithm", metavar="PATH", default="BRAINS", + parser.add_argument("-t", "--tool", dest="tool", metavar="PATH", default="BRAINS", choices=registration.__tools__.keys(), required=False, - help="Algorithm to be used for registration (default: %(default)s)") + help="Image Registration Tool to be used for registration (default: %(default)s)") args = parser.parse_args(argv) @@ -197,10 +197,10 @@ def main(argv): success, fixedVolume = slicer.util.loadVolume(args.fixed_volume, returnNode=True) success, movingVolume = slicer.util.loadVolume(args.moving_volume, returnNode=True) - imageRegistrationTool = registration.__tools__[args.algorithm] + imageRegistrationTool = registration.__tools__[args.tool] if not imageRegistrationTool.isToolAvailable(): - raise RuntimeError("Registration algorithm {} cannot be executed due to missing dependencies.".format(args.algorithm)) + raise RuntimeError("Image Registration Tool {} cannot be executed due to missing dependencies.".format(args.tool)) logic = SliceTrackerRegistrationLogic(imageRegistrationTool()) parameterNode = logic.initializeParameterNode(fixedVolume, fixedLabel, movingVolume, movingLabel) diff --git a/SliceTracker/SliceTrackerUtils/algorithms/registration.py b/SliceTracker/SliceTrackerUtils/algorithms/registration.py index a6263a7..e3f830a 100644 --- a/SliceTracker/SliceTrackerUtils/algorithms/registration.py +++ b/SliceTracker/SliceTrackerUtils/algorithms/registration.py @@ -212,6 +212,12 @@ def isToolAvailable(): return False return True + def __init__(self): + super(ElastixRegistration, self).__init__() + import os, sys + self.parametersDirectory = os.path.join(os.path.dirname(sys.modules[self.__module__].__file__), '..', '..', + 'Resources', 'ElastixRegistrationParameters') + def hardenTransform(self, nodes, transform): tfmLogic = slicer.modules.transforms.logic() for node in nodes: @@ -236,7 +242,7 @@ def run(self, parameterNode, result, progressCallback=None): transform=self.registrationResult.transforms.rigid) self.registrationResult.transforms.rigid.Inverse() - self.hardenTransform(nodes=[self.registrationResult.volumes.moving, self.registrationResult.labels.moving], + self.hardenTransform(nodes=[self.registrationResult.labels.moving], transform=self.registrationResult.transforms.rigid) self.registrationResult.transforms.rigid.Inverse() @@ -262,10 +268,11 @@ def __runRigidBRAINSRegistration(self): def __runElastixRegistration(self): self.updateProgress(labelText='\nElastix registration', value=3) - from Elastix import ElastixLogic, RegistrationPresets_ParameterFilenames + from Elastix import ElastixLogic + import os logic = ElastixLogic() - parameterFileNames = logic.getRegistrationPresets()[0][RegistrationPresets_ParameterFilenames] - print RegistrationPresets_ParameterFilenames + pFileNames = ["Parameters_BSpline.txt"] # "Parameters_Rigid.txt", "Parameters_Affine.txt" + parameterFileNames = [os.path.join(self.parametersDirectory, pFile) for pFile in pFileNames] logic.registerVolumes(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, parameterFileNames, self.registrationResult.volumes.bSpline, self.registrationResult.transforms.bSpline, From ebc4efd93b81d859d267d69b4fcd6637b7c8a9fc Mon Sep 17 00:00:00 2001 From: Christian Herz Date: Tue, 30 Jan 2018 15:54:20 -0500 Subject: [PATCH 5/5] ENH: changed parameter files and addede BRAINSROIInitializer (issue #334) --- .../Parameters_BSpline.txt | 14 -- .../Parameters_Rigid.txt | 18 +-- .../algorithms/registration.py | 128 +++++++++--------- SliceTracker/SliceTrackerUtils/session.py | 3 +- 4 files changed, 67 insertions(+), 96 deletions(-) diff --git a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt index 2b97d43..619905c 100644 --- a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt +++ b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_BSpline.txt @@ -11,20 +11,6 @@ (FixedInternalImagePixelType "float") (MovingInternalImagePixelType "float") -// The dimensions of the fixed and moving image -// NB: This has to be specified by the user. The dimension of -// the images is currently NOT read from the images. -// Also note that some other settings may have to specified -// for each dimension separately. -(MovingImageDimension 3) - -// Specify whether you want to take into account the so-called -// direction cosines of the images. Recommended: true. -// In some cases, the direction cosines of the image are corrupt, -// due to image format conversions for example. In that case, you -// may want to set this option to "false". -(UseDirectionCosines "true") - // **************** Main Components ************************** // The following components should usually be left as they are: diff --git a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt index 43fd118..e5ba7ec 100644 --- a/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt +++ b/SliceTracker/Resources/ElastixRegistrationParameters/Parameters_Rigid.txt @@ -11,20 +11,6 @@ (FixedInternalImagePixelType "float") (MovingInternalImagePixelType "float") -// The dimensions of the fixed and moving image -// NB: This has to be specified by the user. The dimension of -// the images is currently NOT read from the images. -// Also note that some other settings may have to specified -// for each dimension separately. -(MovingImageDimension 3) - -// Specify whether you want to take into account the so-called -// direction cosines of the images. Recommended: true. -// In some cases, the direction cosines of the image are corrupt, -// due to image format conversions for example. In that case, you -// may want to set this option to "false". -(UseDirectionCosines "true") - // **************** Main Components ************************** // The following components should usually be left as they are: @@ -55,7 +41,7 @@ // Automatically guess an initial translation by aligning the // geometric centers of the fixed and moving. -(AutomaticTransformInitialization "true") +(AutomaticTransformInitialization "false") // Whether transforms are combined by composition or by addition. // In generally, Compose is the best option in most cases. @@ -81,7 +67,7 @@ // The number of resolutions. 1 Is only enough if the expected // deformations are small. 3 or 4 mostly works fine. For large // images and large deformations, 5 or 6 may even be useful. -(NumberOfResolutions 4) +(NumberOfResolutions 2) // The downsampling/blurring factors for the image pyramids. // By default, the images are downsampled by a factor of 2 diff --git a/SliceTracker/SliceTrackerUtils/algorithms/registration.py b/SliceTracker/SliceTrackerUtils/algorithms/registration.py index e3f830a..8276844 100644 --- a/SliceTracker/SliceTrackerUtils/algorithms/registration.py +++ b/SliceTracker/SliceTrackerUtils/algorithms/registration.py @@ -109,7 +109,7 @@ def __runRigidRegistration(self): self.registrationResult.labels.fixed, self.registrationResult.labels.moving, self.registrationResult.transforms.rigid, self.registrationResult.volumes.rigid) reg.run() - self.registrationResult.cmdArguments += reg.params + self.registrationResult.cmdArguments += reg.paramsInfo def __runAffineRegistration(self): self.updateProgress(labelText='\nAffine registration', value=2) @@ -118,7 +118,7 @@ def __runAffineRegistration(self): self.registrationResult.transforms.affine, self.registrationResult.volumes.affine, self.registrationResult.transforms.rigid) reg.run() - self.registrationResult.cmdArguments += reg.params + self.registrationResult.cmdArguments += reg.paramsInfo def __runBSplineRegistration(self): self.updateProgress(labelText='\nBSpline registration', value=3) @@ -128,7 +128,7 @@ def __runBSplineRegistration(self): self.registrationResult.transforms.bSpline, self.registrationResult.volumes.bSpline, self.registrationResult.transforms.affine) reg.run() - self.registrationResult.cmdArguments += reg.params + self.registrationResult.cmdArguments += reg.paramsInfo self.updateProgress(labelText='\nCompleted registration', value=4) @@ -137,67 +137,64 @@ class IBRAINSRegistrationType(object): __metaclass__ = ABCMeta - def __init__(self, fixedVolume, movingVolume, fixedLabel, movingLabel, outputTransform, outputVolume, + def __init__(self, fixedVolume, movingVolume, fixedLabel, movingLabel, outputTransform, outputVolume=None, initialTransform=None): - self.fixedVolume = fixedVolume - self.movingVolume = movingVolume - self.fixedLabel = fixedLabel - self.movingLabel = movingLabel - self.outputTransform = outputTransform - self.outputVolume = outputVolume - self.initialTransform = initialTransform - self.params = None + self.params = dict(fixedVolume=fixedVolume, + movingVolume=movingVolume, + fixedBinaryVolume=fixedLabel, + movingBinaryVolume=movingLabel, + outputTransform=outputTransform.GetID()) + if outputVolume: + self.params['outputVolume'] = outputVolume.GetID() + if initialTransform: + self.params['initialTransform'] = initialTransform @abstractmethod def run(self): pass - def getGeneralParams(self): - return {'fixedVolume': self.fixedVolume, - 'movingVolume': self.movingVolume, - 'fixedBinaryVolume': self.fixedLabel, - 'movingBinaryVolume': self.movingLabel, - 'outputTransform': self.outputTransform.GetID(), - 'outputVolume': self.outputVolume.GetID()} - class BRAINSRigidRegistration(IBRAINSRegistrationType): def run(self): - params = {'maskProcessingMode': "ROI", - 'initializeTransformMode': "useCenterOfROIAlign", - 'useRigid': True} - params.update(self.getGeneralParams()) - slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) - self.params = "Rigid Registration Parameters: %s" % str(params) + "\n\n" + self.params.update({'maskProcessingMode': "ROI", + 'initializeTransformMode': "useCenterOfROIAlign", + 'useRigid': True}) + slicer.cli.run(slicer.modules.brainsfit, None, self.params, wait_for_completion=True) + self.paramsInfo = "Rigid Registration Parameters: %s" % str(self.params) + "\n\n" + + +class BRAINSROIInitializer(IBRAINSRegistrationType): + + def run(self): + self.params.update({'maskProcessingMode': "ROI", + 'initializeTransformMode': "useCenterOfROIAlign"}) + slicer.cli.run(slicer.modules.brainsfit, None, self.params, wait_for_completion=True) + self.paramsInfo = "ROI Initializer Parameters: %s" % str(self.params) + "\n\n" class BRAINSAffineRegistration(IBRAINSRegistrationType): def run(self): - params = {'maskProcessingMode': "ROI", - 'useAffine': True, - 'initialTransform': self.initialTransform} - params.update(self.getGeneralParams()) - slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) - self.params = "Affine Registration Parameters: %s" % str(params) + "\n\n" + self.params.update({'maskProcessingMode': "ROI", + 'useAffine': True}) + slicer.cli.run(slicer.modules.brainsfit, None, self.params, wait_for_completion=True) + self.paramsInfo = "Affine Registration Parameters: %s" % str(self.params) + "\n\n" class BRAINSBSplineRegistration(IBRAINSRegistrationType): def run(self): - params = {'useROIBSpline': True, - 'useBSpline': True, - 'splineGridSize': "3,3,3", - 'maskProcessing': "ROI", - 'minimumStepLength': "0.005", - 'maximumStepLength': "0.2", - 'costFunctionConvergenceFactor': "1.00E+09", - 'maskProcessingMode': "ROI", - 'initialTransform': self.initialTransform} - params.update(self.getGeneralParams()) - slicer.cli.run(slicer.modules.brainsfit, None, params, wait_for_completion=True) - self.params = "BSpline Registration Parameters: %s" % str(params) + "\n\n" + self.params.update({'useROIBSpline': True, + 'useBSpline': True, + 'splineGridSize': "3,3,3", + 'maskProcessing': "ROI", + 'minimumStepLength': "0.005", + 'maximumStepLength': "0.2", + 'costFunctionConvergenceFactor': "1.00E+09", + 'maskProcessingMode': "ROI"}) + slicer.cli.run(slicer.modules.brainsfit, None, self.params, wait_for_completion=True) + self.paramsInfo = "BSpline Registration Parameters: %s" % str(self.params) + "\n\n" class ElastixRegistration(ImageRegistrationTool): @@ -229,49 +226,52 @@ def run(self, parameterNode, result, progressCallback=None): self.registrationResult = result self._processParameterNode(parameterNode) - registrationTypes = ['rigid', 'bSpline'] + registrationTypes = ['bSpline'] self.createVolumeAndTransformNodes(registrationTypes, prefix=str(result.seriesNumber), suffix=result.suffix, deformableRegistrationNodeClass=slicer.vtkMRMLTransformNode) - self.__runRigidBRAINSRegistration() + initTransform = self.__runBRAINSROIInitializer() self.hardenTransform(nodes=[self.registrationResult.volumes.moving, self.registrationResult.labels.moving], - transform=self.registrationResult.transforms.rigid) + transform=initTransform) self.__runElastixRegistration() - self.hardenTransform(nodes=[self.registrationResult.transforms.bSpline], - transform=self.registrationResult.transforms.rigid) + initTransform.Inverse() + self.hardenTransform(nodes=[self.registrationResult.labels.moving], transform=initTransform) + initTransform.Inverse() + self.hardenTransform(nodes=[initTransform], transform=self.registrationResult.transforms.bSpline) - self.registrationResult.transforms.rigid.Inverse() - self.hardenTransform(nodes=[self.registrationResult.labels.moving], - transform=self.registrationResult.transforms.rigid) - self.registrationResult.transforms.rigid.Inverse() + initTransform.SetName(self.registrationResult.transforms.bSpline.GetName()) + slicer.mrmlScene.RemoveNode(self.registrationResult.transforms.bSpline) + self.registrationResult.transforms.bSpline = initTransform targetsNodeID = parameterNode.GetAttribute('TargetsNodeID') if targetsNodeID: result.targets.original = slicer.mrmlScene.GetNodeByID(targetsNodeID) self.transformTargets(registrationTypes, result.targets.original, str(result.seriesNumber), suffix=result.suffix) + slicer.mrmlScene.RemoveNode(result.volumes.moving) result.volumes.moving = slicer.mrmlScene.GetNodeByID(parameterNode.GetAttribute('MovingImageNodeID')) - def __runRigidBRAINSRegistration(self): - self.updateProgress(labelText='\nRigid registration', value=2) + def __runBRAINSROIInitializer(self): + self.updateProgress(labelText='\nROI initialization', value=2) - rigidRegistration = BRAINSRigidRegistration(self.registrationResult.volumes.fixed, - self.registrationResult.volumes.moving, - self.registrationResult.labels.fixed, - self.registrationResult.labels.moving, - self.registrationResult.transforms.rigid, - self.registrationResult.volumes.rigid) + transform = self.createLinearTransformNode(self.registrationResult.volumes.moving.GetName()+"_temp_transform") + slicer.mrmlScene.AddNode(transform) + rigidRegistration = BRAINSROIInitializer(self.registrationResult.volumes.fixed, + self.registrationResult.volumes.moving, + self.registrationResult.labels.fixed, + self.registrationResult.labels.moving, + outputTransform=transform) rigidRegistration.run() - - self.registrationResult.cmdArguments += rigidRegistration.params + self.registrationResult.cmdArguments += rigidRegistration.paramsInfo + return transform def __runElastixRegistration(self): self.updateProgress(labelText='\nElastix registration', value=3) from Elastix import ElastixLogic import os logic = ElastixLogic() - pFileNames = ["Parameters_BSpline.txt"] # "Parameters_Rigid.txt", "Parameters_Affine.txt" + pFileNames = ["Parameters_Rigid.txt", "Parameters_BSpline.txt"] parameterFileNames = [os.path.join(self.parametersDirectory, pFile) for pFile in pFileNames] logic.registerVolumes(self.registrationResult.volumes.fixed, self.registrationResult.volumes.moving, parameterFileNames, self.registrationResult.volumes.bSpline, diff --git a/SliceTracker/SliceTrackerUtils/session.py b/SliceTracker/SliceTrackerUtils/session.py index 6058207..c56b79e 100644 --- a/SliceTracker/SliceTrackerUtils/session.py +++ b/SliceTracker/SliceTrackerUtils/session.py @@ -690,9 +690,8 @@ def applyInitialRegistration(self, retryMode, segmentationData, progressCallback def applyRegistration(self, progressCallback=None): coverProstateRegResult = self.data.getMostRecentApprovedCoverProstateRegistration() - lastRigidTfm = self.data.getLastApprovedRigidTransformation() lastApprovedTfm = self.data.getMostRecentApprovedTransform() - initialTransform = lastApprovedTfm if lastApprovedTfm else lastRigidTfm + initialTransform = lastApprovedTfm if lastApprovedTfm else self.data.getLastApprovedRigidTransformation() fixedLabel = self.volumesLogic.CreateAndAddLabelVolume(slicer.mrmlScene, self.currentSeriesVolume, self.currentSeriesVolume.GetName() + '-label')