Skip to content

Commit

Permalink
ENH: Allow editing and saving of registration presets
Browse files Browse the repository at this point in the history
Built-in elastix registration presets can be copied into the scene, edited, and saved in the scene. When registration is launched then the in-scene configuration data is written to files and passed to Elastix.

In-scene presets can also be saved as user presets, into the user profile folder (each preset in a separate subfolder, containing all necessary information, so that presets can be easily shared with other just by copying the complete folder content).

see #50
  • Loading branch information
che85 authored and lassoan committed Jan 17, 2025
1 parent b702416 commit 9ec57ae
Show file tree
Hide file tree
Showing 12 changed files with 1,674 additions and 488 deletions.
349 changes: 349 additions & 0 deletions Doc/SlicerElastix.drawio

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Elastix/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ set(MODULE_NAME Elastix)
set(MODULE_PYTHON_SCRIPTS
${MODULE_NAME}.py
ElastixLib/__init__
ElastixLib/constants.py
ElastixLib/utils.py
ElastixLib/database.py
ElastixLib/preset.py
ElastixLib/manager.py
ElastixLib/ElastixPresetSubjectHierarchyPlugin.py
)

set(MODULE_PYTHON_RESOURCES
Expand Down
272 changes: 121 additions & 151 deletions Elastix/Elastix.py

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions Elastix/ElastixLib/ElastixPresetSubjectHierarchyPlugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import qt, ctk, slicer
from AbstractScriptedSubjectHierarchyPlugin import *


class ElastixPresetSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin):

# Necessary static member to be able to set python source to scripted subject hierarchy plugin
filePath = __file__

def __init__(self, scriptedPlugin):
scriptedPlugin.name = 'Elastix'
AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)

def canAddNodeToSubjectHierarchy(self, node, parentItemID = None):
if node is not None and node.IsA("vtkMRMLScriptedModuleNode"):
if node.GetAttribute("Type") == "ElastixPreset":
return 0.9
return 0.0

def canOwnSubjectHierarchyItem(self, itemID):
pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
shNode = pluginHandlerSingleton.subjectHierarchyNode()
associatedNode = shNode.GetItemDataNode(itemID)
return self.canAddNodeToSubjectHierarchy(associatedNode)

def roleForPlugin(self):
return "ElastixPreset"

def icon(self, itemID):
import os
iconPath = None
pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
shNode = pluginHandlerSingleton.subjectHierarchyNode()
associatedNode = shNode.GetItemDataNode(itemID)
if associatedNode is not None and associatedNode.IsA("vtkMRMLScriptedModuleNode"):
if associatedNode.GetAttribute("Type") == "ElastixPreset":
iconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/Elastix.png')
if iconPath and os.path.exists(iconPath):
return qt.QIcon(iconPath)
# Item unknown by plugin
return qt.QIcon()

def tooltip(self, itemID):
return "Elastix preset"
8 changes: 0 additions & 8 deletions Elastix/ElastixLib/constants.py

This file was deleted.

139 changes: 139 additions & 0 deletions Elastix/ElastixLib/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import logging
import shutil

import qt
import vtk

import abc
import os

from typing import Callable

import slicer
from pathlib import Path
from ElastixLib.preset import Preset, UserPreset, createPreset


class ElastixDatabase(abc.ABC):

@property
def logCallback(self):
return self._logCallback

@logCallback.setter
def logCallback(self, cb: Callable = None):
self._logCallback = cb

def getRegistrationPresetsFromXML(self, elastixParameterSetDatabasePath, presetClass):
if not os.path.isfile(elastixParameterSetDatabasePath):
raise ValueError("Failed to open parameter set database: " + elastixParameterSetDatabasePath)
elastixParameterSetDatabaseXml = vtk.vtkXMLUtilities.ReadElementFromFile(elastixParameterSetDatabasePath)

# Create python list from XML for convenience
registrationPresets = []
if elastixParameterSetDatabaseXml is not None:
for parameterSetIndex in range(elastixParameterSetDatabaseXml.GetNumberOfNestedElements()):
parameterSetXml = elastixParameterSetDatabaseXml.GetNestedElement(parameterSetIndex)
parameterFilesXml = parameterSetXml.FindNestedElementWithName('ParameterFiles')
parameterFiles = []
for parameterFileIndex in range(parameterFilesXml.GetNumberOfNestedElements()):
parameterFiles.append(os.path.join(
str(Path(elastixParameterSetDatabasePath).parent),
parameterFilesXml.GetNestedElement(parameterFileIndex).GetAttribute('Name'))
)
parameterSetAttributes = \
[parameterSetXml.GetAttribute(attr) if parameterSetXml.GetAttribute(attr) is not None else "" for attr in ['id', 'modality', 'content', 'description', 'publications']]
try:
registrationPresets.append(
createPreset(*parameterSetAttributes, parameterFiles=parameterFiles, presetClass=presetClass)
)
except FileNotFoundError as exc:
msg = f"Cannot load preset. Loading failed with error: {exc}"
logging.error(msg)
if self.logCallback:
self.logCallback(msg)
continue
return registrationPresets

def __init__(self):
self._logCallback = None
self.registrationPresets = None

def getRegistrationPresets(self, force_refresh=False):
if self.registrationPresets and not force_refresh:
return self.registrationPresets

self.registrationPresets = self._getRegistrationPresets()

return self.registrationPresets

@abc.abstractmethod
def _getRegistrationPresets(self):
pass


class BuiltinElastixDatabase(ElastixDatabase):

# load txt files into slicer scene
DATABASE_FILE = os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'Resources', 'RegistrationParameters',
'ElastixParameterSetDatabase.xml'))

def getPresetsDir(self):
return str(Path(self.DATABASE_FILE).parent)

def _getRegistrationPresets(self):
return self.getRegistrationPresetsFromXML(self.DATABASE_FILE, presetClass=Preset)


class UserElastixDataBase(ElastixDatabase):

DATABASE_LOCATION = Path(slicer.app.slicerUserSettingsFilePath).parent / "Elastix"

@staticmethod
def getAllXMLFiles(directory):
import fnmatch
files = []
for root, dirnames, filenames in os.walk(directory):
for filename in fnmatch.filter(filenames, '*{}'.format(".xml")):
files.append(os.path.join(root, filename))
return files

def __init__(self):
self.DATABASE_LOCATION.mkdir(exist_ok=True)
self._presetLocations = {}
super().__init__()

def getPresetsDir(self):
return str(self.DATABASE_LOCATION)

def _getRegistrationPresets(self):
xml_files = self.getAllXMLFiles(self.DATABASE_LOCATION)
registrationPresets = []
for xml_file in xml_files:
presets = self.getRegistrationPresetsFromXML(xml_file, presetClass=UserPreset)
if len(presets) > 1:
raise RuntimeError("The User presets are intended to have one preset per .xml file only.")
for preset in presets:
self._presetLocations[preset] = str(Path(xml_file).parent)
registrationPresets.extend(presets)
return registrationPresets

def deletePreset(self, preset: UserPreset):
path = self._presetLocations.pop(preset)
shutil.rmtree(path)


class InSceneElastixDatabase(ElastixDatabase):

def _getRegistrationPresets(self):
registrationPresets = []

nodes = filter(lambda n: n.GetAttribute('Type') == 'ElastixPreset',
slicer.util.getNodesByClass('vtkMRMLTextNode'))

from ElastixLib.preset import getInScenePreset
for node in nodes:
registrationPresets.append(getInScenePreset(node))

return registrationPresets
Loading

0 comments on commit 9ec57ae

Please sign in to comment.