``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ """
+ import os
+ import shutil
+ import logging
- _cleanup()
- return False
+ maxNumberOfTrials = numberOfTrials
+
+ def _cleanup():
+ # If there was a failure, delete downloaded file and empty output folder
+ logging.warning('Download and extract failed, removing archive and destination folder and retrying. Attempt #%d...' % (maxNumberOfTrials - numberOfTrials))
+ os.remove(archiveFilePath)
+ shutil.rmtree(outputDir)
+ os.mkdir(outputDir)
+
+ while numberOfTrials:
+ if not downloadFile(url, archiveFilePath, checksum):
+ numberOfTrials -= 1
+ _cleanup()
+ continue
+ if not extractArchive(archiveFilePath, outputDir, expectedNumberOfExtractedFiles):
+ numberOfTrials -= 1
+ _cleanup()
+ continue
+ return True
+
+ _cleanup()
+ return False
def getFilesInDirectory(directory, absolutePath=True):
- """Collect all files in a directory and its subdirectories in a list."""
- import os
- allFiles = []
- for root, subdirs, files in os.walk(directory):
- for fileName in files:
- if absolutePath:
- fileAbsolutePath = os.path.abspath(os.path.join(root, fileName)).replace('\\', '/')
- allFiles.append(fileAbsolutePath)
- else:
- allFiles.append(fileName)
- return allFiles
+ """Collect all files in a directory and its subdirectories in a list."""
+ import os
+ allFiles = []
+ for root, subdirs, files in os.walk(directory):
+ for fileName in files:
+ if absolutePath:
+ fileAbsolutePath = os.path.abspath(os.path.join(root, fileName)).replace('\\', '/')
+ allFiles.append(fileAbsolutePath)
+ else:
+ allFiles.append(fileName)
+ return allFiles
class chdir:
- """Non thread-safe context manager to change the current working directory.
+ """Non thread-safe context manager to change the current working directory.
- .. note::
+ .. note::
- Available in Python 3.11 as ``contextlib.chdir`` and adapted from https://github.com/python/cpython/pull/28271
+ Available in Python 3.11 as ``contextlib.chdir`` and adapted from https://github.com/python/cpython/pull/28271
- Available in CTK as ``ctkScopedCurrentDir`` C++ class
- """
- def __init__(self, path):
- self.path = path
- self._old_cwd = []
+ Available in CTK as ``ctkScopedCurrentDir`` C++ class
+ """
+ def __init__(self, path):
+ self.path = path
+ self._old_cwd = []
- def __enter__(self):
- import os
- self._old_cwd.append(os.getcwd())
- os.chdir(self.path)
+ def __enter__(self):
+ import os
+ self._old_cwd.append(os.getcwd())
+ os.chdir(self.path)
- def __exit__(self, *excinfo):
- import os
- os.chdir(self._old_cwd.pop())
+ def __exit__(self, *excinfo):
+ import os
+ os.chdir(self._old_cwd.pop())
def plot(narray, xColumnIndex=-1, columnNames=None, title=None, show=True, nodes=None):
- """Create a plot from a numpy array that contains two or more columns.
-
- :param narray: input numpy array containing data series in columns.
- :param xColumnIndex: index of column that will be used as x axis.
- If it is set to negative number (by default) then row index will be used as x coordinate.
- :param columnNames: names of each column of the input array. If title is specified for the plot
- then title+columnName will be used as series name.
- :param title: title of the chart. Plot node names are set based on this value.
- :param nodes: plot chart, table, and list of plot series nodes.
- Specified in a dictionary, with keys: 'chart', 'table', 'series'.
- Series contains a list of plot series nodes (one for each table column).
- The parameter is used both as an input and output.
- :return: plot chart node. Plot chart node provides access to chart properties and plot series nodes.
+ """Create a plot from a numpy array that contains two or more columns.
+
+ :param narray: input numpy array containing data series in columns.
+ :param xColumnIndex: index of column that will be used as x axis.
+ If it is set to negative number (by default) then row index will be used as x coordinate.
+ :param columnNames: names of each column of the input array. If title is specified for the plot
+ then title+columnName will be used as series name.
+ :param title: title of the chart. Plot node names are set based on this value.
+ :param nodes: plot chart, table, and list of plot series nodes.
+ Specified in a dictionary, with keys: 'chart', 'table', 'series'.
+ Series contains a list of plot series nodes (one for each table column).
+ The parameter is used both as an input and output.
+ :return: plot chart node. Plot chart node provides access to chart properties and plot series nodes.
+
+ Example 1: simple plot
- Example 1: simple plot
+ .. code-block:: python
- .. code-block:: python
+ # Get sample data
+ import numpy as np
+ import SampleData
+ volumeNode = SampleData.downloadSample("MRHead")
- # Get sample data
- import numpy as np
- import SampleData
- volumeNode = SampleData.downloadSample("MRHead")
+ # Create new plot
+ histogram = np.histogram(arrayFromVolume(volumeNode), bins=50)
+ chartNode = plot(histogram, xColumnIndex = 1)
- # Create new plot
- histogram = np.histogram(arrayFromVolume(volumeNode), bins=50)
- chartNode = plot(histogram, xColumnIndex = 1)
+ # Change some plot properties
+ chartNode.SetTitle("My histogram")
+ chartNode.GetNthPlotSeriesNode(0).SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatterBar)
- # Change some plot properties
- chartNode.SetTitle("My histogram")
- chartNode.GetNthPlotSeriesNode(0).SetPlotType(slicer.vtkMRMLPlotSeriesNode.PlotTypeScatterBar)
+ Example 2: plot with multiple updates
- Example 2: plot with multiple updates
+ .. code-block:: python
- .. code-block:: python
+ # Get sample data
+ import numpy as np
+ import SampleData
+ volumeNode = SampleData.downloadSample("MRHead")
- # Get sample data
- import numpy as np
- import SampleData
- volumeNode = SampleData.downloadSample("MRHead")
+ # Create variable that will store plot nodes (chart, table, series)
+ plotNodes = {}
- # Create variable that will store plot nodes (chart, table, series)
- plotNodes = {}
+ # Create new plot
+ histogram = np.histogram(arrayFromVolume(volumeNode), bins=80)
+ plot(histogram, xColumnIndex = 1, nodes = plotNodes)
- # Create new plot
- histogram = np.histogram(arrayFromVolume(volumeNode), bins=80)
- plot(histogram, xColumnIndex = 1, nodes = plotNodes)
+ # Update plot
+ histogram = np.histogram(arrayFromVolume(volumeNode), bins=40)
+ plot(histogram, xColumnIndex = 1, nodes = plotNodes)
+ """
+ import slicer
- # Update plot
- histogram = np.histogram(arrayFromVolume(volumeNode), bins=40)
- plot(histogram, xColumnIndex = 1, nodes = plotNodes)
- """
- import slicer
-
- chartNode = None
- tableNode = None
- seriesNodes = []
-
- # Retrieve nodes that must be reused
- if nodes is not None:
- if 'chart' in nodes:
- chartNode = nodes['chart']
- if 'table' in nodes:
- tableNode = nodes['table']
- if 'series' in nodes:
- seriesNodes = nodes['series']
-
- # Create table node
- if tableNode is None:
- tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode")
-
- if title is not None:
- tableNode.SetName(title + ' table')
- updateTableFromArray(tableNode, narray)
- # Update column names
- numberOfColumns = tableNode.GetTable().GetNumberOfColumns()
- yColumnIndex = 0
- for columnIndex in range(numberOfColumns):
- if (columnNames is not None) and (len(columnNames) > columnIndex):
- columnName = columnNames[columnIndex]
- else:
- if columnIndex == xColumnIndex:
- columnName = "X"
- elif yColumnIndex == 0:
- columnName = "Y"
- yColumnIndex += 1
- else:
- columnName = "Y" + str(yColumnIndex)
- yColumnIndex += 1
- tableNode.GetTable().GetColumn(columnIndex).SetName(columnName)
-
- # Create chart and add plot
- if chartNode is None:
- chartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode")
- if title is not None:
- chartNode.SetName(title + ' chart')
- chartNode.SetTitle(title)
-
- # Create plot series node(s)
- xColumnName = columnNames[xColumnIndex] if (columnNames is not None) and (len(columnNames) > 0) else "X"
- seriesIndex = -1
- for columnIndex in range(numberOfColumns):
- if columnIndex == xColumnIndex:
- continue
- seriesIndex += 1
- if len(seriesNodes) > seriesIndex:
- seriesNode = seriesNodes[seriesIndex]
- else:
- seriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode")
- seriesNodes.append(seriesNode)
- seriesNode.SetUniqueColor()
- seriesNode.SetAndObserveTableNodeID(tableNode.GetID())
- if xColumnIndex < 0:
- seriesNode.SetXColumnName("")
- seriesNode.SetPlotType(seriesNode.PlotTypeLine)
- else:
- seriesNode.SetXColumnName(xColumnName)
- seriesNode.SetPlotType(seriesNode.PlotTypeScatter)
- yColumnName = tableNode.GetTable().GetColumn(columnIndex).GetName()
- seriesNode.SetYColumnName(yColumnName)
- if title:
- seriesNode.SetName(title + " " + yColumnName)
- if not chartNode.HasPlotSeriesNodeID(seriesNode.GetID()):
- chartNode.AddAndObservePlotSeriesNodeID(seriesNode.GetID())
+ chartNode = None
+ tableNode = None
+ seriesNodes = []
+
+ # Retrieve nodes that must be reused
+ if nodes is not None:
+ if 'chart' in nodes:
+ chartNode = nodes['chart']
+ if 'table' in nodes:
+ tableNode = nodes['table']
+ if 'series' in nodes:
+ seriesNodes = nodes['series']
+
+ # Create table node
+ if tableNode is None:
+ tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode")
+
+ if title is not None:
+ tableNode.SetName(title + ' table')
+ updateTableFromArray(tableNode, narray)
+ # Update column names
+ numberOfColumns = tableNode.GetTable().GetNumberOfColumns()
+ yColumnIndex = 0
+ for columnIndex in range(numberOfColumns):
+ if (columnNames is not None) and (len(columnNames) > columnIndex):
+ columnName = columnNames[columnIndex]
+ else:
+ if columnIndex == xColumnIndex:
+ columnName = "X"
+ elif yColumnIndex == 0:
+ columnName = "Y"
+ yColumnIndex += 1
+ else:
+ columnName = "Y" + str(yColumnIndex)
+ yColumnIndex += 1
+ tableNode.GetTable().GetColumn(columnIndex).SetName(columnName)
+
+ # Create chart and add plot
+ if chartNode is None:
+ chartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode")
+ if title is not None:
+ chartNode.SetName(title + ' chart')
+ chartNode.SetTitle(title)
+
+ # Create plot series node(s)
+ xColumnName = columnNames[xColumnIndex] if (columnNames is not None) and (len(columnNames) > 0) else "X"
+ seriesIndex = -1
+ for columnIndex in range(numberOfColumns):
+ if columnIndex == xColumnIndex:
+ continue
+ seriesIndex += 1
+ if len(seriesNodes) > seriesIndex:
+ seriesNode = seriesNodes[seriesIndex]
+ else:
+ seriesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode")
+ seriesNodes.append(seriesNode)
+ seriesNode.SetUniqueColor()
+ seriesNode.SetAndObserveTableNodeID(tableNode.GetID())
+ if xColumnIndex < 0:
+ seriesNode.SetXColumnName("")
+ seriesNode.SetPlotType(seriesNode.PlotTypeLine)
+ else:
+ seriesNode.SetXColumnName(xColumnName)
+ seriesNode.SetPlotType(seriesNode.PlotTypeScatter)
+ yColumnName = tableNode.GetTable().GetColumn(columnIndex).GetName()
+ seriesNode.SetYColumnName(yColumnName)
+ if title:
+ seriesNode.SetName(title + " " + yColumnName)
+ if not chartNode.HasPlotSeriesNodeID(seriesNode.GetID()):
+ chartNode.AddAndObservePlotSeriesNodeID(seriesNode.GetID())
- # Show plot in layout
- if show:
- slicer.modules.plots.logic().ShowChartInLayout(chartNode)
+ # Show plot in layout
+ if show:
+ slicer.modules.plots.logic().ShowChartInLayout(chartNode)
- # Without this, chart view may show up completely empty when the same nodes are updated
- # (this is probably due to a bug in plotting nodes or widgets).
- chartNode.Modified()
+ # Without this, chart view may show up completely empty when the same nodes are updated
+ # (this is probably due to a bug in plotting nodes or widgets).
+ chartNode.Modified()
- if nodes is not None:
- nodes['table'] = tableNode
- nodes['chart'] = chartNode
- nodes['series'] = seriesNodes
+ if nodes is not None:
+ nodes['table'] = tableNode
+ nodes['chart'] = chartNode
+ nodes['series'] = seriesNodes
- return chartNode
+ return chartNode
def launchConsoleProcess(args, useStartupEnvironment=True, updateEnvironment=None, cwd=None):
- """Launch a process. Hiding the console and captures the process output.
+ """Launch a process. Hiding the console and captures the process output.
- The console window is hidden when running on Windows.
+ The console window is hidden when running on Windows.
- :param args: executable name, followed by command-line arguments
- :param useStartupEnvironment: launch the process in the original environment as the original Slicer process
- :param updateEnvironment: map containing optional additional environment variables (existing variables are overwritten)
- :param cwd: current working directory
- :return: process object.
- """
- import subprocess
- import os
- if useStartupEnvironment:
- startupEnv = startupEnvironment()
- if updateEnvironment:
- startupEnv.update(updateEnvironment)
- else:
- if updateEnvironment:
- startupEnv = os.environ.copy()
- startupEnv.update(updateEnvironment)
+ :param args: executable name, followed by command-line arguments
+ :param useStartupEnvironment: launch the process in the original environment as the original Slicer process
+ :param updateEnvironment: map containing optional additional environment variables (existing variables are overwritten)
+ :param cwd: current working directory
+ :return: process object.
+ """
+ import subprocess
+ import os
+ if useStartupEnvironment:
+ startupEnv = startupEnvironment()
+ if updateEnvironment:
+ startupEnv.update(updateEnvironment)
else:
- startupEnv = None
- if os.name == 'nt':
- # Hide console window (only needed on Windows)
- info = subprocess.STARTUPINFO()
- info.dwFlags = 1
- info.wShowWindow = 0
- proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, startupinfo=info, cwd=cwd)
- else:
- proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=cwd)
- return proc
+ if updateEnvironment:
+ startupEnv = os.environ.copy()
+ startupEnv.update(updateEnvironment)
+ else:
+ startupEnv = None
+ if os.name == 'nt':
+ # Hide console window (only needed on Windows)
+ info = subprocess.STARTUPINFO()
+ info.dwFlags = 1
+ info.wShowWindow = 0
+ proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, startupinfo=info, cwd=cwd)
+ else:
+ proc = subprocess.Popen(args, env=startupEnv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, cwd=cwd)
+ return proc
def logProcessOutput(proc):
- """Continuously write process output to the application log and the Python console.
-
- :param proc: process object.
- """
- from subprocess import CalledProcessError
- import logging
- try:
- from slicer import app
- guiApp = app
- except ImportError:
- # Running from console
- guiApp = None
+ """Continuously write process output to the application log and the Python console.
- while True:
+ :param proc: process object.
+ """
+ from subprocess import CalledProcessError
+ import logging
try:
- line = proc.stdout.readline()
- if not line:
- break
- if guiApp:
- logging.info(line.rstrip())
- guiApp.processEvents() # give a chance the application to refresh GUI
- else:
- print(line.rstrip())
- except UnicodeDecodeError as e:
- # Code page conversion happens because `universal_newlines=True` sets process output to text mode,
- # and it fails because probably system locale is not UTF8. We just ignore the error and discard the string,
- # as we only guarantee correct behavior if an UTF8 locale is used.
- pass
-
- proc.wait()
- retcode = proc.returncode
- if retcode != 0:
- raise CalledProcessError(retcode, proc.args, output=proc.stdout, stderr=proc.stderr)
+ from slicer import app
+ guiApp = app
+ except ImportError:
+ # Running from console
+ guiApp = None
+ while True:
+ try:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ if guiApp:
+ logging.info(line.rstrip())
+ guiApp.processEvents() # give a chance the application to refresh GUI
+ else:
+ print(line.rstrip())
+ except UnicodeDecodeError as e:
+ # Code page conversion happens because `universal_newlines=True` sets process output to text mode,
+ # and it fails because probably system locale is not UTF8. We just ignore the error and discard the string,
+ # as we only guarantee correct behavior if an UTF8 locale is used.
+ pass
+
+ proc.wait()
+ retcode = proc.returncode
+ if retcode != 0:
+ raise CalledProcessError(retcode, proc.args, output=proc.stdout, stderr=proc.stderr)
-def _executePythonModule(module, args):
- """Execute a Python module as a script in Slicer's Python environment.
- Internally python -m is called with the module name and additional arguments.
+def _executePythonModule(module, args):
+ """Execute a Python module as a script in Slicer's Python environment.
- :raises RuntimeError: in case of failure
- """
- # Determine pythonSlicerExecutablePath
- try:
- from slicer import app # noqa: F401
- # If we get to this line then import from "app" is succeeded,
- # which means that we run this function from Slicer Python interpreter.
- # PythonSlicer is added to PATH environment variable in Slicer
- # therefore shutil.which will be able to find it.
- import shutil
- pythonSlicerExecutablePath = shutil.which('PythonSlicer')
- if not pythonSlicerExecutablePath:
- raise RuntimeError("PythonSlicer executable not found")
- except ImportError:
- # Running from console
- import os
- import sys
- pythonSlicerExecutablePath = os.path.dirname(sys.executable) + "/PythonSlicer"
- if os.name == 'nt':
- pythonSlicerExecutablePath += ".exe"
+ Internally python -m is called with the module name and additional arguments.
- commandLine = [pythonSlicerExecutablePath, "-m", module, *args]
- proc = launchConsoleProcess(commandLine, useStartupEnvironment=False)
- logProcessOutput(proc)
+ :raises RuntimeError: in case of failure
+ """
+ # Determine pythonSlicerExecutablePath
+ try:
+ from slicer import app # noqa: F401
+ # If we get to this line then import from "app" is succeeded,
+ # which means that we run this function from Slicer Python interpreter.
+ # PythonSlicer is added to PATH environment variable in Slicer
+ # therefore shutil.which will be able to find it.
+ import shutil
+ pythonSlicerExecutablePath = shutil.which('PythonSlicer')
+ if not pythonSlicerExecutablePath:
+ raise RuntimeError("PythonSlicer executable not found")
+ except ImportError:
+ # Running from console
+ import os
+ import sys
+ pythonSlicerExecutablePath = os.path.dirname(sys.executable) + "/PythonSlicer"
+ if os.name == 'nt':
+ pythonSlicerExecutablePath += ".exe"
+
+ commandLine = [pythonSlicerExecutablePath, "-m", module, *args]
+ proc = launchConsoleProcess(commandLine, useStartupEnvironment=False)
+ logProcessOutput(proc)
def pip_install(requirements):
- """Install python packages.
+ """Install python packages.
- Currently, the method simply calls ``python -m pip install`` but in the future further checks, optimizations,
- user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain
- pip install.
- :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html).
- It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list
- if the arguments may contain spaces (because no escaping of the strings with quotes is necessary).
+ Currently, the method simply calls ``python -m pip install`` but in the future further checks, optimizations,
+ user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain
+ pip install.
+ :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html).
+ It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list
+ if the arguments may contain spaces (because no escaping of the strings with quotes is necessary).
- Example: calling from Slicer GUI
+ Example: calling from Slicer GUI
- .. code-block:: python
+ .. code-block:: python
- pip_install("tensorflow keras scikit-learn ipywidgets")
+ pip_install("tensorflow keras scikit-learn ipywidgets")
- Example: calling from PythonSlicer console
+ Example: calling from PythonSlicer console
- .. code-block:: python
+ .. code-block:: python
- from slicer.util import pip_install
- pip_install("tensorflow")
+ from slicer.util import pip_install
+ pip_install("tensorflow")
- """
+ """
- if type(requirements) == str:
- # shlex.split splits string the same way as the shell (keeping quoted string as a single argument)
- import shlex
- args = 'install', *(shlex.split(requirements))
- elif type(requirements) == list:
- args = 'install', *requirements
- else:
- raise ValueError("pip_install requirement input must be string or list")
+ if type(requirements) == str:
+ # shlex.split splits string the same way as the shell (keeping quoted string as a single argument)
+ import shlex
+ args = 'install', *(shlex.split(requirements))
+ elif type(requirements) == list:
+ args = 'install', *requirements
+ else:
+ raise ValueError("pip_install requirement input must be string or list")
- _executePythonModule('pip', args)
+ _executePythonModule('pip', args)
def pip_uninstall(requirements):
- """Uninstall python packages.
+ """Uninstall python packages.
- Currently, the method simply calls ``python -m pip uninstall`` but in the future further checks, optimizations,
- user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain
- pip uninstall.
+ Currently, the method simply calls ``python -m pip uninstall`` but in the future further checks, optimizations,
+ user confirmation may be implemented, therefore it is recommended to use this method call instead of a plain
+ pip uninstall.
- :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html).
- It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list
- if the arguments may contain spaces (because no escaping of the strings with quotes is necessary).
+ :param requirements: requirement specifier in the same format as used by pip (https://docs.python.org/3/installing/index.html).
+ It can be either a single string or a list of command-line arguments. It may be simpler to pass command-line arguments as a list
+ if the arguments may contain spaces (because no escaping of the strings with quotes is necessary).
- Example: calling from Slicer GUI
+ Example: calling from Slicer GUI
- .. code-block:: python
+ .. code-block:: python
- pip_uninstall("tensorflow keras scikit-learn ipywidgets")
+ pip_uninstall("tensorflow keras scikit-learn ipywidgets")
- Example: calling from PythonSlicer console
+ Example: calling from PythonSlicer console
- .. code-block:: python
+ .. code-block:: python
- from slicer.util import pip_uninstall
- pip_uninstall("tensorflow")
+ from slicer.util import pip_uninstall
+ pip_uninstall("tensorflow")
- """
- if type(requirements) == str:
- # shlex.split splits string the same way as the shell (keeping quoted string as a single argument)
- import shlex
- args = 'uninstall', *(shlex.split(requirements)), '--yes'
- elif type(requirements) == list:
- args = 'uninstall', *requirements, '--yes'
- else:
- raise ValueError("pip_uninstall requirement input must be string or list")
- _executePythonModule('pip', args)
+ """
+ if type(requirements) == str:
+ # shlex.split splits string the same way as the shell (keeping quoted string as a single argument)
+ import shlex
+ args = 'uninstall', *(shlex.split(requirements)), '--yes'
+ elif type(requirements) == list:
+ args = 'uninstall', *requirements, '--yes'
+ else:
+ raise ValueError("pip_uninstall requirement input must be string or list")
+ _executePythonModule('pip', args)
def longPath(path):
- """Make long paths work on Windows, where the maximum path length is 260 characters.
+ """Make long paths work on Windows, where the maximum path length is 260 characters.
- For example, the files in the DICOM database may have paths longer than this limit.
- Accessing these can be made safe by prefixing it with the UNC prefix ('\\?\').
+ For example, the files in the DICOM database may have paths longer than this limit.
+ Accessing these can be made safe by prefixing it with the UNC prefix ('\\?\').
- :param string path: Path to be made safe if too long
+ :param string path: Path to be made safe if too long
- :return string: Safe path
- """
- # Return path as is if conversion is disabled
- longPathConversionEnabled = settingsValue('General/LongPathConversionEnabled', True, converter=toBool)
- if not longPathConversionEnabled:
- return path
- # Return path as is on operating systems other than Windows
- import qt
- sysInfo = qt.QSysInfo()
- if sysInfo.productType() != 'windows':
- return path
- # Skip prefixing relative paths as UNC prefix wors only on absolute paths
- if not qt.QDir.isAbsolutePath(path):
- return path
- # Return path as is if UNC prefix is already applied
- if path[:4] == '\\\\?\\':
- return path
- return "\\\\?\\" + path.replace('/', '\\')
+ :return string: Safe path
+ """
+ # Return path as is if conversion is disabled
+ longPathConversionEnabled = settingsValue('General/LongPathConversionEnabled', True, converter=toBool)
+ if not longPathConversionEnabled:
+ return path
+ # Return path as is on operating systems other than Windows
+ import qt
+ sysInfo = qt.QSysInfo()
+ if sysInfo.productType() != 'windows':
+ return path
+ # Skip prefixing relative paths as UNC prefix wors only on absolute paths
+ if not qt.QDir.isAbsolutePath(path):
+ return path
+ # Return path as is if UNC prefix is already applied
+ if path[:4] == '\\\\?\\':
+ return path
+ return "\\\\?\\" + path.replace('/', '\\')
diff --git a/Base/Python/tests/test_sitkUtils.py b/Base/Python/tests/test_sitkUtils.py
index 11574fd31d6..2c72c23bb4c 100644
--- a/Base/Python/tests/test_sitkUtils.py
+++ b/Base/Python/tests/test_sitkUtils.py
@@ -31,7 +31,7 @@ def test_SimpleITK_SlicerPushPull(self):
self.assertEqual(volumeNode1, slicer.util.getNode('MRHead'),
'Original volume is changed')
self.assertNotEqual(volumeNode1, volumeNode1Copy,
- 'Copy of original volume is not created')
+ 'Copy of original volume is not created')
""" Few modification of the image : Direction, Origin """
sitkimage.SetDirection((-1.0, 1.0, 0.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0))
@@ -67,8 +67,8 @@ def test_SimpleITK_SlicerPushPull(self):
if pushToNewNode:
volumeNodeTested = su.PushVolumeToSlicer(sitkimage,
- name='volumeNode-' + volumeClassName + "-" + str(pushToNewNode),
- className=volumeClassName)
+ name='volumeNode-' + volumeClassName + "-" + str(pushToNewNode),
+ className=volumeClassName)
existingVolumeNode = volumeNodeTested
else:
volumeNodeTested = su.PushVolumeToSlicer(sitkimage, existingVolumeNode)
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py
index 82c150f00a0..d75e7e6d930 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTest.py
@@ -1,16 +1,16 @@
class qSlicerScriptedLoadableModuleNewStyleTest:
- def __init__(self, parent):
- parent.title = "qSlicerScriptedLoadableModuleNewStyle Test"
- parent.categories = ["Testing"]
- parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"]
- parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware); Max Smolens (Kitware)"]
- parent.helpText = """
+ def __init__(self, parent):
+ parent.title = "qSlicerScriptedLoadableModuleNewStyle Test"
+ parent.categories = ["Testing"]
+ parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"]
+ parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware); Max Smolens (Kitware)"]
+ parent.helpText = """
This module is for testing.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
Based on qSlicerScriptedLoadableModuleTest .
"""
- self.parent = parent
+ self.parent = parent
- def setup(self):
- self.parent.setProperty('setup_called_within_Python', True)
+ def setup(self):
+ self.parent.setProperty('setup_called_within_Python', True)
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py
index acb8587cd0e..14f34357896 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleNewStyleTestWidget.py
@@ -1,22 +1,22 @@
class qSlicerScriptedLoadableModuleNewStyleTestWidget:
- def __init__(self, parent=None):
- self.parent = parent
+ def __init__(self, parent=None):
+ self.parent = parent
- def setup(self):
- self.parent.setProperty('setup_called_within_Python', True)
+ def setup(self):
+ self.parent.setProperty('setup_called_within_Python', True)
- def enter(self):
- self.parent.setProperty('enter_called_within_Python', True)
+ def enter(self):
+ self.parent.setProperty('enter_called_within_Python', True)
- def exit(self):
- self.parent.setProperty('exit_called_within_Python', True)
+ def exit(self):
+ self.parent.setProperty('exit_called_within_Python', True)
- def setEditedNode(self, node, role='', context=''):
- self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "")
- self.parent.setProperty('editedNodeRole', role)
- self.parent.setProperty('editedNodeContext', context)
- return (node is not None)
+ def setEditedNode(self, node, role='', context=''):
+ self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "")
+ self.parent.setProperty('editedNodeRole', role)
+ self.parent.setProperty('editedNodeContext', context)
+ return (node is not None)
- def nodeEditable(self, node):
- self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "")
- return 0.7 if node is not None else 0.3
+ def nodeEditable(self, node):
+ self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "")
+ return 0.7 if node is not None else 0.3
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py
index 140670e201c..2d93886e157 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTest.py
@@ -1,3 +1,3 @@
class qSlicerScriptedLoadableModuleSyntaxErrorTest:
- def __init__(self, parent):
- s elf.parent = parent # noqa: E999
+ def __init__(self, parent):
+ s elf.parent = parent # noqa: E999
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py
index 19b07adaf56..3b3d3cbac9c 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleSyntaxErrorTestWidget.py
@@ -1,3 +1,3 @@
class qSlicerScriptedLoadableModuleSyntaxErrorTestWidget:
- def __init__(self, parent=None):
- s elf.parent = parent # noqa: E999
+ def __init__(self, parent=None):
+ s elf.parent = parent # noqa: E999
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py
index 8c79907143a..2451c6cf8c2 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTest.py
@@ -1,30 +1,30 @@
class qSlicerScriptedLoadableModuleTest:
- def __init__(self, parent):
- parent.title = "qSlicerScriptedLoadableModule Test"
- parent.categories = ["Testing"]
- parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"]
- parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"]
- parent.helpText = """
+ def __init__(self, parent):
+ parent.title = "qSlicerScriptedLoadableModule Test"
+ parent.categories = ["Testing"]
+ parent.associatedNodeTypes = ["vtkMRMLModelNode", "vtkMRMLScalarVolumeNode"]
+ parent.contributors = ["Jean-Christophe Fillion-Robin (Kitware)"]
+ parent.helpText = """
This module is used to test qSlicerScriptedLoadableModule and qSlicerScriptedLoadableModuleWidget classes.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc. and was partially funded by NIH grant 3P41RR013218-12S1
"""
- self.parent = parent
+ self.parent = parent
- def setup(self):
- self.parent.setProperty('setup_called_within_Python', True)
+ def setup(self):
+ self.parent.setProperty('setup_called_within_Python', True)
class qSlicerScriptedLoadableModuleTestWidget:
- def __init__(self, parent=None):
- self.parent = parent
+ def __init__(self, parent=None):
+ self.parent = parent
- def setup(self):
- self.parent.setProperty('setup_called_within_Python', True)
+ def setup(self):
+ self.parent.setProperty('setup_called_within_Python', True)
- def enter(self):
- self.parent.setProperty('enter_called_within_Python', True)
+ def enter(self):
+ self.parent.setProperty('enter_called_within_Python', True)
- def exit(self):
- self.parent.setProperty('exit_called_within_Python', True)
+ def exit(self):
+ self.parent.setProperty('exit_called_within_Python', True)
diff --git a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py
index 72e89524bc3..371b00b4b0b 100644
--- a/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py
+++ b/Base/QTGUI/Testing/Data/Input/qSlicerScriptedLoadableModuleTestWidget.py
@@ -1,22 +1,22 @@
class qSlicerScriptedLoadableModuleTestWidget:
- def __init__(self, parent=None):
- self.parent = parent
+ def __init__(self, parent=None):
+ self.parent = parent
- def setup(self):
- self.parent.setProperty('setup_called_within_Python', True)
+ def setup(self):
+ self.parent.setProperty('setup_called_within_Python', True)
- def enter(self):
- self.parent.setProperty('enter_called_within_Python', True)
+ def enter(self):
+ self.parent.setProperty('enter_called_within_Python', True)
- def exit(self):
- self.parent.setProperty('exit_called_within_Python', True)
+ def exit(self):
+ self.parent.setProperty('exit_called_within_Python', True)
- def setEditedNode(self, node, role='', context=''):
- self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "")
- self.parent.setProperty('editedNodeRole', role)
- self.parent.setProperty('editedNodeContext', context)
- return (node is not None)
+ def setEditedNode(self, node, role='', context=''):
+ self.parent.setProperty('editedNodeName', node.GetName() if node is not None else "")
+ self.parent.setProperty('editedNodeRole', role)
+ self.parent.setProperty('editedNodeContext', context)
+ return (node is not None)
- def nodeEditable(self, node):
- self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "")
- return 0.7 if node is not None else 0.3
+ def nodeEditable(self, node):
+ self.parent.setProperty('editableNodeName', node.GetName() if node is not None else "")
+ return 0.7 if node is not None else 0.3
diff --git a/Docs/conf.py b/Docs/conf.py
index 56efdc592cc..42a88d2e6b6 100644
--- a/Docs/conf.py
+++ b/Docs/conf.py
@@ -241,7 +241,7 @@
inputpaths = [
os.path.join(docsfolder, "../Modules/CLI"),
os.path.join(docsfolder, "_extracli"),
- ]
+]
# List of modules to be excluded from documentation generation
# (for example, testing modules only).
@@ -249,7 +249,7 @@
'CLIROITest.xml',
'TestGridTransformRegistration.xml',
'DiffusionTensorTest.xml',
- ]
+]
# Output folder that contains all generated markdown files.
outpath = os.path.join(docsfolder, "_moduledescriptions")
diff --git a/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py b/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py
index 9efe4ee299d..70f8f06df5f 100644
--- a/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py
+++ b/Extensions/Testing/ScriptedLoadableExtensionTemplate/ScriptedLoadableModuleTemplate/ScriptedLoadableModuleTemplate.py
@@ -13,22 +13,22 @@
class ScriptedLoadableModuleTemplate(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "ScriptedLoadableModuleTemplate" # TODO make this more human readable by adding spaces
- self.parent.categories = ["Examples"]
- self.parent.dependencies = []
- self.parent.contributors = ["John Doe (AnyWare Corp.)"] # replace with "Firstname Lastname (Organization)"
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "ScriptedLoadableModuleTemplate" # TODO make this more human readable by adding spaces
+ self.parent.categories = ["Examples"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["John Doe (AnyWare Corp.)"] # replace with "Firstname Lastname (Organization)"
+ self.parent.helpText = """
This is an example of scripted loadable module bundled in an extension.
It performs a simple thresholding on the input volume and optionally captures a screenshot.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1.
""" # replace with organization, grant and thanks.
@@ -39,104 +39,104 @@ def __init__(self, parent):
class ScriptedLoadableModuleTemplateWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Instantiate and connect widgets ...
-
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
-
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- #
- # input volume selector
- #
- self.inputSelector = slicer.qMRMLNodeComboBox()
- self.inputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
- self.inputSelector.selectNodeUponCreation = True
- self.inputSelector.addEnabled = False
- self.inputSelector.removeEnabled = False
- self.inputSelector.noneEnabled = False
- self.inputSelector.showHidden = False
- self.inputSelector.showChildNodeTypes = False
- self.inputSelector.setMRMLScene(slicer.mrmlScene)
- self.inputSelector.setToolTip("Pick the input to the algorithm.")
- parametersFormLayout.addRow("Input Volume: ", self.inputSelector)
-
- #
- # output volume selector
- #
- self.outputSelector = slicer.qMRMLNodeComboBox()
- self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
- self.outputSelector.selectNodeUponCreation = True
- self.outputSelector.addEnabled = True
- self.outputSelector.removeEnabled = True
- self.outputSelector.noneEnabled = True
- self.outputSelector.showHidden = False
- self.outputSelector.showChildNodeTypes = False
- self.outputSelector.setMRMLScene(slicer.mrmlScene)
- self.outputSelector.setToolTip("Pick the output to the algorithm.")
- parametersFormLayout.addRow("Output Volume: ", self.outputSelector)
-
- #
- # threshold value
- #
- self.imageThresholdSliderWidget = ctk.ctkSliderWidget()
- self.imageThresholdSliderWidget.singleStep = 0.1
- self.imageThresholdSliderWidget.minimum = -100
- self.imageThresholdSliderWidget.maximum = 100
- self.imageThresholdSliderWidget.value = 0.5
- self.imageThresholdSliderWidget.setToolTip("Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero.")
- parametersFormLayout.addRow("Image threshold", self.imageThresholdSliderWidget)
-
- #
- # check box to trigger taking screen shots for later use in tutorials
- #
- self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
- self.enableScreenshotsFlagCheckBox.checked = 0
- self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
- parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
-
- #
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = False
- parametersFormLayout.addRow(self.applyButton)
-
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
- self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
- self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- # Refresh Apply button state
- self.onSelect()
-
- def cleanup(self):
- pass
-
- def onSelect(self):
- self.applyButton.enabled = self.inputSelector.currentNode() and self.outputSelector.currentNode()
-
- def onApplyButton(self):
- logic = ScriptedLoadableModuleTemplateLogic()
- enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked
- imageThreshold = self.imageThresholdSliderWidget.value
- logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), imageThreshold, enableScreenshotsFlag)
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Instantiate and connect widgets ...
+
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
+
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ #
+ # input volume selector
+ #
+ self.inputSelector = slicer.qMRMLNodeComboBox()
+ self.inputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
+ self.inputSelector.selectNodeUponCreation = True
+ self.inputSelector.addEnabled = False
+ self.inputSelector.removeEnabled = False
+ self.inputSelector.noneEnabled = False
+ self.inputSelector.showHidden = False
+ self.inputSelector.showChildNodeTypes = False
+ self.inputSelector.setMRMLScene(slicer.mrmlScene)
+ self.inputSelector.setToolTip("Pick the input to the algorithm.")
+ parametersFormLayout.addRow("Input Volume: ", self.inputSelector)
+
+ #
+ # output volume selector
+ #
+ self.outputSelector = slicer.qMRMLNodeComboBox()
+ self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
+ self.outputSelector.selectNodeUponCreation = True
+ self.outputSelector.addEnabled = True
+ self.outputSelector.removeEnabled = True
+ self.outputSelector.noneEnabled = True
+ self.outputSelector.showHidden = False
+ self.outputSelector.showChildNodeTypes = False
+ self.outputSelector.setMRMLScene(slicer.mrmlScene)
+ self.outputSelector.setToolTip("Pick the output to the algorithm.")
+ parametersFormLayout.addRow("Output Volume: ", self.outputSelector)
+
+ #
+ # threshold value
+ #
+ self.imageThresholdSliderWidget = ctk.ctkSliderWidget()
+ self.imageThresholdSliderWidget.singleStep = 0.1
+ self.imageThresholdSliderWidget.minimum = -100
+ self.imageThresholdSliderWidget.maximum = 100
+ self.imageThresholdSliderWidget.value = 0.5
+ self.imageThresholdSliderWidget.setToolTip("Set threshold value for computing the output image. Voxels that have intensities lower than this value will set to zero.")
+ parametersFormLayout.addRow("Image threshold", self.imageThresholdSliderWidget)
+
+ #
+ # check box to trigger taking screen shots for later use in tutorials
+ #
+ self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
+ self.enableScreenshotsFlagCheckBox.checked = 0
+ self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
+ parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
+
+ #
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = False
+ parametersFormLayout.addRow(self.applyButton)
+
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
+ self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ # Refresh Apply button state
+ self.onSelect()
+
+ def cleanup(self):
+ pass
+
+ def onSelect(self):
+ self.applyButton.enabled = self.inputSelector.currentNode() and self.outputSelector.currentNode()
+
+ def onApplyButton(self):
+ logic = ScriptedLoadableModuleTemplateLogic()
+ enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked
+ imageThreshold = self.imageThresholdSliderWidget.value
+ logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), imageThreshold, enableScreenshotsFlag)
#
# ScriptedLoadableModuleTemplateLogic
@@ -144,105 +144,105 @@ def onApplyButton(self):
class ScriptedLoadableModuleTemplateLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def hasImageData(self, volumeNode):
- """This is an example logic method that
- returns true if the passed in volume
- node has valid image data
- """
- if not volumeNode:
- logging.debug('hasImageData failed: no volume node')
- return False
- if volumeNode.GetImageData() is None:
- logging.debug('hasImageData failed: no image data in volume node')
- return False
- return True
-
- def isValidInputOutputData(self, inputVolumeNode, outputVolumeNode):
- """Validates if the output is not the same as input
- """
- if not inputVolumeNode:
- logging.debug('isValidInputOutputData failed: no input volume node defined')
- return False
- if not outputVolumeNode:
- logging.debug('isValidInputOutputData failed: no output volume node defined')
- return False
- if inputVolumeNode.GetID() == outputVolumeNode.GetID():
- logging.debug('isValidInputOutputData failed: input and output volume is the same. Create a new volume for output to avoid this error.')
- return False
- return True
-
- def run(self, inputVolume, outputVolume, imageThreshold):
- """
- Run the actual algorithm
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- if not self.isValidInputOutputData(inputVolume, outputVolume):
- slicer.util.errorDisplay('Input volume is the same as output volume. Choose a different output volume.')
- return False
-
- logging.info('Processing started')
-
- # Compute the thresholded output volume using the Threshold Scalar Volume CLI module
- cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue': imageThreshold, 'ThresholdType': 'Above'}
- cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True)
-
- logging.info('Processing completed')
-
- return True
+ def hasImageData(self, volumeNode):
+ """This is an example logic method that
+ returns true if the passed in volume
+ node has valid image data
+ """
+ if not volumeNode:
+ logging.debug('hasImageData failed: no volume node')
+ return False
+ if volumeNode.GetImageData() is None:
+ logging.debug('hasImageData failed: no image data in volume node')
+ return False
+ return True
+
+ def isValidInputOutputData(self, inputVolumeNode, outputVolumeNode):
+ """Validates if the output is not the same as input
+ """
+ if not inputVolumeNode:
+ logging.debug('isValidInputOutputData failed: no input volume node defined')
+ return False
+ if not outputVolumeNode:
+ logging.debug('isValidInputOutputData failed: no output volume node defined')
+ return False
+ if inputVolumeNode.GetID() == outputVolumeNode.GetID():
+ logging.debug('isValidInputOutputData failed: input and output volume is the same. Create a new volume for output to avoid this error.')
+ return False
+ return True
+
+ def run(self, inputVolume, outputVolume, imageThreshold):
+ """
+ Run the actual algorithm
+ """
+
+ if not self.isValidInputOutputData(inputVolume, outputVolume):
+ slicer.util.errorDisplay('Input volume is the same as output volume. Choose a different output volume.')
+ return False
+
+ logging.info('Processing started')
+
+ # Compute the thresholded output volume using the Threshold Scalar Volume CLI module
+ cliParams = {'InputVolume': inputVolume.GetID(), 'OutputVolume': outputVolume.GetID(), 'ThresholdValue': imageThreshold, 'ThresholdType': 'Above'}
+ cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True)
+
+ logging.info('Processing completed')
+
+ return True
class ScriptedLoadableModuleTemplateTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
"""
- self.setUp()
- self.test_ScriptedLoadableModuleTemplate1()
-
- def test_ScriptedLoadableModuleTemplate1(self):
- """ Ideally you should have several levels of tests. At the lowest level
- tests should exercise the functionality of the logic with different inputs
- (both valid and invalid). At higher levels your tests should emulate the
- way the user would interact with your code and confirm that it still works
- the way you intended.
- One of the most important features of the tests is that it should alert other
- developers when their changes will have an impact on the behavior of your
- module. For example, if a developer removes a feature that you depend on,
- your test should break so they know that the feature is needed.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting the test")
- #
- # first, get some data
- #
- import SampleData
- volumeNode = SampleData.downloadFromURL(
- nodeNames='MRHead',
- fileNames='MR-head.nrrd',
- uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- checksums='SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93')
- self.delayDisplay('Finished with download and loading')
-
- logic = ScriptedLoadableModuleTemplateLogic()
- self.assertIsNotNone(logic.hasImageData(volumeNode))
- self.takeScreenshot('ScriptedLoadableModuleTemplateTest-Start', 'MyScreenshot', -1)
- self.delayDisplay('Test passed!')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_ScriptedLoadableModuleTemplate1()
+
+ def test_ScriptedLoadableModuleTemplate1(self):
+ """ Ideally you should have several levels of tests. At the lowest level
+ tests should exercise the functionality of the logic with different inputs
+ (both valid and invalid). At higher levels your tests should emulate the
+ way the user would interact with your code and confirm that it still works
+ the way you intended.
+ One of the most important features of the tests is that it should alert other
+ developers when their changes will have an impact on the behavior of your
+ module. For example, if a developer removes a feature that you depend on,
+ your test should break so they know that the feature is needed.
+ """
+
+ self.delayDisplay("Starting the test")
+ #
+ # first, get some data
+ #
+ import SampleData
+ volumeNode = SampleData.downloadFromURL(
+ nodeNames='MRHead',
+ fileNames='MR-head.nrrd',
+ uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ checksums='SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93')
+ self.delayDisplay('Finished with download and loading')
+
+ logic = ScriptedLoadableModuleTemplateLogic()
+ self.assertIsNotNone(logic.hasImageData(volumeNode))
+ self.takeScreenshot('ScriptedLoadableModuleTemplateTest-Start', 'MyScreenshot', -1)
+ self.delayDisplay('Test passed!')
diff --git a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py
index c75bdfb0e31..305c48dbc94 100644
--- a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py
+++ b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplate.py
@@ -6,133 +6,133 @@
class SegmentEditorScriptedSegmentEditorEffectModuleTemplate(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "SegmentEditorScriptedSegmentEditorEffectModuleTemplate"
- self.parent.categories = ["Segmentation"]
- self.parent.dependencies = ["Segmentations"]
- self.parent.contributors = ["Andras Lasso (PerkLab)"]
- self.parent.hidden = True
- self.parent.helpText = "This hidden module registers the segment editor effect"
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details."
- slicer.app.connect("startupCompleted()", self.registerEditorEffect)
-
- def registerEditorEffect(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects
- instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None)
- effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py')
- instance.setPythonSource(effectFilename.replace('\\', '/'))
- instance.self().register()
-
-
-class SegmentEditorScriptedSegmentEditorEffectModuleTemplateTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- slicer.mrmlScene.Clear(0)
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_ScriptedSegmentEditorEffectModuleTemplate1()
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "SegmentEditorScriptedSegmentEditorEffectModuleTemplate"
+ self.parent.categories = ["Segmentation"]
+ self.parent.dependencies = ["Segmentations"]
+ self.parent.contributors = ["Andras Lasso (PerkLab)"]
+ self.parent.hidden = True
+ self.parent.helpText = "This hidden module registers the segment editor effect"
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details."
+ slicer.app.connect("startupCompleted()", self.registerEditorEffect)
+
+ def registerEditorEffect(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects
+ instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None)
+ effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py')
+ instance.setPythonSource(effectFilename.replace('\\', '/'))
+ instance.self().register()
+
- def test_ScriptedSegmentEditorEffectModuleTemplate1(self):
+class SegmentEditorScriptedSegmentEditorEffectModuleTemplateTest(ScriptedLoadableModuleTest):
"""
- Basic automated test of the segmentation method:
- - Create segmentation by placing sphere-shaped seeds
- - Run segmentation
- - Verify results using segment statistics
- The test can be executed from SelfTests module (test name: SegmentEditorScriptedSegmentEditorEffectModuleTemplate)
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting test_ScriptedSegmentEditorEffectModuleTemplate1")
-
- import vtkSegmentationCorePython as vtkSegmentationCore
- import SampleData
- from SegmentStatistics import SegmentStatisticsLogic
-
- ##################################
- self.delayDisplay("Load master volume")
-
- masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
-
- ##################################
- self.delayDisplay("Create segmentation containing a few spheres")
-
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- segmentationNode.CreateDefaultDisplayNodes()
- segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
-
- # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ]
- segmentGeometries = [
- ['Tumor', [[10, -6, 30, 28]]],
- ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]],
- ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]]
- for segmentGeometry in segmentGeometries:
- segmentName = segmentGeometry[0]
- appender = vtk.vtkAppendPolyData()
- for sphere in segmentGeometry[1]:
- sphereSource = vtk.vtkSphereSource()
- sphereSource.SetRadius(sphere[0])
- sphereSource.SetCenter(sphere[1], sphere[2], sphere[3])
- appender.AddInputConnection(sphereSource.GetOutputPort())
- segment = vtkSegmentationCore.vtkSegment()
- segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName))
- appender.Update()
- segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput())
- segmentationNode.GetSegmentation().AddSegment(segment)
-
- ##################################
- self.delayDisplay("Create segment editor")
-
- segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
- segmentEditorWidget.show()
- segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
- segmentEditorNode = slicer.vtkMRMLSegmentEditorNode()
- slicer.mrmlScene.AddNode(segmentEditorNode)
- segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)
- segmentEditorWidget.setSegmentationNode(segmentationNode)
- segmentEditorWidget.setMasterVolumeNode(masterVolumeNode)
-
- ##################################
- self.delayDisplay("Run segmentation")
- segmentEditorWidget.setActiveEffectByName("ScriptedSegmentEditorEffectModuleTemplate")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("ObjectScaleMm", 3.0)
- effect.self().onApply()
-
- ##################################
- self.delayDisplay("Make segmentation results nicely visible in 3D")
- segmentationDisplayNode = segmentationNode.GetDisplayNode()
- segmentationDisplayNode.SetSegmentVisibility("Air", False)
- segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5)
-
- ##################################
- self.delayDisplay("Compute statistics")
-
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.computeStatistics(segmentationNode, masterVolumeNode)
-
- # Export results to table (just to see all results)
- resultsTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(resultsTableNode)
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
-
- self.delayDisplay("Check a few numerical results")
- self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16)
- self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010)
-
- self.delayDisplay('test_ScriptedSegmentEditorEffectModuleTemplate1 passed')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_ScriptedSegmentEditorEffectModuleTemplate1()
+
+ def test_ScriptedSegmentEditorEffectModuleTemplate1(self):
+ """
+ Basic automated test of the segmentation method:
+ - Create segmentation by placing sphere-shaped seeds
+ - Run segmentation
+ - Verify results using segment statistics
+ The test can be executed from SelfTests module (test name: SegmentEditorScriptedSegmentEditorEffectModuleTemplate)
+ """
+
+ self.delayDisplay("Starting test_ScriptedSegmentEditorEffectModuleTemplate1")
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ import SampleData
+ from SegmentStatistics import SegmentStatisticsLogic
+
+ ##################################
+ self.delayDisplay("Load master volume")
+
+ masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
+
+ ##################################
+ self.delayDisplay("Create segmentation containing a few spheres")
+
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ segmentationNode.CreateDefaultDisplayNodes()
+ segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
+
+ # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ]
+ segmentGeometries = [
+ ['Tumor', [[10, -6, 30, 28]]],
+ ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]],
+ ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]]
+ for segmentGeometry in segmentGeometries:
+ segmentName = segmentGeometry[0]
+ appender = vtk.vtkAppendPolyData()
+ for sphere in segmentGeometry[1]:
+ sphereSource = vtk.vtkSphereSource()
+ sphereSource.SetRadius(sphere[0])
+ sphereSource.SetCenter(sphere[1], sphere[2], sphere[3])
+ appender.AddInputConnection(sphereSource.GetOutputPort())
+ segment = vtkSegmentationCore.vtkSegment()
+ segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName))
+ appender.Update()
+ segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput())
+ segmentationNode.GetSegmentation().AddSegment(segment)
+
+ ##################################
+ self.delayDisplay("Create segment editor")
+
+ segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
+ segmentEditorWidget.show()
+ segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
+ segmentEditorNode = slicer.vtkMRMLSegmentEditorNode()
+ slicer.mrmlScene.AddNode(segmentEditorNode)
+ segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)
+ segmentEditorWidget.setSegmentationNode(segmentationNode)
+ segmentEditorWidget.setMasterVolumeNode(masterVolumeNode)
+
+ ##################################
+ self.delayDisplay("Run segmentation")
+ segmentEditorWidget.setActiveEffectByName("ScriptedSegmentEditorEffectModuleTemplate")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("ObjectScaleMm", 3.0)
+ effect.self().onApply()
+
+ ##################################
+ self.delayDisplay("Make segmentation results nicely visible in 3D")
+ segmentationDisplayNode = segmentationNode.GetDisplayNode()
+ segmentationDisplayNode.SetSegmentVisibility("Air", False)
+ segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5)
+
+ ##################################
+ self.delayDisplay("Compute statistics")
+
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.computeStatistics(segmentationNode, masterVolumeNode)
+
+ # Export results to table (just to see all results)
+ resultsTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(resultsTableNode)
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+
+ self.delayDisplay("Check a few numerical results")
+ self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16)
+ self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010)
+
+ self.delayDisplay('test_ScriptedSegmentEditorEffectModuleTemplate1 passed')
diff --git a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py
index 6ba16c89780..4dba1b6034b 100644
--- a/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py
+++ b/Extensions/Testing/ScriptedSegmentEditorEffectExtensionTemplate/ScriptedSegmentEditorEffectModuleTemplate/SegmentEditorScriptedSegmentEditorEffectModuleTemplateLib/SegmentEditorEffect.py
@@ -10,123 +10,123 @@
class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect):
- """This effect uses Watershed algorithm to partition the input volume"""
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'ScriptedSegmentEditorEffectModuleTemplate'
- scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment)
- scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- # It should not be necessary to modify this method
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- # It should not be necessary to modify this method
- iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Existing segments are grown to fill the image.
+ """This effect uses Watershed algorithm to partition the input volume"""
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'ScriptedSegmentEditorEffectModuleTemplate'
+ scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment)
+ scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ # It should not be necessary to modify this method
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ # It should not be necessary to modify this method
+ iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Existing segments are grown to fill the image.
The effect is different from the Grow from seeds effect in that smoothness of structures can be defined, which can prevent leakage.
To segment a single object, create a segment and paint inside and create another segment and paint outside on each axis.
"""
- def setupOptionsFrame(self):
-
- # Object scale slider
- self.objectScaleMmSlider = slicer.qMRMLSliderWidget()
- self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene)
- self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node
- self.objectScaleMmSlider.minimum = 0
- self.objectScaleMmSlider.maximum = 10
- self.objectScaleMmSlider.value = 2.0
- self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.')
- self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider)
- self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI)
-
- # Apply button
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Accept previewed result")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
- self.applyButton.connect('clicked()', self.onApply)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0)
-
- def updateGUIFromMRML(self):
- objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm")
- wasBlocked = self.objectScaleMmSlider.blockSignals(True)
- self.objectScaleMmSlider.value = abs(objectScaleMm)
- self.objectScaleMmSlider.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value)
-
- def onApply(self):
-
- # Get list of visible segment IDs, as the effect ignores hidden segments.
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- visibleSegmentIds = vtk.vtkStringArray()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
- if visibleSegmentIds.GetNumberOfValues() == 0:
- logging.info("Smoothing operation skipped: there are no visible segments")
- return
-
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- # Allow users revert to this state by clicking Undo
- self.scriptedEffect.saveStateForUndo()
-
- # Export master image data to temporary new volume node.
- # Note: Although the original master volume node is already in the scene, we do not use it here,
- # because the master volume may have been resampled to match segmentation geometry.
- masterVolumeNode = slicer.vtkMRMLScalarVolumeNode()
- slicer.mrmlScene.AddNode(masterVolumeNode)
- masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID())
- slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode)
- # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels.
- mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
- slicer.mrmlScene.AddNode(mergedLabelmapNode)
- slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode)
-
- # Run segmentation algorithm
- import SimpleITK as sitk
- import sitkUtils
- # Read input data from Slicer into SimpleITK
- labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
- backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName()))
- # Run watershed filter
- featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm")))
- del backgroundImage
- f = sitk.MorphologicalWatershedFromMarkersImageFilter()
- f.SetMarkWatershedLine(False)
- f.SetFullyConnected(False)
- labelImage = f.Execute(featureImage, labelImage)
- del featureImage
- # Pixel type of watershed output is the same as the input. Convert it to int16 now.
- if labelImage.GetPixelID() != sitk.sitkInt16:
- labelImage = sitk.Cast(labelImage, sitk.sitkInt16)
- # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data.
- sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
- mergedLabelmapNode.GetImageData().Modified()
- mergedLabelmapNode.Modified()
-
- # Update segmentation from labelmap node and remove temporary nodes
- slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds)
- slicer.mrmlScene.RemoveNode(masterVolumeNode)
- slicer.mrmlScene.RemoveNode(mergedLabelmapNode)
-
- qt.QApplication.restoreOverrideCursor()
+ def setupOptionsFrame(self):
+
+ # Object scale slider
+ self.objectScaleMmSlider = slicer.qMRMLSliderWidget()
+ self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene)
+ self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node
+ self.objectScaleMmSlider.minimum = 0
+ self.objectScaleMmSlider.maximum = 10
+ self.objectScaleMmSlider.value = 2.0
+ self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.')
+ self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider)
+ self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI)
+
+ # Apply button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Accept previewed result")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+ self.applyButton.connect('clicked()', self.onApply)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0)
+
+ def updateGUIFromMRML(self):
+ objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm")
+ wasBlocked = self.objectScaleMmSlider.blockSignals(True)
+ self.objectScaleMmSlider.value = abs(objectScaleMm)
+ self.objectScaleMmSlider.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value)
+
+ def onApply(self):
+
+ # Get list of visible segment IDs, as the effect ignores hidden segments.
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ visibleSegmentIds = vtk.vtkStringArray()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
+ if visibleSegmentIds.GetNumberOfValues() == 0:
+ logging.info("Smoothing operation skipped: there are no visible segments")
+ return
+
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ # Allow users revert to this state by clicking Undo
+ self.scriptedEffect.saveStateForUndo()
+
+ # Export master image data to temporary new volume node.
+ # Note: Although the original master volume node is already in the scene, we do not use it here,
+ # because the master volume may have been resampled to match segmentation geometry.
+ masterVolumeNode = slicer.vtkMRMLScalarVolumeNode()
+ slicer.mrmlScene.AddNode(masterVolumeNode)
+ masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID())
+ slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode)
+ # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels.
+ mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
+ slicer.mrmlScene.AddNode(mergedLabelmapNode)
+ slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode)
+
+ # Run segmentation algorithm
+ import SimpleITK as sitk
+ import sitkUtils
+ # Read input data from Slicer into SimpleITK
+ labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
+ backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName()))
+ # Run watershed filter
+ featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm")))
+ del backgroundImage
+ f = sitk.MorphologicalWatershedFromMarkersImageFilter()
+ f.SetMarkWatershedLine(False)
+ f.SetFullyConnected(False)
+ labelImage = f.Execute(featureImage, labelImage)
+ del featureImage
+ # Pixel type of watershed output is the same as the input. Convert it to int16 now.
+ if labelImage.GetPixelID() != sitk.sitkInt16:
+ labelImage = sitk.Cast(labelImage, sitk.sitkInt16)
+ # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data.
+ sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
+ mergedLabelmapNode.GetImageData().Modified()
+ mergedLabelmapNode.Modified()
+
+ # Update segmentation from labelmap node and remove temporary nodes
+ slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds)
+ slicer.mrmlScene.RemoveNode(masterVolumeNode)
+ slicer.mrmlScene.RemoveNode(mergedLabelmapNode)
+
+ qt.QApplication.restoreOverrideCursor()
diff --git a/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py b/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py
index b77919432a0..bb5ce9881ec 100644
--- a/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py
+++ b/Libs/MRML/DisplayableManager/Python/mrmlDisplayableManager/vtkScriptedExampleDisplayableManager.py
@@ -50,39 +50,39 @@
class vtkScriptedExampleDisplayableManager:
- def __init__(self, parent):
- self.Parent = parent
- print("vtkScriptedExampleDisplayableManager - __init__")
+ def __init__(self, parent):
+ self.Parent = parent
+ print("vtkScriptedExampleDisplayableManager - __init__")
- def Create(self):
- print("vtkScriptedExampleDisplayableManager - Create")
- pass
+ def Create(self):
+ print("vtkScriptedExampleDisplayableManager - Create")
+ pass
- def GetMRMLSceneEventsToObserve(self):
- print("vtkScriptedExampleDisplayableManager - GetMRMLSceneEventsToObserve")
- sceneEvents = vtkIntArray()
- sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeAddedEvent)
- sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeRemovedEvent)
- return sceneEvents
+ def GetMRMLSceneEventsToObserve(self):
+ print("vtkScriptedExampleDisplayableManager - GetMRMLSceneEventsToObserve")
+ sceneEvents = vtkIntArray()
+ sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeAddedEvent)
+ sceneEvents.InsertNextValue(slicer.vtkMRMLScene.NodeRemovedEvent)
+ return sceneEvents
- def ProcessMRMLSceneEvents(self, scene, eventid, node):
- print("vtkScriptedExampleDisplayableManager - ProcessMRMLSceneEvents(eventid,", eventid, ")")
- pass
+ def ProcessMRMLSceneEvents(self, scene, eventid, node):
+ print("vtkScriptedExampleDisplayableManager - ProcessMRMLSceneEvents(eventid,", eventid, ")")
+ pass
- def ProcessMRMLNodesEvents(self, scene, eventid, callData):
- print("vtkScriptedExampleDisplayableManager - ProcessMRMLNodesEvents(eventid,", eventid, ")")
- pass
+ def ProcessMRMLNodesEvents(self, scene, eventid, callData):
+ print("vtkScriptedExampleDisplayableManager - ProcessMRMLNodesEvents(eventid,", eventid, ")")
+ pass
- def RemoveMRMLObservers(self):
- print("vtkScriptedExampleDisplayableManager - RemoveMRMLObservers")
- pass
+ def RemoveMRMLObservers(self):
+ print("vtkScriptedExampleDisplayableManager - RemoveMRMLObservers")
+ pass
- def UpdateFromMRML(self):
- print("vtkScriptedExampleDisplayableManager - UpdateFromMRML")
- pass
+ def UpdateFromMRML(self):
+ print("vtkScriptedExampleDisplayableManager - UpdateFromMRML")
+ pass
- def OnInteractorStyleEvent(self, eventid):
- print("vtkScriptedExampleDisplayableManager - OnInteractorStyleEvent(eventid,", eventid, ")")
+ def OnInteractorStyleEvent(self, eventid):
+ print("vtkScriptedExampleDisplayableManager - OnInteractorStyleEvent(eventid,", eventid, ")")
- def OnMRMLDisplayableNodeModifiedEvent(self, viewNode):
- print("vtkScriptedExampleDisplayableManager - onMRMLDisplayableNodeModifiedEvent")
+ def OnMRMLDisplayableNodeModifiedEvent(self, viewNode):
+ print("vtkScriptedExampleDisplayableManager - onMRMLDisplayableNodeModifiedEvent")
diff --git a/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py b/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py
index afb77d2d83e..e8336587967 100644
--- a/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py
+++ b/Libs/vtkITK/Testing/vtkITKArchetypeDiffusionTensorReaderFile.py
@@ -62,10 +62,10 @@ def test_pointdata(self):
self.assertTrue(numpy.allclose(self.nrrdArray, self.itkArray))
def runTest(self):
- self.setUp()
- self.test_measurement_frame()
- self.test_pointdata()
- self.test_ras_to_ijk()
+ self.setUp()
+ self.test_measurement_frame()
+ self.test_pointdata()
+ self.test_ras_to_ijk()
def compare_vtk_matrix(m1, m2, n=4):
diff --git a/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py b/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py
index 88fd51cc65f..d2b4e3c9cf2 100644
--- a/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py
+++ b/Libs/vtkITK/Testing/vtkITKArchetypeScalarReaderFile.py
@@ -47,9 +47,9 @@ def test_pointdata(self):
self.assertTrue(numpy.allclose(self.nrrdArray, self.itkArray))
def runTest(self):
- self.setUp()
- self.test_pointdata()
- self.test_ras_to_ijk()
+ self.setUp()
+ self.test_pointdata()
+ self.test_ras_to_ijk()
def compare_vtk_matrix(m1, m2, n=4):
diff --git a/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py b/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py
index bf51fd403d2..4c5fec78a0a 100644
--- a/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py
+++ b/Modules/Loadable/Annotations/SubjectHierarchyPlugins/AnnotationsSubjectHierarchyPlugin.py
@@ -6,122 +6,122 @@
class AnnotationsSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin):
- """ Scripted subject hierarchy plugin for the Annotations module.
-
- This is also an example for scripted plugins, so includes all possible methods.
- The methods that are not needed (i.e. the default implementation in
- qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
- omitted in plugins created based on this one.
-
- The plugin registers itself on creation, but needs to be initialized from the
- module or application as follows:
- from SubjectHierarchyPlugins import AnnotationsSubjectHierarchyPlugin
- scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
- scriptedPlugin.setPythonSource(AnnotationsSubjectHierarchyPlugin.filePath)
- """
-
- # Necessary static member to be able to set python source to scripted subject hierarchy plugin
- filePath = __file__
-
- def __init__(self, scriptedPlugin):
- scriptedPlugin.name = 'Annotations'
- AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
-
- def canAddNodeToSubjectHierarchy(self, node, parentItemID):
- if node is not None:
- if node.IsA("vtkMRMLAnnotationROINode") or node.IsA("vtkMRMLAnnotationRulerNode"):
- return 1.0
- return 0.0
-
- def canOwnSubjectHierarchyItem(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- shNode = pluginHandlerSingleton.subjectHierarchyNode()
- associatedNode = shNode.GetItemDataNode(itemID)
- # ROI or Ruler
- if associatedNode is not None:
- if associatedNode.IsA("vtkMRMLAnnotationROINode") or associatedNode.IsA("vtkMRMLAnnotationRulerNode"):
- return 1.0
- return 0.0
-
- def roleForPlugin(self):
- return "Annotation"
-
- def helpText(self):
- # return (""
- # ""
- # "SegmentEditor module subject hierarchy help text"
- # ""
- # "
"
- # ""
- # ""
- # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
- # ""
- # "
\n")
- return ""
-
- def icon(self, itemID):
- import os
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- shNode = pluginHandlerSingleton.subjectHierarchyNode()
- associatedNode = shNode.GetItemDataNode(itemID)
- if associatedNode is not None:
- # ROI
- if associatedNode.IsA("vtkMRMLAnnotationROINode"):
- roiIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationROI.png')
- if os.path.exists(roiIconPath):
- return qt.QIcon(roiIconPath)
- # Ruler
- if associatedNode.IsA("vtkMRMLAnnotationRulerNode"):
- rulerIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationDistance.png')
- if os.path.exists(rulerIconPath):
- return qt.QIcon(rulerIconPath)
- # Item unknown by plugin
- return qt.QIcon()
-
- def visibilityIcon(self, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
-
- def editProperties(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
-
- def itemContextMenuActions(self):
- return []
-
- def sceneContextMenuActions(self):
- return []
-
- def showContextMenuActionsForItem(self, itemID):
- pass
-
- def viewContextMenuActions(self):
- """ Important note:
- In order to use view menus in scripted plugins, it needs to be registered differently,
- so that the Python API can be fully built by the time this function is called.
-
- The following changes are necessary:
- 1. Remove or comment out the following line from constructor
- AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
- 2. In addition to the initialization where the scripted plugin is instantialized and
- the source set, the plugin also needs to be registered manually:
- pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandler.registerPlugin(scriptedPlugin)
+ """ Scripted subject hierarchy plugin for the Annotations module.
+
+ This is also an example for scripted plugins, so includes all possible methods.
+ The methods that are not needed (i.e. the default implementation in
+ qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
+ omitted in plugins created based on this one.
+
+ The plugin registers itself on creation, but needs to be initialized from the
+ module or application as follows:
+ from SubjectHierarchyPlugins import AnnotationsSubjectHierarchyPlugin
+ scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
+ scriptedPlugin.setPythonSource(AnnotationsSubjectHierarchyPlugin.filePath)
"""
- return []
- def showViewContextMenuActionsForItem(self, itemID, eventData):
- pass
-
- def tooltip(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- tooltip = pluginHandlerSingleton.pluginByName('Default').tooltip(itemID)
- return str(tooltip)
-
- def setDisplayVisibility(self, itemID, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
-
- def getDisplayVisibility(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
+ # Necessary static member to be able to set python source to scripted subject hierarchy plugin
+ filePath = __file__
+
+ def __init__(self, scriptedPlugin):
+ scriptedPlugin.name = 'Annotations'
+ AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
+
+ def canAddNodeToSubjectHierarchy(self, node, parentItemID):
+ if node is not None:
+ if node.IsA("vtkMRMLAnnotationROINode") or node.IsA("vtkMRMLAnnotationRulerNode"):
+ return 1.0
+ return 0.0
+
+ def canOwnSubjectHierarchyItem(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ shNode = pluginHandlerSingleton.subjectHierarchyNode()
+ associatedNode = shNode.GetItemDataNode(itemID)
+ # ROI or Ruler
+ if associatedNode is not None:
+ if associatedNode.IsA("vtkMRMLAnnotationROINode") or associatedNode.IsA("vtkMRMLAnnotationRulerNode"):
+ return 1.0
+ return 0.0
+
+ def roleForPlugin(self):
+ return "Annotation"
+
+ def helpText(self):
+ # return (""
+ # ""
+ # "SegmentEditor module subject hierarchy help text"
+ # ""
+ # "
"
+ # ""
+ # ""
+ # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
+ # ""
+ # "
\n")
+ return ""
+
+ def icon(self, itemID):
+ import os
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ shNode = pluginHandlerSingleton.subjectHierarchyNode()
+ associatedNode = shNode.GetItemDataNode(itemID)
+ if associatedNode is not None:
+ # ROI
+ if associatedNode.IsA("vtkMRMLAnnotationROINode"):
+ roiIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationROI.png')
+ if os.path.exists(roiIconPath):
+ return qt.QIcon(roiIconPath)
+ # Ruler
+ if associatedNode.IsA("vtkMRMLAnnotationRulerNode"):
+ rulerIconPath = os.path.join(os.path.dirname(__file__), '../Resources/Icons/AnnotationDistance.png')
+ if os.path.exists(rulerIconPath):
+ return qt.QIcon(rulerIconPath)
+ # Item unknown by plugin
+ return qt.QIcon()
+
+ def visibilityIcon(self, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
+
+ def editProperties(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
+
+ def itemContextMenuActions(self):
+ return []
+
+ def sceneContextMenuActions(self):
+ return []
+
+ def showContextMenuActionsForItem(self, itemID):
+ pass
+
+ def viewContextMenuActions(self):
+ """ Important note:
+ In order to use view menus in scripted plugins, it needs to be registered differently,
+ so that the Python API can be fully built by the time this function is called.
+
+ The following changes are necessary:
+ 1. Remove or comment out the following line from constructor
+ AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
+ 2. In addition to the initialization where the scripted plugin is instantialized and
+ the source set, the plugin also needs to be registered manually:
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandler.registerPlugin(scriptedPlugin)
+ """
+ return []
+
+ def showViewContextMenuActionsForItem(self, itemID, eventData):
+ pass
+
+ def tooltip(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ tooltip = pluginHandlerSingleton.pluginByName('Default').tooltip(itemID)
+ return str(tooltip)
+
+ def setDisplayVisibility(self, itemID, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
+
+ def getDisplayVisibility(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py
index ab16863977c..8a484eb0bb2 100644
--- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py
+++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyFiducials.py
@@ -4,48 +4,48 @@
def TestFiducialAdd(renameFlag=1, visibilityFlag=1, numToAdd=20):
- print("numToAdd = ", numToAdd)
- if renameFlag > 0:
- print("Index\tTime to add fid\tDelta between adds\tTime to rename fid\tDelta between renames")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
- else:
- print("Index\tTime to add fid\tDelta between adds")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt")
- r = 0
- a = 0
- s = 0
- t1 = 0
- t2 = 0
- t3 = 0
- t4 = 0
- timeToAddThisFid = 0
- timeToAddLastFid = 0
- timeToRenameThisFid = 0
- timeToRenameLastFid = 0
- # iterate over the number of fiducials to add
- for i in range(numToAdd):
-# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
- fidNode = slicer.vtkMRMLAnnotationFiducialNode()
- fidNode.SetFiducialCoordinates(r, a, s)
- t1 = time.process_time()
- fidNode.Initialize(slicer.mrmlScene)
- t2 = time.process_time()
- timeToAddThisFid = t2 - t1
- dt = timeToAddThisFid - timeToAddLastFid
+ print("numToAdd = ", numToAdd)
if renameFlag > 0:
- t3 = time.process_time()
- fidNode.SetName(str(i))
- t4 = time.process_time()
- timeToRenameThisFid = t4 - t3
- dt2 = timeToRenameThisFid - timeToRenameLastFid
- print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt, "\t", timeToRenameThisFid, "\t", dt2)
- timeToRenameLastFid = timeToRenameThisFid
+ print("Index\tTime to add fid\tDelta between adds\tTime to rename fid\tDelta between renames")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
else:
- print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt)
- r = r + 1.0
- a = a + 1.0
- s = s + 1.0
- timeToAddLastFid = timeToAddThisFid
+ print("Index\tTime to add fid\tDelta between adds")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt")
+ r = 0
+ a = 0
+ s = 0
+ t1 = 0
+ t2 = 0
+ t3 = 0
+ t4 = 0
+ timeToAddThisFid = 0
+ timeToAddLastFid = 0
+ timeToRenameThisFid = 0
+ timeToRenameLastFid = 0
+ # iterate over the number of fiducials to add
+ for i in range(numToAdd):
+ # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
+ fidNode = slicer.vtkMRMLAnnotationFiducialNode()
+ fidNode.SetFiducialCoordinates(r, a, s)
+ t1 = time.process_time()
+ fidNode.Initialize(slicer.mrmlScene)
+ t2 = time.process_time()
+ timeToAddThisFid = t2 - t1
+ dt = timeToAddThisFid - timeToAddLastFid
+ if renameFlag > 0:
+ t3 = time.process_time()
+ fidNode.SetName(str(i))
+ t4 = time.process_time()
+ timeToRenameThisFid = t4 - t3
+ dt2 = timeToRenameThisFid - timeToRenameLastFid
+ print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt, "\t", timeToRenameThisFid, "\t", dt2)
+ timeToRenameLastFid = timeToRenameThisFid
+ else:
+ print('%(index)04d\t' % {'index': i}, timeToAddThisFid, "\t", dt)
+ r = r + 1.0
+ a = a + 1.0
+ s = s + 1.0
+ timeToAddLastFid = timeToAddThisFid
testStartTime = time.process_time()
diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py
index f953e582a69..eca9bc91a86 100644
--- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py
+++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyROIs.py
@@ -2,55 +2,55 @@
def TestROIAdd(renameFlag=1, visibilityFlag=1, numToAdd=20):
- print("numToAdd = ", numToAdd)
- if renameFlag > 0:
- print("Index\tTime to add roi\tDelta between adds\tTime to rename roi\tDelta between renames")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
- else:
- print("Index\tTime to add roi\tDelta between adds")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt")
- cx = 0
- cy = 0
- cz = 0
- rx = 1
- ry = 1
- rz = 1
- t1 = 0
- t2 = 0
- t3 = 0
- t4 = 0
- timeToAddThisROI = 0
- timeToAddLastROI = 0
- timeToRenameThisROI = 0
- timeToRenameLastROI = 0
- # iterate over the number of rois to add
- for i in range(numToAdd):
-# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
- roiNode = slicer.vtkMRMLAnnotationROINode()
- roiNode.SetXYZ(cx, cy, cz)
- roiNode.SetRadiusXYZ(rx, ry, rz)
- t1 = time.process_time()
- roiNode.Initialize(slicer.mrmlScene)
- t2 = time.process_time()
- timeToAddThisROI = t2 - t1
- dt = timeToAddThisROI - timeToAddLastROI
+ print("numToAdd = ", numToAdd)
if renameFlag > 0:
- t3 = time.process_time()
- roiNode.SetName(str(i))
- t4 = time.process_time()
- timeToRenameThisROI = t4 - t3
- dt2 = timeToRenameThisROI - timeToRenameLastROI
- print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt, "\t", timeToRenameThisROI, "\t", dt2)
- timeToRenameLastROI = timeToRenameThisROI
+ print("Index\tTime to add roi\tDelta between adds\tTime to rename roi\tDelta between renames")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
else:
- print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt)
- rx = rx + 0.5
- ry = ry + 0.5
- rz = rz + 0.5
- cx = cx + 2.0
- cy = cy + 2.0
- cz = cz + 2.0
- timeToAddLastROI = timeToAddThisROI
+ print("Index\tTime to add roi\tDelta between adds")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt")
+ cx = 0
+ cy = 0
+ cz = 0
+ rx = 1
+ ry = 1
+ rz = 1
+ t1 = 0
+ t2 = 0
+ t3 = 0
+ t4 = 0
+ timeToAddThisROI = 0
+ timeToAddLastROI = 0
+ timeToRenameThisROI = 0
+ timeToRenameLastROI = 0
+ # iterate over the number of rois to add
+ for i in range(numToAdd):
+ # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
+ roiNode = slicer.vtkMRMLAnnotationROINode()
+ roiNode.SetXYZ(cx, cy, cz)
+ roiNode.SetRadiusXYZ(rx, ry, rz)
+ t1 = time.process_time()
+ roiNode.Initialize(slicer.mrmlScene)
+ t2 = time.process_time()
+ timeToAddThisROI = t2 - t1
+ dt = timeToAddThisROI - timeToAddLastROI
+ if renameFlag > 0:
+ t3 = time.process_time()
+ roiNode.SetName(str(i))
+ t4 = time.process_time()
+ timeToRenameThisROI = t4 - t3
+ dt2 = timeToRenameThisROI - timeToRenameLastROI
+ print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt, "\t", timeToRenameThisROI, "\t", dt2)
+ timeToRenameLastROI = timeToRenameThisROI
+ else:
+ print('%(index)04d\t' % {'index': i}, timeToAddThisROI, "\t", dt)
+ rx = rx + 0.5
+ ry = ry + 0.5
+ rz = rz + 0.5
+ cx = cx + 2.0
+ cy = cy + 2.0
+ cz = cz + 2.0
+ timeToAddLastROI = timeToAddThisROI
testStartTime = time.process_time()
diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py
index bc8a8575918..ae40f755656 100644
--- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py
+++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingAddManyRulers.py
@@ -4,55 +4,55 @@
def TestRulerAdd(renameFlag=1, visibilityFlag=1, numToAdd=20):
- print("numToAdd = ", numToAdd)
- if renameFlag > 0:
- print("Index\tTime to add ruler\tDelta between adds\tTime to rename ruler\tDelta between renames")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
- else:
- print("Index\tTime to add ruler\tDelta between adds")
- print("%(index)04s\t" % {'index': "i"}, "t\tdt")
- r1 = 0
- a1 = 0
- s1 = 0
- r2 = 1
- a2 = 1
- s2 = 1
- t1 = 0
- t2 = 0
- t3 = 0
- t4 = 0
- timeToAddThisRuler = 0
- timeToAddLastRuler = 0
- timeToRenameThisRuler = 0
- timeToRenameLastRuler = 0
- # iterate over the number of rulers to add
- for i in range(numToAdd):
-# print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
- rulerNode = slicer.vtkMRMLAnnotationRulerNode()
- rulerNode.SetPosition1(r1, a1, s1)
- rulerNode.SetPosition2(r2, a2, s2)
- t1 = time.process_time()
- rulerNode.Initialize(slicer.mrmlScene)
- t2 = time.process_time()
- timeToAddThisRuler = t2 - t1
- dt = timeToAddThisRuler - timeToAddLastRuler
+ print("numToAdd = ", numToAdd)
if renameFlag > 0:
- t3 = time.process_time()
- rulerNode.SetName(str(i))
- t4 = time.process_time()
- timeToRenameThisRuler = t4 - t3
- dt2 = timeToRenameThisRuler - timeToRenameLastRuler
- print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt, "\t", timeToRenameThisRuler, "\t", dt2)
- timeToRenameLastRuler = timeToRenameThisRuler
+ print("Index\tTime to add ruler\tDelta between adds\tTime to rename ruler\tDelta between renames")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt\tt\tdt")
else:
- print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt)
- r1 = r1 + 1.0
- a1 = a1 + 1.0
- s1 = s1 + 1.0
- r2 = r2 + 1.5
- a2 = a2 + 1.5
- s2 = s2 + 1.5
- timeToAddLastRuler = timeToAddThisRuler
+ print("Index\tTime to add ruler\tDelta between adds")
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt")
+ r1 = 0
+ a1 = 0
+ s1 = 0
+ r2 = 1
+ a2 = 1
+ s2 = 1
+ t1 = 0
+ t2 = 0
+ t3 = 0
+ t4 = 0
+ timeToAddThisRuler = 0
+ timeToAddLastRuler = 0
+ timeToRenameThisRuler = 0
+ timeToRenameLastRuler = 0
+ # iterate over the number of rulers to add
+ for i in range(numToAdd):
+ # print "i = ", i, "/", numToAdd, ", r = ", r, ", a = ", a, ", s = ", s
+ rulerNode = slicer.vtkMRMLAnnotationRulerNode()
+ rulerNode.SetPosition1(r1, a1, s1)
+ rulerNode.SetPosition2(r2, a2, s2)
+ t1 = time.process_time()
+ rulerNode.Initialize(slicer.mrmlScene)
+ t2 = time.process_time()
+ timeToAddThisRuler = t2 - t1
+ dt = timeToAddThisRuler - timeToAddLastRuler
+ if renameFlag > 0:
+ t3 = time.process_time()
+ rulerNode.SetName(str(i))
+ t4 = time.process_time()
+ timeToRenameThisRuler = t4 - t3
+ dt2 = timeToRenameThisRuler - timeToRenameLastRuler
+ print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt, "\t", timeToRenameThisRuler, "\t", dt2)
+ timeToRenameLastRuler = timeToRenameThisRuler
+ else:
+ print('%(index)04d\t' % {'index': i}, timeToAddThisRuler, "\t", dt)
+ r1 = r1 + 1.0
+ a1 = a1 + 1.0
+ s1 = s1 + 1.0
+ r2 = r2 + 1.5
+ a2 = a2 + 1.5
+ s2 = s2 + 1.5
+ timeToAddLastRuler = timeToAddThisRuler
testStartTime = time.process_time()
diff --git a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py
index c1d4378bd87..41c0ca3f29d 100644
--- a/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py
+++ b/Modules/Loadable/Annotations/Testing/Python/AnnotationsTestingFiducialWithSceneViewRestore.py
@@ -41,5 +41,5 @@
diffTotal = xdiff + ydiff + zdiff
if diffTotal > 0.1:
- exceptionMessage = "Difference between coordinate values total = " + str(diffTotal)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Difference between coordinate values total = " + str(diffTotal)
+ raise Exception(exceptionMessage)
diff --git a/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py b/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py
index 0cd8783f8f0..205f4b1abd0 100644
--- a/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py
+++ b/Modules/Loadable/Annotations/Testing/Python/LoadAnnotationRulerScene.py
@@ -7,10 +7,10 @@
# try get the path of the ruler scene file from the arguments
numArgs = len(sys.argv)
if numArgs > 1:
- scenePath = sys.argv[1]
+ scenePath = sys.argv[1]
else:
- # set the url as best guess from SLICER_HOME
- scenePath = os.path.join(os.environ['SLICER_HOME'], "../../Slicer4/Modules/Loadable/Annotations/Testing/Data/Input/ruler.mrml")
+ # set the url as best guess from SLICER_HOME
+ scenePath = os.path.join(os.environ['SLICER_HOME'], "../../Slicer4/Modules/Loadable/Annotations/Testing/Data/Input/ruler.mrml")
scenePath = os.path.normpath(scenePath)
print("Trying to load ruler mrml file", scenePath)
diff --git a/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py b/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py
index 7dcb33ced03..9fe0d5c1c88 100644
--- a/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py
+++ b/Modules/Loadable/Annotations/Testing/Python/RemoveAnnotationsIDFromSelectionNode.py
@@ -5,32 +5,32 @@
selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
if selectionNode:
- print(selectionNode)
- annotClassName = "vtkMRMLAnnotationRulerNode"
- startIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
- print("Removing ", annotClassName)
- selectionNode.RemovePlaceNodeClassNameFromList(annotClassName)
- endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
- print(selectionNode)
- print("Start index for ", annotClassName, " = ", startIndex, ", end index after removing it = ", endIndex)
- if endIndex != -1:
- raise Exception(f"Failed to remove annotation {annotClassName} from list, end index = {endIndex} should be -1")
+ print(selectionNode)
+ annotClassName = "vtkMRMLAnnotationRulerNode"
+ startIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
+ print("Removing ", annotClassName)
+ selectionNode.RemovePlaceNodeClassNameFromList(annotClassName)
+ endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
+ print(selectionNode)
+ print("Start index for ", annotClassName, " = ", startIndex, ", end index after removing it = ", endIndex)
+ if endIndex != -1:
+ raise Exception(f"Failed to remove annotation {annotClassName} from list, end index = {endIndex} should be -1")
- # now make one active and remove it
- annotClassName = "vtkMRMLAnnotationFiducialNode"
- selectionNode.SetActivePlaceNodeClassName(annotClassName)
- interactionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton")
- interactionNode.SwitchToSinglePlaceMode()
- print("Removing", annotClassName)
- selectionNode.RemovePlaceNodeClassNameFromList(annotClassName)
- endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
- if endIndex != -1:
- raise Exception(f"Failed to remove active annotation {annotClassName} from list, end index = {endIndex} should be -1")
+ # now make one active and remove it
+ annotClassName = "vtkMRMLAnnotationFiducialNode"
+ selectionNode.SetActivePlaceNodeClassName(annotClassName)
+ interactionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton")
+ interactionNode.SwitchToSinglePlaceMode()
+ print("Removing", annotClassName)
+ selectionNode.RemovePlaceNodeClassNameFromList(annotClassName)
+ endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
+ if endIndex != -1:
+ raise Exception(f"Failed to remove active annotation {annotClassName} from list, end index = {endIndex} should be -1")
- # re-add the ruler one
- annotClassName = "vtkMRMLAnnotationRulerNode"
- print("Adding back the ruler node")
- selectionNode.AddNewPlaceNodeClassNameToList("vtkMRMLAnnotationRulerNode", ":/Icons/AnnotationDistanceWithArrow.png")
- endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
- if endIndex == -1:
- raise Exception(f"Failed to re-add {annotClassName}, end index = {endIndex}")
+ # re-add the ruler one
+ annotClassName = "vtkMRMLAnnotationRulerNode"
+ print("Adding back the ruler node")
+ selectionNode.AddNewPlaceNodeClassNameToList("vtkMRMLAnnotationRulerNode", ":/Icons/AnnotationDistanceWithArrow.png")
+ endIndex = selectionNode.PlaceNodeClassNameInList(annotClassName)
+ if endIndex == -1:
+ raise Exception(f"Failed to re-add {annotClassName}, end index = {endIndex}")
diff --git a/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py b/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py
index e803e1bcc33..4bd54e65ad1 100644
--- a/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py
+++ b/Modules/Loadable/Colors/Testing/Python/ColorLegendSelfTest.py
@@ -10,18 +10,18 @@
class ColorLegendSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "ColorLegendSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = []
- self.parent.contributors = ["Kevin Wang (PMH), Nicole Aucoin (BWH), Mikhail Polkovnikov (IHEP)"]
- self.parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "ColorLegendSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Kevin Wang (PMH), Nicole Aucoin (BWH), Mikhail Polkovnikov (IHEP)"]
+ self.parent.helpText = """
This is a test case for the new vtkSlicerScalarBarActor class.
It iterates through all the color nodes and sets them active in the
Colors module while the color legend widget is displayed.
"""
- self.parent.acknowledgementText = """
+ self.parent.acknowledgementText = """
This file was originally developed by Kevin Wang, PMH and was funded by CCO and OCAIRO.
""" # replace with organization, grant and thanks.
@@ -32,140 +32,140 @@ def __init__(self, parent):
class ColorLegendSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- test = ColorLegendSelfTestTest()
- print("Run the test algorithm")
- test.test_ColorLegendSelfTest1()
+ def onApplyButton(self):
+ test = ColorLegendSelfTestTest()
+ print("Run the test algorithm")
+ test.test_ColorLegendSelfTest1()
class ColorLegendSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
- # Timeout delay
- self.delayMs = 700
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_ColorLegendSelfTest1()
-
- def test_ColorLegendSelfTest1(self):
-
- self.delayDisplay("Starting test_ColorLegendSelfTest1")
-
- self.delayDisplay('Load CTChest sample volume')
- import SampleData
- sampleDataLogic = SampleData.SampleDataLogic()
- ctVolumeNode = sampleDataLogic.downloadCTChest()
- self.assertIsNotNone(ctVolumeNode)
-
- self.delayDisplay('Switch to Colors module')
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('Colors')
-
- # Get widgets for testing via GUI
- colorWidget = slicer.modules.colors.widgetRepresentation()
- activeColorNodeSelector = slicer.util.findChildren(colorWidget, 'ColorTableComboBox')[0]
- self.assertIsNotNone(activeColorNodeSelector)
- activeDisplayableNodeSelector = slicer.util.findChildren(colorWidget, 'DisplayableNodeComboBox')[0]
- self.assertIsNotNone(activeDisplayableNodeSelector)
- createColorLegendButton = slicer.util.findChildren(colorWidget, 'CreateColorLegendButton')[0]
- self.assertIsNotNone(createColorLegendButton)
- useCurrentColorsButton = slicer.util.findChildren(colorWidget, 'UseCurrentColorsButton')[0]
- self.assertIsNotNone(useCurrentColorsButton)
- colorLegendDisplayNodeWidget = slicer.util.findChildren(colorWidget, 'ColorLegendDisplayNodeWidget')[0]
- self.assertIsNotNone(colorLegendDisplayNodeWidget)
- colorLegendVisibilityCheckBox = slicer.util.findChildren(colorLegendDisplayNodeWidget, 'ColorLegendVisibilityCheckBox')[0]
- self.assertIsNotNone(colorLegendVisibilityCheckBox)
-
- self.delayDisplay('Show color legend on all views and slices', self.delayMs)
- activeDisplayableNodeSelector.setCurrentNode(ctVolumeNode)
- createColorLegendButton.click()
- self.assertTrue(colorLegendVisibilityCheckBox.checked)
-
- self.delayDisplay('Iterate over the color nodes and set each one active', self.delayMs)
- shortDelayMs = 5
- # There are many color nodes, we don't test each to make the test complete faster
- testedColorNodeIndices = list(range(0, 60, 3))
- for ind, n in enumerate(testedColorNodeIndices):
- colorNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLColorNode')
- self.delayDisplay(f"Setting color node {colorNode.GetName()} ({ind}/{len(testedColorNodeIndices)}) for the displayable node", shortDelayMs)
- activeColorNodeSelector.setCurrentNodeID(colorNode.GetID())
- # use the delay display here to ensure a render
- useCurrentColorsButton.click()
-
- self.delayDisplay('Test color legend visibility', self.delayMs)
- colorLegend = slicer.modules.colors.logic().GetColorLegendDisplayNode(ctVolumeNode)
- self.assertIsNotNone(colorLegend)
-
- self.delayDisplay('Exercise color legend updates via MRML', self.delayMs)
- # signal to displayable manager to show a created color legend
- colorLegend.SetMaxNumberOfColors(256)
- colorLegend.SetVisibility(True)
-
- self.delayDisplay('Show color legend in Red slice and 3D views only', self.delayMs)
- sliceNodeRed = slicer.app.layoutManager().sliceWidget('Red').mrmlSliceNode()
- self.assertIsNotNone(sliceNodeRed)
- threeDViewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode()
- self.assertIsNotNone(threeDViewNode)
- colorLegend.SetViewNodeIDs([sliceNodeRed.GetID(), threeDViewNode.GetID()])
-
- self.delayDisplay('Show color legend in the 3D view only', self.delayMs)
- colorLegend.SetViewNodeIDs([threeDViewNode.GetID()])
- self.delayDisplay('Test color legend on 3D view finished!', self.delayMs)
-
- # Test showing color legend only in a single slice node
- sliceNameColor = {
- 'Red': [1., 0., 0.],
- 'Green': [0., 1., 0.],
- 'Yellow': [1., 1., 0.]
- }
- for sliceName, titleColor in sliceNameColor.items():
- self.delayDisplay('Test color legend on the ' + sliceName + ' slice view', self.delayMs)
- sliceNode = slicer.app.layoutManager().sliceWidget(sliceName).mrmlSliceNode()
- colorLegend.SetViewNodeIDs([sliceNode.GetID()])
- colorLegend.SetTitleText(sliceName)
- colorLegend.GetTitleTextProperty().SetColor(titleColor)
- self.delayDisplay('Test color legend on the ' + sliceName + ' slice view finished!', self.delayMs * 2)
-
- colorLegend.SetVisibility(False)
-
- self.delayDisplay('Test passed!')
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+ # Timeout delay
+ self.delayMs = 700
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_ColorLegendSelfTest1()
+
+ def test_ColorLegendSelfTest1(self):
+
+ self.delayDisplay("Starting test_ColorLegendSelfTest1")
+
+ self.delayDisplay('Load CTChest sample volume')
+ import SampleData
+ sampleDataLogic = SampleData.SampleDataLogic()
+ ctVolumeNode = sampleDataLogic.downloadCTChest()
+ self.assertIsNotNone(ctVolumeNode)
+
+ self.delayDisplay('Switch to Colors module')
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('Colors')
+
+ # Get widgets for testing via GUI
+ colorWidget = slicer.modules.colors.widgetRepresentation()
+ activeColorNodeSelector = slicer.util.findChildren(colorWidget, 'ColorTableComboBox')[0]
+ self.assertIsNotNone(activeColorNodeSelector)
+ activeDisplayableNodeSelector = slicer.util.findChildren(colorWidget, 'DisplayableNodeComboBox')[0]
+ self.assertIsNotNone(activeDisplayableNodeSelector)
+ createColorLegendButton = slicer.util.findChildren(colorWidget, 'CreateColorLegendButton')[0]
+ self.assertIsNotNone(createColorLegendButton)
+ useCurrentColorsButton = slicer.util.findChildren(colorWidget, 'UseCurrentColorsButton')[0]
+ self.assertIsNotNone(useCurrentColorsButton)
+ colorLegendDisplayNodeWidget = slicer.util.findChildren(colorWidget, 'ColorLegendDisplayNodeWidget')[0]
+ self.assertIsNotNone(colorLegendDisplayNodeWidget)
+ colorLegendVisibilityCheckBox = slicer.util.findChildren(colorLegendDisplayNodeWidget, 'ColorLegendVisibilityCheckBox')[0]
+ self.assertIsNotNone(colorLegendVisibilityCheckBox)
+
+ self.delayDisplay('Show color legend on all views and slices', self.delayMs)
+ activeDisplayableNodeSelector.setCurrentNode(ctVolumeNode)
+ createColorLegendButton.click()
+ self.assertTrue(colorLegendVisibilityCheckBox.checked)
+
+ self.delayDisplay('Iterate over the color nodes and set each one active', self.delayMs)
+ shortDelayMs = 5
+ # There are many color nodes, we don't test each to make the test complete faster
+ testedColorNodeIndices = list(range(0, 60, 3))
+ for ind, n in enumerate(testedColorNodeIndices):
+ colorNode = slicer.mrmlScene.GetNthNodeByClass(n, 'vtkMRMLColorNode')
+ self.delayDisplay(f"Setting color node {colorNode.GetName()} ({ind}/{len(testedColorNodeIndices)}) for the displayable node", shortDelayMs)
+ activeColorNodeSelector.setCurrentNodeID(colorNode.GetID())
+ # use the delay display here to ensure a render
+ useCurrentColorsButton.click()
+
+ self.delayDisplay('Test color legend visibility', self.delayMs)
+ colorLegend = slicer.modules.colors.logic().GetColorLegendDisplayNode(ctVolumeNode)
+ self.assertIsNotNone(colorLegend)
+
+ self.delayDisplay('Exercise color legend updates via MRML', self.delayMs)
+ # signal to displayable manager to show a created color legend
+ colorLegend.SetMaxNumberOfColors(256)
+ colorLegend.SetVisibility(True)
+
+ self.delayDisplay('Show color legend in Red slice and 3D views only', self.delayMs)
+ sliceNodeRed = slicer.app.layoutManager().sliceWidget('Red').mrmlSliceNode()
+ self.assertIsNotNone(sliceNodeRed)
+ threeDViewNode = slicer.app.layoutManager().threeDWidget(0).mrmlViewNode()
+ self.assertIsNotNone(threeDViewNode)
+ colorLegend.SetViewNodeIDs([sliceNodeRed.GetID(), threeDViewNode.GetID()])
+
+ self.delayDisplay('Show color legend in the 3D view only', self.delayMs)
+ colorLegend.SetViewNodeIDs([threeDViewNode.GetID()])
+ self.delayDisplay('Test color legend on 3D view finished!', self.delayMs)
+
+ # Test showing color legend only in a single slice node
+ sliceNameColor = {
+ 'Red': [1., 0., 0.],
+ 'Green': [0., 1., 0.],
+ 'Yellow': [1., 1., 0.]
+ }
+ for sliceName, titleColor in sliceNameColor.items():
+ self.delayDisplay('Test color legend on the ' + sliceName + ' slice view', self.delayMs)
+ sliceNode = slicer.app.layoutManager().sliceWidget(sliceName).mrmlSliceNode()
+ colorLegend.SetViewNodeIDs([sliceNode.GetID()])
+ colorLegend.SetTitleText(sliceName)
+ colorLegend.GetTitleTextProperty().SetColor(titleColor)
+ self.delayDisplay('Test color legend on the ' + sliceName + ' slice view finished!', self.delayMs * 2)
+
+ colorLegend.SetVisibility(False)
+
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py b/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py
index 0a3e971bb18..3b771a62e88 100644
--- a/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py
+++ b/Modules/Loadable/Colors/Testing/Python/CustomColorTableSceneViewRestoreTestBug3992.py
@@ -12,7 +12,7 @@
colorNode.NamesInitialisedOff()
colorNode.SetNumberOfColors(3)
if colorNode.GetLookupTable() is not None:
- colorNode.GetLookupTable().SetTableRange(0, 2)
+ colorNode.GetLookupTable().SetTableRange(0, 2)
colorNode.SetColor(0, 'zero', 0.0, 0.0, 0.0, 0.0)
colorNode.SetColor(1, 'one', 1.0, 1.0, 1.0, 1.0)
@@ -52,7 +52,7 @@
# make sure it writes the color table
writeFlag = colorStorageNode.WriteData(colorNode)
if writeFlag == 0:
- print("Error writing out file ", colorStorageNode.GetFileName())
+ print("Error writing out file ", colorStorageNode.GetFileName())
# clear out the scene and re-read from disk
@@ -76,14 +76,14 @@
# mrmlScene.GetNodeByID("vtkMRMLColorTableNode1")
if colorNodeAfterRestore is None:
- exceptionMessage = "Unable to find vtkMRMLColorTableNode1 in scene after restore"
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unable to find vtkMRMLColorTableNode1 in scene after restore"
+ raise Exception(exceptionMessage)
numColors = colorNodeAfterRestore.GetNumberOfColors()
if numColors != 3:
- exceptionMessage = "Color node doesn't have 3 colors, instead has " + str(numColors)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Color node doesn't have 3 colors, instead has " + str(numColors)
+ raise Exception(exceptionMessage)
afterRestoreSceneCol2 = [0., 0., 0., 0.0]
colorNodeAfterRestore.GetColor(2, afterRestoreSceneCol2)
@@ -104,5 +104,5 @@
print("Difference between colors after restored the scene and value from when it was read in from disk:\n\t", rdiff, gdiff, bdiff, adiff, "\n\tsummed absolute diff = ", diffTotal)
if diffTotal > 0.1:
- exceptionMessage = "Difference between color values total = " + str(diffTotal)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Difference between color values total = " + str(diffTotal)
+ raise Exception(exceptionMessage)
diff --git a/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py b/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py
index f10798f66bc..86e140c6983 100644
--- a/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py
+++ b/Modules/Loadable/CropVolume/Testing/Python/CropVolumeSelfTest.py
@@ -7,16 +7,16 @@
class CropVolumeSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "CropVolumeSelfTest" # TODO make this more human readable by adding spaces
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = []
- parent.contributors = ["Andrey Fedorov (BWH)"] # replace with "Firstname Lastname (Org)"
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "CropVolumeSelfTest" # TODO make this more human readable by adding spaces
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = []
+ parent.contributors = ["Andrey Fedorov (BWH)"] # replace with "Firstname Lastname (Org)"
+ parent.helpText = """
This module was developed as a self test to perform the operations needed for crop volume.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
""" # replace with organization, grant and thanks.
@@ -25,69 +25,69 @@ def __init__(self, parent):
#
class CropVolumeSelfTestWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+ # Instantiate and connect widgets ...
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
class CropVolumeSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
+ """
+ This is the test case for your scripted module.
+ """
- def setUp(self):
- slicer.mrmlScene.Clear(0)
+ def setUp(self):
+ slicer.mrmlScene.Clear(0)
- def runTest(self):
- self.setUp()
- self.test_CropVolumeSelfTest()
+ def runTest(self):
+ self.setUp()
+ self.test_CropVolumeSelfTest()
- def test_CropVolumeSelfTest(self):
- """
- Replicate the crashe in issue 3117
- """
+ def test_CropVolumeSelfTest(self):
+ """
+ Replicate the crashe in issue 3117
+ """
- print("Running CropVolumeSelfTest Test case:")
+ print("Running CropVolumeSelfTest Test case:")
- import SampleData
+ import SampleData
- vol = SampleData.downloadSample("MRHead")
- roi = slicer.vtkMRMLMarkupsROINode()
+ vol = SampleData.downloadSample("MRHead")
+ roi = slicer.vtkMRMLMarkupsROINode()
- mainWindow = slicer.util.mainWindow()
- mainWindow.moduleSelector().selectModule('CropVolume')
+ mainWindow = slicer.util.mainWindow()
+ mainWindow.moduleSelector().selectModule('CropVolume')
- cropVolumeNode = slicer.vtkMRMLCropVolumeParametersNode()
- cropVolumeNode.SetScene(slicer.mrmlScene)
- cropVolumeNode.SetName('ChangeTracker_CropVolume_node')
- cropVolumeNode.SetIsotropicResampling(True)
- cropVolumeNode.SetSpacingScalingConst(0.5)
- slicer.mrmlScene.AddNode(cropVolumeNode)
+ cropVolumeNode = slicer.vtkMRMLCropVolumeParametersNode()
+ cropVolumeNode.SetScene(slicer.mrmlScene)
+ cropVolumeNode.SetName('ChangeTracker_CropVolume_node')
+ cropVolumeNode.SetIsotropicResampling(True)
+ cropVolumeNode.SetSpacingScalingConst(0.5)
+ slicer.mrmlScene.AddNode(cropVolumeNode)
- cropVolumeNode.SetInputVolumeNodeID(vol.GetID())
- cropVolumeNode.SetROINodeID(roi.GetID())
+ cropVolumeNode.SetInputVolumeNodeID(vol.GetID())
+ cropVolumeNode.SetROINodeID(roi.GetID())
- cropVolumeLogic = slicer.modules.cropvolume.logic()
- cropVolumeLogic.Apply(cropVolumeNode)
+ cropVolumeLogic = slicer.modules.cropvolume.logic()
+ cropVolumeLogic.Apply(cropVolumeNode)
- self.delayDisplay('First test passed, closing the scene and running again')
- # test clearing the scene and running a second time
- slicer.mrmlScene.Clear(0)
- # the module will re-add the removed parameters node
- mainWindow.moduleSelector().selectModule('Transforms')
- mainWindow.moduleSelector().selectModule('CropVolume')
- cropVolumeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLCropVolumeParametersNode1')
- vol = SampleData.downloadSample("MRHead")
- roi = slicer.vtkMRMLMarkupsROINode()
- cropVolumeNode.SetInputVolumeNodeID(vol.GetID())
- cropVolumeNode.SetROINodeID(roi.GetID())
- cropVolumeLogic.Apply(cropVolumeNode)
+ self.delayDisplay('First test passed, closing the scene and running again')
+ # test clearing the scene and running a second time
+ slicer.mrmlScene.Clear(0)
+ # the module will re-add the removed parameters node
+ mainWindow.moduleSelector().selectModule('Transforms')
+ mainWindow.moduleSelector().selectModule('CropVolume')
+ cropVolumeNode = slicer.mrmlScene.GetNodeByID('vtkMRMLCropVolumeParametersNode1')
+ vol = SampleData.downloadSample("MRHead")
+ roi = slicer.vtkMRMLMarkupsROINode()
+ cropVolumeNode.SetInputVolumeNodeID(vol.GetID())
+ cropVolumeNode.SetROINodeID(roi.GetID())
+ cropVolumeLogic.Apply(cropVolumeNode)
- self.delayDisplay('Test passed')
+ self.delayDisplay('Test passed')
diff --git a/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py b/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py
index ebb57f1d1af..9b4241428e6 100644
--- a/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/AddManyMarkupsFiducialTest.py
@@ -13,16 +13,16 @@
#
class AddManyMarkupsFiducialTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "AddManyMarkupsFiducialTest"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = []
- parent.contributors = ["Nicole Aucoin (BWH)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "AddManyMarkupsFiducialTest"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = []
+ parent.contributors = ["Nicole Aucoin (BWH)"]
+ parent.helpText = """
This is a test case that adds many Markup Fiducials to the scene and times the operation.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1.
""" # replace with organization, grant and thanks.
@@ -32,111 +32,111 @@ def __init__(self, parent):
#
class AddManyMarkupsFiducialTestWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
-
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
-
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- #
- # node type to add
- #
- self.nodeTypeComboBox = qt.QComboBox()
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsFiducialNode")
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsLineNode")
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsAngleNode")
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsCurveNode")
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsClosedCurveNode")
- self.nodeTypeComboBox.addItem("vtkMRMLMarkupsROINode")
- parametersFormLayout.addRow("Node type: ", self.nodeTypeComboBox)
-
- #
- # number of nodes to add
- #
- self.numberOfNodesSliderWidget = ctk.ctkSliderWidget()
- self.numberOfNodesSliderWidget.singleStep = 1.0
- self.numberOfNodesSliderWidget.decimals = 0
- self.numberOfNodesSliderWidget.minimum = 0.0
- self.numberOfNodesSliderWidget.maximum = 1000.0
- self.numberOfNodesSliderWidget.value = 1.0
- self.numberOfNodesSliderWidget.toolTip = "Set the number of nodes to add."
- parametersFormLayout.addRow("Number of nodes: ", self.numberOfNodesSliderWidget)
-
- #
- # number of fiducials to add
- #
- self.numberOfControlPointsSliderWidget = ctk.ctkSliderWidget()
- self.numberOfControlPointsSliderWidget.singleStep = 1.0
- self.numberOfControlPointsSliderWidget.decimals = 0
- self.numberOfControlPointsSliderWidget.minimum = 0.0
- self.numberOfControlPointsSliderWidget.maximum = 10000.0
- self.numberOfControlPointsSliderWidget.value = 500.0
- self.numberOfControlPointsSliderWidget.toolTip = "Set the number of control points to add per node."
- parametersFormLayout.addRow("Number of control points: ", self.numberOfControlPointsSliderWidget)
-
- #
- # check box to trigger fewer modify events, adding all the new points
- # is wrapped inside of a StartModify/EndModify block
- #
- self.fewerModifyFlagCheckBox = qt.QCheckBox()
- self.fewerModifyFlagCheckBox.checked = 0
- self.fewerModifyFlagCheckBox.toolTip = 'If checked, wrap adding points inside of a StartModify - EndModify block'
- parametersFormLayout.addRow("Fewer modify events: ", self.fewerModifyFlagCheckBox)
-
- #
- # markups locked
- #
- self.lockedFlagCheckBox = qt.QCheckBox()
- self.lockedFlagCheckBox.checked = 0
- self.lockedFlagCheckBox.toolTip = 'If checked, markups will be locked for editing'
- parametersFormLayout.addRow("Locked nodes: ", self.lockedFlagCheckBox)
-
- #
- # markups labels hidden
- #
- self.labelsHiddenFlagCheckBox = qt.QCheckBox()
- self.labelsHiddenFlagCheckBox.checked = 0
- self.labelsHiddenFlagCheckBox.toolTip = 'If checked, markups labels will be forced to be hidden, regardless of default markups properties'
- parametersFormLayout.addRow("Labels hidden: ", self.labelsHiddenFlagCheckBox)
-
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
-
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- def cleanup(self):
- pass
-
- def onApplyButton(self):
- logic = AddManyMarkupsFiducialTestLogic()
- nodeType = self.nodeTypeComboBox.currentText
- numberOfNodes = int(self.numberOfNodesSliderWidget.value)
- numberOfControlPoints = int(self.numberOfControlPointsSliderWidget.value)
- fewerModifyFlag = self.fewerModifyFlagCheckBox.checked
- labelsHiddenFlag = self.labelsHiddenFlagCheckBox.checked
- locked = self.lockedFlagCheckBox.checked
- print(f"Run the logic method to add {numberOfNodes} nodes with {numberOfControlPoints} control points each")
- logic.run(nodeType, numberOfNodes, numberOfControlPoints, 0, fewerModifyFlag, locked, labelsHiddenFlag)
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+ # Instantiate and connect widgets ...
+
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
+
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ #
+ # node type to add
+ #
+ self.nodeTypeComboBox = qt.QComboBox()
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsFiducialNode")
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsLineNode")
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsAngleNode")
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsCurveNode")
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsClosedCurveNode")
+ self.nodeTypeComboBox.addItem("vtkMRMLMarkupsROINode")
+ parametersFormLayout.addRow("Node type: ", self.nodeTypeComboBox)
+
+ #
+ # number of nodes to add
+ #
+ self.numberOfNodesSliderWidget = ctk.ctkSliderWidget()
+ self.numberOfNodesSliderWidget.singleStep = 1.0
+ self.numberOfNodesSliderWidget.decimals = 0
+ self.numberOfNodesSliderWidget.minimum = 0.0
+ self.numberOfNodesSliderWidget.maximum = 1000.0
+ self.numberOfNodesSliderWidget.value = 1.0
+ self.numberOfNodesSliderWidget.toolTip = "Set the number of nodes to add."
+ parametersFormLayout.addRow("Number of nodes: ", self.numberOfNodesSliderWidget)
+
+ #
+ # number of fiducials to add
+ #
+ self.numberOfControlPointsSliderWidget = ctk.ctkSliderWidget()
+ self.numberOfControlPointsSliderWidget.singleStep = 1.0
+ self.numberOfControlPointsSliderWidget.decimals = 0
+ self.numberOfControlPointsSliderWidget.minimum = 0.0
+ self.numberOfControlPointsSliderWidget.maximum = 10000.0
+ self.numberOfControlPointsSliderWidget.value = 500.0
+ self.numberOfControlPointsSliderWidget.toolTip = "Set the number of control points to add per node."
+ parametersFormLayout.addRow("Number of control points: ", self.numberOfControlPointsSliderWidget)
+
+ #
+ # check box to trigger fewer modify events, adding all the new points
+ # is wrapped inside of a StartModify/EndModify block
+ #
+ self.fewerModifyFlagCheckBox = qt.QCheckBox()
+ self.fewerModifyFlagCheckBox.checked = 0
+ self.fewerModifyFlagCheckBox.toolTip = 'If checked, wrap adding points inside of a StartModify - EndModify block'
+ parametersFormLayout.addRow("Fewer modify events: ", self.fewerModifyFlagCheckBox)
+
+ #
+ # markups locked
+ #
+ self.lockedFlagCheckBox = qt.QCheckBox()
+ self.lockedFlagCheckBox.checked = 0
+ self.lockedFlagCheckBox.toolTip = 'If checked, markups will be locked for editing'
+ parametersFormLayout.addRow("Locked nodes: ", self.lockedFlagCheckBox)
+
+ #
+ # markups labels hidden
+ #
+ self.labelsHiddenFlagCheckBox = qt.QCheckBox()
+ self.labelsHiddenFlagCheckBox.checked = 0
+ self.labelsHiddenFlagCheckBox.toolTip = 'If checked, markups labels will be forced to be hidden, regardless of default markups properties'
+ parametersFormLayout.addRow("Labels hidden: ", self.labelsHiddenFlagCheckBox)
+
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
+
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ def cleanup(self):
+ pass
+
+ def onApplyButton(self):
+ logic = AddManyMarkupsFiducialTestLogic()
+ nodeType = self.nodeTypeComboBox.currentText
+ numberOfNodes = int(self.numberOfNodesSliderWidget.value)
+ numberOfControlPoints = int(self.numberOfControlPointsSliderWidget.value)
+ fewerModifyFlag = self.fewerModifyFlagCheckBox.checked
+ labelsHiddenFlag = self.labelsHiddenFlagCheckBox.checked
+ locked = self.lockedFlagCheckBox.checked
+ print(f"Run the logic method to add {numberOfNodes} nodes with {numberOfControlPoints} control points each")
+ logic.run(nodeType, numberOfNodes, numberOfControlPoints, 0, fewerModifyFlag, locked, labelsHiddenFlag)
#
@@ -145,104 +145,104 @@ def onApplyButton(self):
class AddManyMarkupsFiducialTestLogic(ScriptedLoadableModuleLogic):
- def run(self, nodeType, numberOfNodes=10, numberOfControlPoints=10, rOffset=0, usefewerModifyCalls=False, locked=False, labelsHidden=False):
- """
- Run the actual algorithm
- """
- print(f'Running test to add {numberOfNodes} nodes markups with {numberOfControlPoints} control points')
- print('Index\tTime to add fid\tDelta between adds')
- print("%(index)04s\t" % {'index': "i"}, "t\tdt'")
- r = rOffset
- a = 0
- s = 0
- t1 = 0
- t2 = 0
- t3 = 0
- t4 = 0
- timeToAddThisFid = 0
- timeToAddLastFid = 0
-
- testStartTime = time.process_time()
-
- import random
-
- if usefewerModifyCalls:
- print("Pause render")
- slicer.app.pauseRender()
-
- for nodeIndex in range(numberOfNodes):
-
- markupsNode = slicer.mrmlScene.AddNewNodeByClass(nodeType)
- markupsNode.CreateDefaultDisplayNodes()
- if locked:
- markupsNode.SetLocked(True)
-
- if labelsHidden:
- markupsNode.GetDisplayNode().SetPropertiesLabelVisibility(False)
- markupsNode.GetDisplayNode().SetPointLabelsVisibility(False)
-
- if usefewerModifyCalls:
- print("Start modify")
- mod = markupsNode.StartModify()
-
- for controlPointIndex in range(numberOfControlPoints):
- # print "controlPointIndex = ", controlPointIndex, "/", numberOfControlPoints, ", r = ", r, ", a = ", a, ", s = ", s
- t1 = time.process_time()
- markupsNode.AddControlPoint(vtk.vtkVector3d(r, a, s))
- t2 = time.process_time()
- timeToAddThisFid = t2 - t1
- dt = timeToAddThisFid - timeToAddLastFid
- # print '%(index)04d\t' % {'index': controlPointIndex}, timeToAddThisFid, "\t", dt
- r = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0)
- a = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0)
- s = random.uniform(-20.0, 20.0)
- timeToAddLastFid = timeToAddThisFid
-
- if usefewerModifyCalls:
- markupsNode.EndModify(mod)
-
- if usefewerModifyCalls:
- print("Resume render")
- slicer.app.resumeRender()
-
- testEndTime = time.process_time()
- testTime = testEndTime - testStartTime
- print("Total time to add ", numberOfControlPoints, " = ", testTime)
-
- return True
+ def run(self, nodeType, numberOfNodes=10, numberOfControlPoints=10, rOffset=0, usefewerModifyCalls=False, locked=False, labelsHidden=False):
+ """
+ Run the actual algorithm
+ """
+ print(f'Running test to add {numberOfNodes} nodes markups with {numberOfControlPoints} control points')
+ print('Index\tTime to add fid\tDelta between adds')
+ print("%(index)04s\t" % {'index': "i"}, "t\tdt'")
+ r = rOffset
+ a = 0
+ s = 0
+ t1 = 0
+ t2 = 0
+ t3 = 0
+ t4 = 0
+ timeToAddThisFid = 0
+ timeToAddLastFid = 0
+
+ testStartTime = time.process_time()
+
+ import random
+
+ if usefewerModifyCalls:
+ print("Pause render")
+ slicer.app.pauseRender()
+
+ for nodeIndex in range(numberOfNodes):
+
+ markupsNode = slicer.mrmlScene.AddNewNodeByClass(nodeType)
+ markupsNode.CreateDefaultDisplayNodes()
+ if locked:
+ markupsNode.SetLocked(True)
+
+ if labelsHidden:
+ markupsNode.GetDisplayNode().SetPropertiesLabelVisibility(False)
+ markupsNode.GetDisplayNode().SetPointLabelsVisibility(False)
+
+ if usefewerModifyCalls:
+ print("Start modify")
+ mod = markupsNode.StartModify()
+
+ for controlPointIndex in range(numberOfControlPoints):
+ # print "controlPointIndex = ", controlPointIndex, "/", numberOfControlPoints, ", r = ", r, ", a = ", a, ", s = ", s
+ t1 = time.process_time()
+ markupsNode.AddControlPoint(vtk.vtkVector3d(r, a, s))
+ t2 = time.process_time()
+ timeToAddThisFid = t2 - t1
+ dt = timeToAddThisFid - timeToAddLastFid
+ # print '%(index)04d\t' % {'index': controlPointIndex}, timeToAddThisFid, "\t", dt
+ r = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0)
+ a = float(controlPointIndex) / numberOfControlPoints * 100.0 - 50.0 + random.uniform(-20.0, 20.0)
+ s = random.uniform(-20.0, 20.0)
+ timeToAddLastFid = timeToAddThisFid
+
+ if usefewerModifyCalls:
+ markupsNode.EndModify(mod)
+
+ if usefewerModifyCalls:
+ print("Resume render")
+ slicer.app.resumeRender()
+
+ testEndTime = time.process_time()
+ testTime = testEndTime - testStartTime
+ print("Total time to add ", numberOfControlPoints, " = ", testTime)
+
+ return True
class AddManyMarkupsFiducialTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.setUp()
- self.test_AddManyMarkupsFiducialTest1()
- def test_AddManyMarkupsFiducialTest1(self):
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_AddManyMarkupsFiducialTest1()
+
+ def test_AddManyMarkupsFiducialTest1(self):
- self.delayDisplay("Starting the add many Markups fiducials test")
+ self.delayDisplay("Starting the add many Markups fiducials test")
- # start in the welcome module
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('Welcome')
+ # start in the welcome module
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('Welcome')
- logic = AddManyMarkupsFiducialTestLogic()
- logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=0)
+ logic = AddManyMarkupsFiducialTestLogic()
+ logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=0)
- self.delayDisplay("Now running it while the Markups Module is open")
- m.moduleSelector().selectModule('Markups')
- logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=100)
+ self.delayDisplay("Now running it while the Markups Module is open")
+ m.moduleSelector().selectModule('Markups')
+ logic.run('vtkMRMLMarkupsFiducialNode', numberOfNodes=1, numberOfControlPoints=100, rOffset=100)
- self.delayDisplay('Test passed!')
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py
index 303a98d14c7..c1bb5b784ef 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveCoordinateFrameTest.py
@@ -12,68 +12,68 @@
def updateCoordinateSystemsModel(updateInfo):
- model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo
- coordinateSystemAppender.RemoveAllInputs()
- numberOfCurvePoints = curve.GetCurvePointsWorld().GetNumberOfPoints()
- for curvePointIndex in range(numberOfCurvePoints):
- result = curve.GetCurvePointToWorldTransformAtPointIndex(curvePointIndex, curvePointToWorldTransform)
- transform.SetMatrix(curvePointToWorldTransform)
- transformer.Update()
- coordinateSystemInWorld = vtk.vtkPolyData()
- coordinateSystemInWorld.DeepCopy(transformer.GetOutput())
- coordinateSystemAppender.AddInputData(coordinateSystemInWorld)
- coordinateSystemAppender.Update()
- model.SetAndObservePolyData(coordinateSystemAppender.GetOutput())
+ model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo
+ coordinateSystemAppender.RemoveAllInputs()
+ numberOfCurvePoints = curve.GetCurvePointsWorld().GetNumberOfPoints()
+ for curvePointIndex in range(numberOfCurvePoints):
+ result = curve.GetCurvePointToWorldTransformAtPointIndex(curvePointIndex, curvePointToWorldTransform)
+ transform.SetMatrix(curvePointToWorldTransform)
+ transformer.Update()
+ coordinateSystemInWorld = vtk.vtkPolyData()
+ coordinateSystemInWorld.DeepCopy(transformer.GetOutput())
+ coordinateSystemAppender.AddInputData(coordinateSystemInWorld)
+ coordinateSystemAppender.Update()
+ model.SetAndObservePolyData(coordinateSystemAppender.GetOutput())
def createCoordinateSystemsModel(curve, axisLength=5):
- """Add a coordinate system model at each curve point.
- :param curve: input curve
- :param axisLength: length of normal axis is `axisLength*2`, length of binormal and tangent axes are `axisLength`
- :return model, coordinateSystemAppender, curvePointToWorldTransform, transform
- """
- # Create coordinate system polydata
- axisAppender = vtk.vtkAppendPolyData()
- xAxis = vtk.vtkLineSource()
- xAxis.SetPoint1(0, 0, 0)
- xAxis.SetPoint2(axisLength * 2, 0, 0)
- yAxis = vtk.vtkLineSource()
- yAxis.SetPoint1(0, 0, 0)
- yAxis.SetPoint2(0, axisLength, 0)
- zAxis = vtk.vtkLineSource()
- zAxis.SetPoint1(0, 0, 0)
- zAxis.SetPoint2(0, 0, axisLength)
- axisAppender.AddInputConnection(xAxis.GetOutputPort())
- axisAppender.AddInputConnection(yAxis.GetOutputPort())
- axisAppender.AddInputConnection(zAxis.GetOutputPort())
- # Initialize transformer that will place the coordinate system polydata along the curve
- curvePointToWorldTransform = vtk.vtkMatrix4x4()
- transformer = vtk.vtkTransformPolyDataFilter()
- transform = vtk.vtkTransform()
- transformer.SetTransform(transform)
- transformer.SetInputConnection(axisAppender.GetOutputPort())
- # Create model appender that assembles the model that contains all the coordinate systems
- coordinateSystemAppender = vtk.vtkAppendPolyData()
- # model = slicer.modules.models.logic().AddModel(coordinateSystemAppender.GetOutputPort())
- model = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode')
- model.CreateDefaultDisplayNodes()
- # prevent picking by markups so that the control points can be moved without sticking to the generated model
- model.SetSelectable(False)
- updateInfo = [model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer]
- updateCoordinateSystemsModel(updateInfo)
- return updateInfo
+ """Add a coordinate system model at each curve point.
+ :param curve: input curve
+ :param axisLength: length of normal axis is `axisLength*2`, length of binormal and tangent axes are `axisLength`
+ :return model, coordinateSystemAppender, curvePointToWorldTransform, transform
+ """
+ # Create coordinate system polydata
+ axisAppender = vtk.vtkAppendPolyData()
+ xAxis = vtk.vtkLineSource()
+ xAxis.SetPoint1(0, 0, 0)
+ xAxis.SetPoint2(axisLength * 2, 0, 0)
+ yAxis = vtk.vtkLineSource()
+ yAxis.SetPoint1(0, 0, 0)
+ yAxis.SetPoint2(0, axisLength, 0)
+ zAxis = vtk.vtkLineSource()
+ zAxis.SetPoint1(0, 0, 0)
+ zAxis.SetPoint2(0, 0, axisLength)
+ axisAppender.AddInputConnection(xAxis.GetOutputPort())
+ axisAppender.AddInputConnection(yAxis.GetOutputPort())
+ axisAppender.AddInputConnection(zAxis.GetOutputPort())
+ # Initialize transformer that will place the coordinate system polydata along the curve
+ curvePointToWorldTransform = vtk.vtkMatrix4x4()
+ transformer = vtk.vtkTransformPolyDataFilter()
+ transform = vtk.vtkTransform()
+ transformer.SetTransform(transform)
+ transformer.SetInputConnection(axisAppender.GetOutputPort())
+ # Create model appender that assembles the model that contains all the coordinate systems
+ coordinateSystemAppender = vtk.vtkAppendPolyData()
+ # model = slicer.modules.models.logic().AddModel(coordinateSystemAppender.GetOutputPort())
+ model = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLModelNode')
+ model.CreateDefaultDisplayNodes()
+ # prevent picking by markups so that the control points can be moved without sticking to the generated model
+ model.SetSelectable(False)
+ updateInfo = [model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer]
+ updateCoordinateSystemsModel(updateInfo)
+ return updateInfo
def addCoordinateSystemUpdater(updateInfo):
- model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo
- observation = curve.AddObserver(slicer.vtkMRMLMarkupsNode.PointModifiedEvent,
- lambda caller, eventData, updateInfo=updateInfo: updateCoordinateSystemsModel(updateInfo))
- return [curve, observation]
+ model, curve, coordinateSystemAppender, curvePointToWorldTransform, transform, transformer = updateInfo
+ observation = curve.AddObserver(slicer.vtkMRMLMarkupsNode.PointModifiedEvent,
+ lambda caller, eventData, updateInfo=updateInfo: updateCoordinateSystemsModel(updateInfo))
+ return [curve, observation]
def removeCoordinateSystemUpdaters(curveObservations):
- for curve, observer in curveObservations:
- curve.RemoveObserver(observer)
+ for curve, observer in curveObservations:
+ curve.RemoveObserver(observer)
#
@@ -83,7 +83,7 @@ def removeCoordinateSystemUpdaters(curveObservations):
curveMeasurementsTestDir = slicer.app.temporaryPath + '/curveMeasurementsTest'
print('Test directory: ', curveMeasurementsTestDir)
if not os.access(curveMeasurementsTestDir, os.F_OK):
- os.mkdir(curveMeasurementsTestDir)
+ os.mkdir(curveMeasurementsTestDir)
curvePointToWorldTransform = vtk.vtkMatrix4x4()
@@ -95,9 +95,9 @@ def removeCoordinateSystemUpdaters(curveObservations):
testSceneFilePath = curveMeasurementsTestDir + '/MarkupsCurvatureTestScene.mrb'
slicer.util.downloadFile(
- TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32',
- testSceneFilePath,
- checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32')
+ TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32',
+ testSceneFilePath,
+ checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32')
slicer.util.loadScene(testSceneFilePath)
planarCurveNode = slicer.util.getNode('C')
@@ -109,17 +109,17 @@ def removeCoordinateSystemUpdaters(curveObservations):
# Check quantitative results
if not planarCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform):
- raise Exception("Test1 GetCurvePointToWorldTransformAtPointIndex failed")
+ raise Exception("Test1 GetCurvePointToWorldTransformAtPointIndex failed")
curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform)
expectedCurvePointToWorldMatrix = np.array(
- [[2.15191499e-01, 0.00000000e+00, -9.76571871e-01, -3.03394470e+01],
- [0.00000000e+00, -1.00000000e+00, 0.00000000e+00, 3.63797881e-09],
- [-9.76571871e-01, 0.00000000e+00, -2.15191499e-01, 8.10291061e+01],
- [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
+ [[2.15191499e-01, 0.00000000e+00, -9.76571871e-01, -3.03394470e+01],
+ [0.00000000e+00, -1.00000000e+00, 0.00000000e+00, 3.63797881e-09],
+ [-9.76571871e-01, 0.00000000e+00, -2.15191499e-01, 8.10291061e+01],
+ [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all():
- raise Exception(f"Test1 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
+ raise Exception(f"Test1 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
#
# Test2. Test free-form closed curve with 6 control points, all points in one plane
@@ -128,8 +128,8 @@ def removeCoordinateSystemUpdaters(curveObservations):
closedCurveNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsClosedCurveNode')
pos = np.zeros(3)
for i in range(planarCurveNode.GetNumberOfControlPoints()):
- planarCurveNode.GetNthControlPointPosition(i, pos)
- pointIndex = closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos))
+ planarCurveNode.GetNthControlPointPosition(i, pos)
+ pointIndex = closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos))
# Visualize
@@ -139,17 +139,17 @@ def removeCoordinateSystemUpdaters(curveObservations):
# Check quantitative results
if not closedCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform):
- raise Exception("Test2 GetCurvePointToWorldTransformAtPointIndex failed")
+ raise Exception("Test2 GetCurvePointToWorldTransformAtPointIndex failed")
curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform)
expectedCurvePointToWorldMatrix = np.array(
- [[-3.85813409e-01, 0.00000000e+00, -9.22576833e-01, -3.71780586e+01],
- [0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 3.63797881e-09],
- [9.22576833e-01, 0.00000000e+00, -3.85813409e-01, 8.78303909e+01],
- [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
+ [[-3.85813409e-01, 0.00000000e+00, -9.22576833e-01, -3.71780586e+01],
+ [0.00000000e+00, 1.00000000e+00, 0.00000000e+00, 3.63797881e-09],
+ [9.22576833e-01, 0.00000000e+00, -3.85813409e-01, 8.78303909e+01],
+ [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all():
- raise Exception(f"Test2 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
+ raise Exception(f"Test2 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
#
# Test3. Test on a vessel centerline curve
@@ -161,9 +161,9 @@ def removeCoordinateSystemUpdaters(curveObservations):
testSceneFilePath = curveMeasurementsTestDir + '/MarkupsControlPointMeasurementInterpolationTestScene.mrb'
slicer.util.downloadFile(
- TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344',
- testSceneFilePath,
- checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344')
+ TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344',
+ testSceneFilePath,
+ checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344')
# Import test scene
slicer.util.loadScene(testSceneFilePath)
@@ -177,7 +177,7 @@ def removeCoordinateSystemUpdaters(curveObservations):
centerlineCurve.SetName('CenterlineCurve')
for i in range(centerlinePolyData.GetNumberOfPoints()):
- pointIndex = centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i)))
+ pointIndex = centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i)))
# Spacing between control points is not uniform. Applying b-spline interpolation to it directly would
# cause discontinuities (overshoots where a large gap between points is followed by points close to each other).
@@ -194,17 +194,17 @@ def removeCoordinateSystemUpdaters(curveObservations):
# Check quantitative results
if not centerlineCurve.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform):
- raise Exception("Test3 GetCurvePointToWorldTransformAtPointIndex failed")
+ raise Exception("Test3 GetCurvePointToWorldTransformAtPointIndex failed")
curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform)
expectedCurvePointToWorldMatrix = np.array(
- [[9.85648052e-01, 8.80625424e-03, -1.68583415e-01, -3.08991909e+00],
- [-6.35257659e-02, 9.44581803e-01, -3.22070946e-01, 2.22146526e-01],
- [1.56404587e-01, 3.28157991e-01, 9.31584638e-01, -7.34501495e+01],
- [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
+ [[9.85648052e-01, 8.80625424e-03, -1.68583415e-01, -3.08991909e+00],
+ [-6.35257659e-02, 9.44581803e-01, -3.22070946e-01, 2.22146526e-01],
+ [1.56404587e-01, 3.28157991e-01, 9.31584638e-01, -7.34501495e+01],
+ [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all():
- raise Exception(f"Test3 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
+ raise Exception(f"Test3 CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
#
# Test4. curvature computation for a circle-shaped closed curve
@@ -226,17 +226,17 @@ def removeCoordinateSystemUpdaters(curveObservations):
# Check quantitative results
if not circleCurveNode.GetCurvePointToWorldTransformAtPointIndex(6, curvePointToWorldTransform):
- raise Exception("Test4. GetCurvePointToWorldTransformAtPointIndex failed")
+ raise Exception("Test4. GetCurvePointToWorldTransformAtPointIndex failed")
curvePointToWorldMatrix = slicer.util.arrayFromVTKMatrix(curvePointToWorldTransform)
expectedCurvePointToWorldMatrix = np.array(
- [[0.10190135, 0., 0.99479451, 3.29378772],
- [0.99479451, 0., -0.10190135, 34.84461594],
- [0., 1., 0., 0.],
- [0., 0., 0., 1.]])
+ [[0.10190135, 0., 0.99479451, 3.29378772],
+ [0.99479451, 0., -0.10190135, 34.84461594],
+ [0., 1., 0., 0.],
+ [0., 0., 0., 1.]])
if not np.isclose(curvePointToWorldMatrix, expectedCurvePointToWorldMatrix).all():
- raise Exception(f"Test4. CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
+ raise Exception(f"Test4. CurvePointToWorldTransformAtPointIndex value incorrect: got {curvePointToWorldMatrix}, expected {expectedCurvePointToWorldMatrix}.")
#
# Remove observations
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py
index f2f8640c172..5b13f927174 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsCurveMeasurementsTest.py
@@ -10,14 +10,14 @@
curveMeasurementsTestDir = slicer.app.temporaryPath + '/curveMeasurementsTest'
print('Test directory: ', curveMeasurementsTestDir)
if not os.access(curveMeasurementsTestDir, os.F_OK):
- os.mkdir(curveMeasurementsTestDir)
+ os.mkdir(curveMeasurementsTestDir)
testSceneFilePath = curveMeasurementsTestDir + '/MarkupsCurvatureTestScene.mrb'
slicer.util.downloadFile(
- TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32',
- testSceneFilePath,
- checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32')
+ TESTING_DATA_URL + 'SHA256/5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32',
+ testSceneFilePath,
+ checksum='SHA256:5b1f39e28ad8611790152fdc092ec9b3ee14254aad4897377db9576139c88e32')
# Import test scene
slicer.util.loadScene(testSceneFilePath)
@@ -26,8 +26,8 @@
# Check number of arrays in the curve node
curvePointData = curveNode.GetCurveWorld().GetPointData()
if curvePointData.GetNumberOfArrays() != 1:
- exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 1)"
- raise Exception(exceptionMessage)
+ exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 1)"
+ raise Exception(exceptionMessage)
# Turn on curvature calculation in curve node
curveNode.GetMeasurement("curvature max").SetEnabled(True)
@@ -35,31 +35,31 @@
# Check curvature computation result
curvePointData = curveNode.GetCurveWorld().GetPointData()
if curvePointData.GetNumberOfArrays() != 2:
- exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 2)"
- raise Exception(exceptionMessage)
+ exceptionMessage = f"Unexpected number of data arrays in curve: {curvePointData.GetNumberOfArrays()} (expected 2)"
+ raise Exception(exceptionMessage)
if curvePointData.GetArrayName(1) != 'Curvature':
- exceptionMessage = f"Unexpected data array name in curve: {curvePointData.GetArrayName(1)} (expected 'Curvature')"
- raise Exception(exceptionMessage)
+ exceptionMessage = f"Unexpected data array name in curve: {curvePointData.GetArrayName(1)} (expected 'Curvature')"
+ raise Exception(exceptionMessage)
curvatureArray = curvePointData.GetArray(1)
if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples() - 1:
- exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1)
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[0] - 0.0) > 0.0001:
- exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[1] - 0.9816015970208652) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
+ raise Exception(exceptionMessage)
# Turn off curvature computation
curveNode.GetMeasurement("curvature max").SetEnabled(False)
curvePointData = curveNode.GetCurveWorld().GetPointData()
if curvePointData.GetNumberOfArrays() != 1:
- exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays())
+ raise Exception(exceptionMessage)
print('Open curve curvature test finished successfully')
@@ -70,30 +70,30 @@
closedCurveNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsClosedCurveNode')
pos = np.zeros(3)
for i in range(curveNode.GetNumberOfControlPoints()):
- curveNode.GetNthControlPointPosition(i, pos)
- closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos))
+ curveNode.GetNthControlPointPosition(i, pos)
+ closedCurveNode.AddControlPoint(vtk.vtkVector3d(pos))
closedCurveNode.GetMeasurement("curvature mean").SetEnabled(True)
curvePointData = closedCurveNode.GetCurveWorld().GetPointData()
if curvePointData.GetNumberOfArrays() != 2:
- exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of data arrays in curve: " + str(curvePointData.GetNumberOfArrays())
+ raise Exception(exceptionMessage)
if curvePointData.GetArrayName(1) != 'Curvature':
- exceptionMessage = "Unexpected data array name in curve: " + str(curvePointData.GetArrayName(1))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected data array name in curve: " + str(curvePointData.GetArrayName(1))
+ raise Exception(exceptionMessage)
curvatureArray = curvePointData.GetArray(1)
if curvatureArray.GetMaxId() != curvePointData.GetNumberOfTuples() - 1:
- exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of values in curvature data array: %d (expected %d)" % (curvatureArray.GetMaxId(), curvePointData.GetNumberOfTuples() - 1)
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[0] - 0.0) > 0.0001:
- exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[1] - 0.26402460470400924) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
+ raise Exception(exceptionMessage)
print('Closed curve curvature test finished successfully')
@@ -109,9 +109,9 @@
testSceneFilePath = curveMeasurementsTestDir + '/MarkupsControlPointMeasurementInterpolationTestScene.mrb'
slicer.util.downloadFile(
- TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344',
- testSceneFilePath,
- checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344')
+ TESTING_DATA_URL + 'SHA256/b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344',
+ testSceneFilePath,
+ checksum='SHA256:b636ecfc1be54504c2c9843e1ff53242ee6b951228490ae99a89e06c8890e344')
# Import test scene
slicer.util.loadScene(testSceneFilePath)
@@ -124,7 +124,7 @@
centerlineCurve = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsCurveNode')
centerlineCurve.SetName('CenterlineCurve')
for i in range(centerlinePolyData.GetNumberOfPoints()):
- centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i)))
+ centerlineCurve.AddControlPoint(vtk.vtkVector3d(centerlinePolyData.GetPoint(i)))
# Add radius data to centerline curve as measurement
radiusMeasurement = slicer.vtkMRMLStaticMeasurement()
@@ -138,36 +138,36 @@
# Check interpolation computation result
if centerlineCurvePointData.GetNumberOfArrays() != 2:
- exceptionMessage = "Unexpected number of data arrays in curve: " + str(centerlineCurvePointData.GetNumberOfArrays())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of data arrays in curve: " + str(centerlineCurvePointData.GetNumberOfArrays())
+ raise Exception(exceptionMessage)
if centerlineCurvePointData.GetArrayName(1) != 'Radius':
- exceptionMessage = "Unexpected data array name in curve: " + str(centerlineCurvePointData.GetArrayName(1))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected data array name in curve: " + str(centerlineCurvePointData.GetArrayName(1))
+ raise Exception(exceptionMessage)
interpolatedRadiusArray = centerlineCurvePointData.GetArray(1)
if interpolatedRadiusArray.GetNumberOfTuples() != 571:
- exceptionMessage = "Unexpected number of data points in interpolated radius array: " + str(interpolatedRadiusArray.GetNumberOfTuples())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected number of data points in interpolated radius array: " + str(interpolatedRadiusArray.GetNumberOfTuples())
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetRange()[0] - 12.322814731747465) > 0.0001:
- exceptionMessage = "Unexpected minimum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[0])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected minimum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[0])
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetRange()[1] - 42.9542138185081) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[1])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetRange()[1])
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetValue(9) - 42.92838813390291) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(9))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(9))
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetValue(10) - 42.9542138185081) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(10))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(10))
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetValue(569) - 12.904227531040913) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(569))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(569))
+ raise Exception(exceptionMessage)
if abs(interpolatedRadiusArray.GetValue(570) - 12.765926543271583) > 0.0001:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(570))
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(interpolatedRadiusArray.GetValue(570))
+ raise Exception(exceptionMessage)
print('Control point measurement interpolation test finished successfully')
@@ -188,36 +188,36 @@
closedCurveNode.GetMeasurement("curvature max").SetEnabled(True)
curvatureArray = closedCurveNode.GetCurveWorld().GetPointData().GetArray('Curvature')
if curvatureArray.GetNumberOfValues() < 10:
- exceptionMessage = "Many values are expected in the curvature array, instead found just %d" % curvatureArray.GetNumberOfValues()
- raise Exception(exceptionMessage)
+ exceptionMessage = "Many values are expected in the curvature array, instead found just %d" % curvatureArray.GetNumberOfValues()
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[0] - 1 / radius) > 1e-4:
- exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected minimum in curvature data array: " + str(curvatureArray.GetRange()[0])
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[1] - 1 / radius) > 1e-4:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
+ raise Exception(exceptionMessage)
if abs(curvatureArray.GetRange()[1] - 1 / radius) > 1e-4:
- exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected maximum in curvature data array: " + str(curvatureArray.GetRange()[1])
+ raise Exception(exceptionMessage)
if abs(closedCurveNode.GetMeasurement("curvature mean").GetValue() - 1 / radius) > 1e-4:
- exceptionMessage = "Unexpected curvature mean value: " + str(closedCurveNode.GetMeasurement("curvature mean").GetValue())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected curvature mean value: " + str(closedCurveNode.GetMeasurement("curvature mean").GetValue())
+ raise Exception(exceptionMessage)
if abs(closedCurveNode.GetMeasurement("curvature max").GetValue() - 1 / radius) > 1e-4:
- exceptionMessage = "Unexpected curvature max value: " + str(closedCurveNode.GetMeasurement("curvature max").GetValue())
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected curvature max value: " + str(closedCurveNode.GetMeasurement("curvature max").GetValue())
+ raise Exception(exceptionMessage)
# Check length and area
closedCurveNode.GetMeasurement("length").SetEnabled(True)
if closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString() != '219.9mm':
- exceptionMessage = "Unexpected curve length value: " + closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString()
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected curve length value: " + closedCurveNode.GetMeasurement("length").GetValueWithUnitsAsPrintableString()
+ raise Exception(exceptionMessage)
closedCurveNode.GetMeasurement("area").SetEnabled(True)
if closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString() != '38.48cm2':
- exceptionMessage = "Unexpected curve area value: " + closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString()
- raise Exception(exceptionMessage)
+ exceptionMessage = "Unexpected curve area value: " + closedCurveNode.GetMeasurement("area").GetValueWithUnitsAsPrintableString()
+ raise Exception(exceptionMessage)
# Display surface area as a model.
# Useful for manual testing of surface quality.
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py
index 29dbe8204bb..f0511777ed0 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsInCompareViewersSelfTest.py
@@ -10,16 +10,16 @@
#
class MarkupsInCompareViewersSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "MarkupsInCompareViewersSelfTest"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = []
- parent.contributors = ["Nicole Aucoin (BWH)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "MarkupsInCompareViewersSelfTest"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = []
+ parent.contributors = ["Nicole Aucoin (BWH)"]
+ parent.helpText = """
This is a test case that exercises the control points lists with compare viewers.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1.
"""
@@ -30,40 +30,40 @@ def __init__(self, parent):
class MarkupsInCompareViewersSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- logic = MarkupsInCompareViewersSelfTestLogic()
- logic.run()
+ def onApplyButton(self):
+ logic = MarkupsInCompareViewersSelfTestLogic()
+ logic.run()
#
@@ -72,141 +72,141 @@ def onApplyButton(self):
class MarkupsInCompareViewersSelfTestLogic(ScriptedLoadableModuleLogic):
- def run(self):
- """
- Run the actual algorithm
- """
- print('Running test of the markups in compare viewers')
-
- #
- # first load the data
- #
- print("Getting MR Head Volume")
- import SampleData
- mrHeadVolume = SampleData.downloadSample("MRHead")
-
- #
- # link the viewers
- #
- sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
- compositeNode = sliceLogic.GetSliceCompositeNode()
- compositeNode.SetLinkedControl(1)
-
- #
- # MR Head in the background
- #
- sliceLogic.StartSliceCompositeNodeInteraction(1)
- compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID())
- sliceLogic.EndSliceCompositeNodeInteraction()
-
- #
- # switch to conventional layout
- #
- lm = slicer.app.layoutManager()
- lm.setLayout(2)
-
- # create a control points list
- displayNode = slicer.vtkMRMLMarkupsDisplayNode()
- slicer.mrmlScene.AddNode(displayNode)
- fidNode = slicer.vtkMRMLMarkupsFiducialNode()
- slicer.mrmlScene.AddNode(fidNode)
- fidNode.SetAndObserveDisplayNodeID(displayNode.GetID())
-
- # make it active
- selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
- if (selectionNode is not None):
- selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID())
-
- # add some known points to it
- eye1 = [33.4975, 79.4042, -10.2143]
- eye2 = [-31.283, 80.9652, -16.2143]
- nose = [4.61944, 114.526, -33.2143]
- index = fidNode.AddControlPoint(eye1)
- fidNode.SetNthControlPointLabel(index, "eye-1")
- index = fidNode.AddControlPoint(eye2)
- fidNode.SetNthControlPointLabel(index, "eye-2")
- index = fidNode.AddControlPoint(nose)
- fidNode.SetNthControlPointLabel(index, "nose")
-
- slicer.util.delayDisplay("Placed 3 control points")
-
- #
- # switch to 2 viewers compare layout
- #
- lm.setLayout(12)
- slicer.util.delayDisplay("Switched to Compare 2 viewers")
-
- #
- # get compare slice composite node
- #
- compareLogic1 = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic()
- compareCompositeNode1 = compareLogic1.GetSliceCompositeNode()
-
- # set MRHead in the background
- compareLogic1.StartSliceCompositeNodeInteraction(1)
- compareCompositeNode1.SetBackgroundVolumeID(mrHeadVolume.GetID())
- compareLogic1.EndSliceCompositeNodeInteraction()
- compareLogic1.FitSliceToAll()
- # make it visible in 3D
- compareLogic1.GetSliceNode().SetSliceVisible(1)
-
- # scroll to a control point location
- compareLogic1.StartSliceOffsetInteraction()
- compareLogic1.SetSliceOffset(eye1[2])
- compareLogic1.EndSliceOffsetInteraction()
- slicer.util.delayDisplay("MH Head in background, scrolled to a control point")
-
- # scroll around through the range of points
- offset = nose[2]
- while offset < eye1[2]:
- compareLogic1.StartSliceOffsetInteraction()
- compareLogic1.SetSliceOffset(offset)
- compareLogic1.EndSliceOffsetInteraction()
- msg = "Scrolled to " + str(offset)
- slicer.util.delayDisplay(msg, 250)
- offset += 1.0
-
- # switch back to conventional
- lm.setLayout(2)
- slicer.util.delayDisplay("Switched back to conventional layout")
-
- # switch to compare grid
- lm.setLayout(23)
- compareLogic1.FitSliceToAll()
- slicer.util.delayDisplay("Switched to Compare grid")
-
- # switch back to conventional
- lm.setLayout(2)
- slicer.util.delayDisplay("Switched back to conventional layout")
-
- return True
+ def run(self):
+ """
+ Run the actual algorithm
+ """
+ print('Running test of the markups in compare viewers')
+
+ #
+ # first load the data
+ #
+ print("Getting MR Head Volume")
+ import SampleData
+ mrHeadVolume = SampleData.downloadSample("MRHead")
+
+ #
+ # link the viewers
+ #
+ sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ compositeNode.SetLinkedControl(1)
+
+ #
+ # MR Head in the background
+ #
+ sliceLogic.StartSliceCompositeNodeInteraction(1)
+ compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID())
+ sliceLogic.EndSliceCompositeNodeInteraction()
+
+ #
+ # switch to conventional layout
+ #
+ lm = slicer.app.layoutManager()
+ lm.setLayout(2)
+
+ # create a control points list
+ displayNode = slicer.vtkMRMLMarkupsDisplayNode()
+ slicer.mrmlScene.AddNode(displayNode)
+ fidNode = slicer.vtkMRMLMarkupsFiducialNode()
+ slicer.mrmlScene.AddNode(fidNode)
+ fidNode.SetAndObserveDisplayNodeID(displayNode.GetID())
+
+ # make it active
+ selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
+ if (selectionNode is not None):
+ selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID())
+
+ # add some known points to it
+ eye1 = [33.4975, 79.4042, -10.2143]
+ eye2 = [-31.283, 80.9652, -16.2143]
+ nose = [4.61944, 114.526, -33.2143]
+ index = fidNode.AddControlPoint(eye1)
+ fidNode.SetNthControlPointLabel(index, "eye-1")
+ index = fidNode.AddControlPoint(eye2)
+ fidNode.SetNthControlPointLabel(index, "eye-2")
+ index = fidNode.AddControlPoint(nose)
+ fidNode.SetNthControlPointLabel(index, "nose")
+
+ slicer.util.delayDisplay("Placed 3 control points")
+
+ #
+ # switch to 2 viewers compare layout
+ #
+ lm.setLayout(12)
+ slicer.util.delayDisplay("Switched to Compare 2 viewers")
+
+ #
+ # get compare slice composite node
+ #
+ compareLogic1 = slicer.app.layoutManager().sliceWidget('Compare1').sliceLogic()
+ compareCompositeNode1 = compareLogic1.GetSliceCompositeNode()
+
+ # set MRHead in the background
+ compareLogic1.StartSliceCompositeNodeInteraction(1)
+ compareCompositeNode1.SetBackgroundVolumeID(mrHeadVolume.GetID())
+ compareLogic1.EndSliceCompositeNodeInteraction()
+ compareLogic1.FitSliceToAll()
+ # make it visible in 3D
+ compareLogic1.GetSliceNode().SetSliceVisible(1)
+
+ # scroll to a control point location
+ compareLogic1.StartSliceOffsetInteraction()
+ compareLogic1.SetSliceOffset(eye1[2])
+ compareLogic1.EndSliceOffsetInteraction()
+ slicer.util.delayDisplay("MH Head in background, scrolled to a control point")
+
+ # scroll around through the range of points
+ offset = nose[2]
+ while offset < eye1[2]:
+ compareLogic1.StartSliceOffsetInteraction()
+ compareLogic1.SetSliceOffset(offset)
+ compareLogic1.EndSliceOffsetInteraction()
+ msg = "Scrolled to " + str(offset)
+ slicer.util.delayDisplay(msg, 250)
+ offset += 1.0
+
+ # switch back to conventional
+ lm.setLayout(2)
+ slicer.util.delayDisplay("Switched back to conventional layout")
+
+ # switch to compare grid
+ lm.setLayout(23)
+ compareLogic1.FitSliceToAll()
+ slicer.util.delayDisplay("Switched to Compare grid")
+
+ # switch back to conventional
+ lm.setLayout(2)
+ slicer.util.delayDisplay("Switched back to conventional layout")
+
+ return True
class MarkupsInCompareViewersSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_MarkupsInCompareViewersSelfTest1()
- def test_MarkupsInCompareViewersSelfTest1(self):
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_MarkupsInCompareViewersSelfTest1()
+
+ def test_MarkupsInCompareViewersSelfTest1(self):
- self.delayDisplay("Starting the Markups in compare viewers test")
+ self.delayDisplay("Starting the Markups in compare viewers test")
- # start in the welcome module
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('Welcome')
+ # start in the welcome module
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('Welcome')
- logic = MarkupsInCompareViewersSelfTestLogic()
- logic.run()
+ logic = MarkupsInCompareViewersSelfTestLogic()
+ logic.run()
- self.delayDisplay('Test passed!')
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py
index c47984fade6..f8b07880540 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsInViewsSelfTest.py
@@ -11,16 +11,16 @@
#
class MarkupsInViewsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "MarkupsInViewsSelfTest"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = []
- parent.contributors = ["Nicole Aucoin (BWH)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "MarkupsInViewsSelfTest"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = []
+ parent.contributors = ["Nicole Aucoin (BWH)"]
+ parent.helpText = """
This is a test case that exercises the control points nodes with different settings on the display node to show only in certain views.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Nicole Aucoin, BWH and was partially funded by NIH grant 3P41RR013218-12S1.
"""
@@ -31,42 +31,42 @@ def __init__(self, parent):
class MarkupsInViewsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- # note difference from running from command line: does not switch to the
- # markups module
- logic = MarkupsInViewsSelfTestLogic()
- logic.run()
+ def onApplyButton(self):
+ # note difference from running from command line: does not switch to the
+ # markups module
+ logic = MarkupsInViewsSelfTestLogic()
+ logic.run()
#
@@ -75,273 +75,273 @@ def onApplyButton(self):
class MarkupsInViewsSelfTestLogic(ScriptedLoadableModuleLogic):
- def controlPointVisible3D(self, fidNode, viewNodeID, controlPointIndex):
- lm = slicer.app.layoutManager()
- for v in range(lm.threeDViewCount):
- td = lm.threeDWidget(v)
- if td.viewLogic().GetViewNode().GetID() != viewNodeID:
- continue
- td.threeDView().forceRender()
- slicer.app.processEvents()
- ms = vtk.vtkCollection()
- td.getDisplayableManagers(ms)
- for i in range(ms.GetNumberOfItems()):
- m = ms.GetItemAsObject(i)
- if m.GetClassName() == "vtkMRMLMarkupsDisplayableManager":
- markupsWidget = m.GetWidget(fidNode.GetDisplayNode())
- return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex)
- return False
-
- def controlPointVisibleSlice(self, fidNode, sliceNodeID, controlPointIndex):
- lm = slicer.app.layoutManager()
- sliceNames = lm.sliceViewNames()
- for sliceName in sliceNames:
- sliceWidget = lm.sliceWidget(sliceName)
- sliceView = sliceWidget.sliceView()
- sliceNode = sliceView.mrmlSliceNode()
- if sliceNode.GetID() != sliceNodeID:
- continue
- sliceView.forceRender()
- slicer.app.processEvents()
- ms = vtk.vtkCollection()
- sliceView.getDisplayableManagers(ms)
- for i in range(ms.GetNumberOfItems()):
- m = ms.GetItemAsObject(i)
- if m.GetClassName() == 'vtkMRMLMarkupsDisplayableManager':
- markupsWidget = m.GetWidget(fidNode.GetDisplayNode())
- return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex)
- return False
-
- def printViewNodeIDs(self, displayNode):
- numIDs = displayNode.GetNumberOfViewNodeIDs()
- if numIDs == 0:
- print('No view node ids for display node', displayNode.GetID())
- return
- print('View node ids for display node', displayNode.GetID())
- for i in range(numIDs):
- id = displayNode.GetNthViewNodeID(i)
- print(id)
-
- def printViewAndSliceNodes(self):
- numViewNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLViewNode')
- print('Number of view nodes = ', numViewNodes)
- for vn in range(numViewNodes):
- viewNode = slicer.mrmlScene.GetNthNodeByClass(vn, 'vtkMRMLViewNode')
- print('\t', viewNode.GetName(), "id =", viewNode.GetID())
-
- numSliceNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode')
- print('Number of slice nodes = ', numSliceNodes)
- for sn in range(numSliceNodes):
- sliceNode = slicer.mrmlScene.GetNthNodeByClass(sn, 'vtkMRMLSliceNode')
- print('\t', sliceNode.GetName(), "id =", sliceNode.GetID())
-
- def onRecordNodeEvent(self, caller, event, eventId):
- self.nodeEvents.append(eventId)
-
- def run(self):
- """
- Run the actual algorithm
- """
- print('Running test of the markups in different views')
-
- #
- # first load the data
- #
- print("Getting MR Head Volume")
- import SampleData
- mrHeadVolume = SampleData.downloadSample("MRHead")
-
- #
- # link the viewers
- #
- sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
- compositeNode = sliceLogic.GetSliceCompositeNode()
- compositeNode.SetLinkedControl(1)
-
- #
- # MR Head in the background
- #
- sliceLogic.StartSliceCompositeNodeInteraction(1)
- compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID())
- sliceLogic.EndSliceCompositeNodeInteraction()
-
- #
- # switch to conventional layout
- #
- lm = slicer.app.layoutManager()
- lm.setLayout(2)
-
- # create a control points list
- fidNode = slicer.vtkMRMLMarkupsFiducialNode()
- slicer.mrmlScene.AddNode(fidNode)
- fidNode.CreateDefaultDisplayNodes()
- displayNode = fidNode.GetDisplayNode()
-
- # make it active
- selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
- if (selectionNode is not None):
- selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID())
-
- fidNodeObserverTags = []
- self.nodeEvents = []
- observedEvents = [
- slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent,
- slicer.vtkMRMLMarkupsNode.PointPositionUndefinedEvent]
- for eventId in observedEvents:
- fidNodeObserverTags.append(fidNode.AddObserver(eventId, lambda caller, event, eventId=eventId: self.onRecordNodeEvent(caller, event, eventId)))
-
- # add some known points to it
- eye1 = [33.4975, 79.4042, -10.2143]
- eye2 = [-31.283, 80.9652, -16.2143]
- nose = [4.61944, 114.526, -33.2143]
- controlPointIndex = fidNode.AddControlPoint(eye1)
- slicer.nodeEvents = self.nodeEvents
- assert(len(self.nodeEvents) == 1)
- assert(self.nodeEvents[0] == slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent)
- fidNode.SetNthControlPointLabel(controlPointIndex, "eye-1")
- controlPointIndex = fidNode.AddControlPoint(eye2)
- fidNode.SetNthControlPointLabel(controlPointIndex, "eye-2")
- # hide the second eye as a test of visibility flags
- fidNode.SetNthControlPointVisibility(controlPointIndex, controlPointIndex)
- controlPointIndex = fidNode.AddControlPoint(nose)
- fidNode.SetNthControlPointLabel(controlPointIndex, "nose")
-
- for tag in fidNodeObserverTags:
- fidNode.RemoveObserver(tag)
-
- slicer.util.delayDisplay("Placed 3 control points")
-
- # self.printViewAndSliceNodes()
-
- if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget is not visible in view 1")
- # self.printViewNodeIDs(displayNode)
- return False
-
- #
- # switch to 2 3D views layout
- #
- lm.setLayout(15)
- slicer.util.delayDisplay("Switched to 2 3D views")
- # self.printViewAndSliceNodes()
-
- controlPointIndex = 0
-
- slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex)
-
- if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex)
- or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)):
- slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2")
- # self.printViewNodeIDs(displayNode)
- return False
-
- #
- # show only in view 2
- #
- displayNode.AddViewNodeID("vtkMRMLViewNode2")
- slicer.util.delayDisplay("Showing only in view 2")
- if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 1")
- # self.printViewNodeIDs(displayNode)
- return False
- if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget is not visible in view 2")
- # self.printViewNodeIDs(displayNode)
- return False
-
- #
- # remove it so show in all
- #
- displayNode.RemoveAllViewNodeIDs()
- slicer.util.delayDisplay("Showing in both views")
- if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex)
- or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)):
- slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2")
- self.printViewNodeIDs(displayNode)
- return False
-
- #
- # show only in view 1
- #
- displayNode.AddViewNodeID("vtkMRMLViewNode1")
- slicer.util.delayDisplay("Showing only in view 1")
- if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 2")
- # self.printViewNodeIDs(displayNode)
- return False
- if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget is not visible in view 1")
- # self.printViewNodeIDs(displayNode)
- return False
-
- # switch back to conventional
- lm.setLayout(2)
- slicer.util.delayDisplay("Switched back to conventional layout")
- # self.printViewAndSliceNodes()
-
- # test of the visibility in slice views
- displayNode.RemoveAllViewNodeIDs()
-
- # jump to the last control point
- slicer.modules.markups.logic().JumpSlicesToNthPointInMarkup(fidNode.GetID(), controlPointIndex, True)
- # refocus the 3D cameras as well
- slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex)
-
- # show only in red
- displayNode.AddViewNodeID('vtkMRMLSliceNodeRed')
- slicer.util.delayDisplay("Show only in red slice")
- if not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex):
- slicer.util.delayDisplay("Test failed: widget not displayed on red slice")
- # self.printViewNodeIDs(displayNode)
- return False
-
- # remove all, add green
- # print 'before remove all, after added red'
- # self.printViewNodeIDs(displayNode)
- displayNode.RemoveAllViewNodeIDs()
- # print 'after removed all'
- # self.printViewNodeIDs(displayNode)
- displayNode.AddViewNodeID('vtkMRMLSliceNodeGreen')
- slicer.util.delayDisplay('Show only in green slice')
- if (self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex)
- or not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex)):
- slicer.util.delayDisplay("Test failed: widget not displayed only on green slice")
- print('\tred = ', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex))
- print('\tgreen =', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex))
- self.printViewNodeIDs(displayNode)
- return False
-
- return True
+ def controlPointVisible3D(self, fidNode, viewNodeID, controlPointIndex):
+ lm = slicer.app.layoutManager()
+ for v in range(lm.threeDViewCount):
+ td = lm.threeDWidget(v)
+ if td.viewLogic().GetViewNode().GetID() != viewNodeID:
+ continue
+ td.threeDView().forceRender()
+ slicer.app.processEvents()
+ ms = vtk.vtkCollection()
+ td.getDisplayableManagers(ms)
+ for i in range(ms.GetNumberOfItems()):
+ m = ms.GetItemAsObject(i)
+ if m.GetClassName() == "vtkMRMLMarkupsDisplayableManager":
+ markupsWidget = m.GetWidget(fidNode.GetDisplayNode())
+ return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex)
+ return False
+
+ def controlPointVisibleSlice(self, fidNode, sliceNodeID, controlPointIndex):
+ lm = slicer.app.layoutManager()
+ sliceNames = lm.sliceViewNames()
+ for sliceName in sliceNames:
+ sliceWidget = lm.sliceWidget(sliceName)
+ sliceView = sliceWidget.sliceView()
+ sliceNode = sliceView.mrmlSliceNode()
+ if sliceNode.GetID() != sliceNodeID:
+ continue
+ sliceView.forceRender()
+ slicer.app.processEvents()
+ ms = vtk.vtkCollection()
+ sliceView.getDisplayableManagers(ms)
+ for i in range(ms.GetNumberOfItems()):
+ m = ms.GetItemAsObject(i)
+ if m.GetClassName() == 'vtkMRMLMarkupsDisplayableManager':
+ markupsWidget = m.GetWidget(fidNode.GetDisplayNode())
+ return markupsWidget.GetMarkupsRepresentation().GetNthControlPointViewVisibility(controlPointIndex)
+ return False
+
+ def printViewNodeIDs(self, displayNode):
+ numIDs = displayNode.GetNumberOfViewNodeIDs()
+ if numIDs == 0:
+ print('No view node ids for display node', displayNode.GetID())
+ return
+ print('View node ids for display node', displayNode.GetID())
+ for i in range(numIDs):
+ id = displayNode.GetNthViewNodeID(i)
+ print(id)
+
+ def printViewAndSliceNodes(self):
+ numViewNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLViewNode')
+ print('Number of view nodes = ', numViewNodes)
+ for vn in range(numViewNodes):
+ viewNode = slicer.mrmlScene.GetNthNodeByClass(vn, 'vtkMRMLViewNode')
+ print('\t', viewNode.GetName(), "id =", viewNode.GetID())
+
+ numSliceNodes = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSliceNode')
+ print('Number of slice nodes = ', numSliceNodes)
+ for sn in range(numSliceNodes):
+ sliceNode = slicer.mrmlScene.GetNthNodeByClass(sn, 'vtkMRMLSliceNode')
+ print('\t', sliceNode.GetName(), "id =", sliceNode.GetID())
+
+ def onRecordNodeEvent(self, caller, event, eventId):
+ self.nodeEvents.append(eventId)
+
+ def run(self):
+ """
+ Run the actual algorithm
+ """
+ print('Running test of the markups in different views')
+
+ #
+ # first load the data
+ #
+ print("Getting MR Head Volume")
+ import SampleData
+ mrHeadVolume = SampleData.downloadSample("MRHead")
+
+ #
+ # link the viewers
+ #
+ sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ compositeNode.SetLinkedControl(1)
+
+ #
+ # MR Head in the background
+ #
+ sliceLogic.StartSliceCompositeNodeInteraction(1)
+ compositeNode.SetBackgroundVolumeID(mrHeadVolume.GetID())
+ sliceLogic.EndSliceCompositeNodeInteraction()
+
+ #
+ # switch to conventional layout
+ #
+ lm = slicer.app.layoutManager()
+ lm.setLayout(2)
+
+ # create a control points list
+ fidNode = slicer.vtkMRMLMarkupsFiducialNode()
+ slicer.mrmlScene.AddNode(fidNode)
+ fidNode.CreateDefaultDisplayNodes()
+ displayNode = fidNode.GetDisplayNode()
+
+ # make it active
+ selectionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLSelectionNodeSingleton")
+ if (selectionNode is not None):
+ selectionNode.SetReferenceActivePlaceNodeID(fidNode.GetID())
+
+ fidNodeObserverTags = []
+ self.nodeEvents = []
+ observedEvents = [
+ slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent,
+ slicer.vtkMRMLMarkupsNode.PointPositionUndefinedEvent]
+ for eventId in observedEvents:
+ fidNodeObserverTags.append(fidNode.AddObserver(eventId, lambda caller, event, eventId=eventId: self.onRecordNodeEvent(caller, event, eventId)))
+
+ # add some known points to it
+ eye1 = [33.4975, 79.4042, -10.2143]
+ eye2 = [-31.283, 80.9652, -16.2143]
+ nose = [4.61944, 114.526, -33.2143]
+ controlPointIndex = fidNode.AddControlPoint(eye1)
+ slicer.nodeEvents = self.nodeEvents
+ assert(len(self.nodeEvents) == 1)
+ assert(self.nodeEvents[0] == slicer.vtkMRMLMarkupsNode.PointPositionDefinedEvent)
+ fidNode.SetNthControlPointLabel(controlPointIndex, "eye-1")
+ controlPointIndex = fidNode.AddControlPoint(eye2)
+ fidNode.SetNthControlPointLabel(controlPointIndex, "eye-2")
+ # hide the second eye as a test of visibility flags
+ fidNode.SetNthControlPointVisibility(controlPointIndex, controlPointIndex)
+ controlPointIndex = fidNode.AddControlPoint(nose)
+ fidNode.SetNthControlPointLabel(controlPointIndex, "nose")
+
+ for tag in fidNodeObserverTags:
+ fidNode.RemoveObserver(tag)
+
+ slicer.util.delayDisplay("Placed 3 control points")
+
+ # self.printViewAndSliceNodes()
+
+ if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget is not visible in view 1")
+ # self.printViewNodeIDs(displayNode)
+ return False
+
+ #
+ # switch to 2 3D views layout
+ #
+ lm.setLayout(15)
+ slicer.util.delayDisplay("Switched to 2 3D views")
+ # self.printViewAndSliceNodes()
+
+ controlPointIndex = 0
+
+ slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex)
+
+ if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex)
+ or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)):
+ slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2")
+ # self.printViewNodeIDs(displayNode)
+ return False
+
+ #
+ # show only in view 2
+ #
+ displayNode.AddViewNodeID("vtkMRMLViewNode2")
+ slicer.util.delayDisplay("Showing only in view 2")
+ if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 1")
+ # self.printViewNodeIDs(displayNode)
+ return False
+ if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget is not visible in view 2")
+ # self.printViewNodeIDs(displayNode)
+ return False
+
+ #
+ # remove it so show in all
+ #
+ displayNode.RemoveAllViewNodeIDs()
+ slicer.util.delayDisplay("Showing in both views")
+ if (not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex)
+ or not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex)):
+ slicer.util.delayDisplay("Test failed: widget is not visible in view 1 and 2")
+ self.printViewNodeIDs(displayNode)
+ return False
+
+ #
+ # show only in view 1
+ #
+ displayNode.AddViewNodeID("vtkMRMLViewNode1")
+ slicer.util.delayDisplay("Showing only in view 1")
+ if self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode2', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget is not supposed to be visible in view 2")
+ # self.printViewNodeIDs(displayNode)
+ return False
+ if not self.controlPointVisible3D(fidNode, 'vtkMRMLViewNode1', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget is not visible in view 1")
+ # self.printViewNodeIDs(displayNode)
+ return False
+
+ # switch back to conventional
+ lm.setLayout(2)
+ slicer.util.delayDisplay("Switched back to conventional layout")
+ # self.printViewAndSliceNodes()
+
+ # test of the visibility in slice views
+ displayNode.RemoveAllViewNodeIDs()
+
+ # jump to the last control point
+ slicer.modules.markups.logic().JumpSlicesToNthPointInMarkup(fidNode.GetID(), controlPointIndex, True)
+ # refocus the 3D cameras as well
+ slicer.modules.markups.logic().FocusCamerasOnNthPointInMarkup(fidNode.GetID(), controlPointIndex)
+
+ # show only in red
+ displayNode.AddViewNodeID('vtkMRMLSliceNodeRed')
+ slicer.util.delayDisplay("Show only in red slice")
+ if not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex):
+ slicer.util.delayDisplay("Test failed: widget not displayed on red slice")
+ # self.printViewNodeIDs(displayNode)
+ return False
+
+ # remove all, add green
+ # print 'before remove all, after added red'
+ # self.printViewNodeIDs(displayNode)
+ displayNode.RemoveAllViewNodeIDs()
+ # print 'after removed all'
+ # self.printViewNodeIDs(displayNode)
+ displayNode.AddViewNodeID('vtkMRMLSliceNodeGreen')
+ slicer.util.delayDisplay('Show only in green slice')
+ if (self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex)
+ or not self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex)):
+ slicer.util.delayDisplay("Test failed: widget not displayed only on green slice")
+ print('\tred = ', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeRed', controlPointIndex))
+ print('\tgreen =', self.controlPointVisibleSlice(fidNode, 'vtkMRMLSliceNodeGreen', controlPointIndex))
+ self.printViewNodeIDs(displayNode)
+ return False
+
+ return True
class MarkupsInViewsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_MarkupsInViewsSelfTest1()
- def test_MarkupsInViewsSelfTest1(self):
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_MarkupsInViewsSelfTest1()
+
+ def test_MarkupsInViewsSelfTest1(self):
- self.delayDisplay("Starting the Markups in viewers test")
+ self.delayDisplay("Starting the Markups in viewers test")
- # start in the Markups module
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('Markups')
+ # start in the Markups module
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('Markups')
- logic = MarkupsInViewsSelfTestLogic()
- retval = logic.run()
+ logic = MarkupsInViewsSelfTestLogic()
+ retval = logic.run()
- if retval == True:
- self.delayDisplay('Test passed!')
- else:
- self.delayDisplay('Test failed!')
+ if retval == True:
+ self.delayDisplay('Test passed!')
+ else:
+ self.delayDisplay('Test failed!')
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py b/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py
index b8729769676..4b410cd0f9c 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsMeasurementsTest.py
@@ -15,7 +15,7 @@
markupsMeasurementsTestDir = slicer.app.temporaryPath + '/markupsMeasurementsTest'
print('Test directory: ', markupsMeasurementsTestDir)
if not os.access(markupsMeasurementsTestDir, os.F_OK):
- os.mkdir(markupsMeasurementsTestDir)
+ os.mkdir(markupsMeasurementsTestDir)
preserveFiles = True
@@ -33,10 +33,10 @@
measurement = markupsNode.GetMeasurement("length")
if abs(measurement.GetValue() - length) > 1e-4:
- raise Exception("Unexpected length value: " + str(measurement.GetValue()))
+ raise Exception("Unexpected length value: " + str(measurement.GetValue()))
if measurement.GetValueWithUnitsAsPrintableString() != '34.12mm':
- raise Exception("Unexpected length measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
+ raise Exception("Unexpected length measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
markupsFilename = markupsMeasurementsTestDir + '/line.mkp.json'
slicer.util.saveNode(markupsNode, markupsFilename)
@@ -46,7 +46,7 @@
result = [{'name': 'length', 'enabled': True, 'value': 34.12, 'units': 'mm', 'printFormat': '%-#4.4gmm'}]
if markupsJson['markups'][0]['measurements'] != result:
- raise Exception("Unexpected length measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
+ raise Exception("Unexpected length measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
if not preserveFiles:
os.remove(markupsFilename)
@@ -62,10 +62,10 @@
measurement = markupsNode.GetMeasurement("angle")
if abs(measurement.GetValue() - 117.4) > 0.1:
- raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
+ raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
if measurement.GetValueWithUnitsAsPrintableString() != '117.4deg':
- raise Exception("Unexpected angle measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
+ raise Exception("Unexpected angle measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
markupsFilename = markupsMeasurementsTestDir + '/angle.mkp.json'
slicer.util.saveNode(markupsNode, markupsFilename)
@@ -75,20 +75,20 @@
result = [{'name': 'angle', 'enabled': True, 'value': 117.36891896165277, 'units': 'deg', 'printFormat': '%3.1f%s'}]
if markupsJson['markups'][0]['measurements'] != result:
- raise Exception("Unexpected angle measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
+ raise Exception("Unexpected angle measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
markupsNode.SetAngleMeasurementModeToOrientedPositive()
measurement = markupsNode.GetMeasurement("angle")
if abs(measurement.GetValue() - 242.6) > 0.1:
- raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
+ raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
markupsNode.SetAngleMeasurementModeToOrientedSigned()
measurement = markupsNode.GetMeasurement("angle")
if abs(measurement.GetValue() - (-117.36891896165277)) > 0.1:
- raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
+ raise Exception("Unexpected angle value: " + str(measurement.GetValue()))
if not preserveFiles:
- os.remove(markupsFilename)
+ os.remove(markupsFilename)
#
# Plane
@@ -103,7 +103,7 @@
measurement = markupsNode.GetMeasurement("area")
measurement.SetEnabled(True)
if measurement.GetValueWithUnitsAsPrintableString() != '32.00cm2':
- raise Exception("Unexpected area measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
+ raise Exception("Unexpected area measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
markupsFilename = markupsMeasurementsTestDir + '/plane.mkp.json'
slicer.util.saveNode(markupsNode, markupsFilename)
@@ -113,10 +113,10 @@
result = [{'name': 'area', 'enabled': True, 'value': 32.00000000000001, 'units': 'cm2', 'printFormat': '%-#4.4gcm2'}]
if markupsJson['markups'][0]['measurements'] != result:
- raise Exception("Unexpected area measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
+ raise Exception("Unexpected area measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
if not preserveFiles:
- os.remove(markupsFilename)
+ os.remove(markupsFilename)
#
# ROI
@@ -129,7 +129,7 @@
measurement = markupsNode.GetMeasurement("volume")
measurement.SetEnabled(True)
if measurement.GetValueWithUnitsAsPrintableString() != '24.000cm3':
- raise Exception("Unexpected volume measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
+ raise Exception("Unexpected volume measurement result: " + measurement.GetValueWithUnitsAsPrintableString())
markupsFilename = markupsMeasurementsTestDir + '/roi.mkp.json'
slicer.util.saveNode(markupsNode, markupsFilename)
@@ -139,9 +139,9 @@
result = [{'name': 'volume', 'enabled': True, 'value': 24.000000000000007, 'units': 'cm3', 'printFormat': '%-#4.5gcm3'}]
if markupsJson['markups'][0]['measurements'] != result:
- raise Exception("Unexpected volume measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
+ raise Exception("Unexpected volume measurement result in file: " + str(markupsJson['markups'][0]['measurements']))
if not preserveFiles:
- os.remove(markupsFilename)
+ os.remove(markupsFilename)
print('Markups computation is verified successfully')
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py
index 23977fa9f6f..a20ca0ba044 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestManyLists.py
@@ -8,20 +8,20 @@
coords = [0.0, 0.0, 0.0]
numFidsInList1 = 5
for i in range(numFidsInList1):
- fidNode1.AddControlPoint(coords)
- coords[0] += 1.0
- coords[1] += 2.0
- coords[2] += 1.0
+ fidNode1.AddControlPoint(coords)
+ coords[0] += 1.0
+ coords[1] += 2.0
+ coords[2] += 1.0
# second control point list
fidNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", "FidNode2")
fidNode2.CreateDefaultDisplayNodes()
numFidsInList2 = 10
for i in range(numFidsInList2):
- fidNode2.AddControlPoint(coords)
- coords[0] += 1.0
- coords[1] += 1.0
- coords[2] += 3.0
+ fidNode2.AddControlPoint(coords)
+ coords[0] += 1.0
+ coords[1] += 1.0
+ coords[2] += 3.0
# Create scene view
numFidNodesBeforeStore = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode')
@@ -34,37 +34,37 @@
fidNode3.CreateDefaultDisplayNodes()
numFidsInList3 = 2
for i in range(numFidsInList3):
- fidNode3.AddControlPoint(coords)
- coords[0] += 1.0
- coords[1] += 2.0
- coords[2] += 3.0
+ fidNode3.AddControlPoint(coords)
+ coords[0] += 1.0
+ coords[1] += 2.0
+ coords[2] += 3.0
# Restore scene view
sv.RestoreScene()
numFidNodesAfterRestore = slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode')
if numFidNodesAfterRestore != numFidNodesBeforeStore:
- print("After restoring the scene, expected ", numFidNodesBeforeStore, " control points nodes, but have ", numFidNodesAfterRestore)
- exceptionMessage = "After restoring the scene, expected " + str(numFidNodesBeforeStore) + " control points nodes, but have " + str(numFidNodesAfterRestore)
- raise Exception(exceptionMessage)
+ print("After restoring the scene, expected ", numFidNodesBeforeStore, " control points nodes, but have ", numFidNodesAfterRestore)
+ exceptionMessage = "After restoring the scene, expected " + str(numFidNodesBeforeStore) + " control points nodes, but have " + str(numFidNodesAfterRestore)
+ raise Exception(exceptionMessage)
fid1AfterRestore = slicer.mrmlScene.GetFirstNodeByName("FidNode1")
numFidsInList1AfterRestore = fid1AfterRestore.GetNumberOfControlPoints()
print("After restore, list with name FidNode1 has id ", fid1AfterRestore.GetID(), " and num fids = ", numFidsInList1AfterRestore)
if numFidsInList1AfterRestore != numFidsInList1:
- exceptionMessage = "After restoring list 1, id = " + fid1AfterRestore.GetID()
- exceptionMessage += ", expected " + str(numFidsInList1) + " but got "
- exceptionMessage += str(numFidsInList1AfterRestore)
- raise Exception(exceptionMessage)
+ exceptionMessage = "After restoring list 1, id = " + fid1AfterRestore.GetID()
+ exceptionMessage += ", expected " + str(numFidsInList1) + " but got "
+ exceptionMessage += str(numFidsInList1AfterRestore)
+ raise Exception(exceptionMessage)
fid2AfterRestore = slicer.mrmlScene.GetFirstNodeByName("FidNode2")
numFidsInList2AfterRestore = fid2AfterRestore.GetNumberOfControlPoints()
print("After restore, list with name FidNode2 has id ", fid2AfterRestore.GetID(), " and num fids = ", numFidsInList2AfterRestore)
if numFidsInList2AfterRestore != numFidsInList2:
- exceptionMessage = "After restoring list 2, id = " + fid2AfterRestore.GetID()
- exceptionMessage += ", expected " + str(numFidsInList2) + " but got "
- exceptionMessage += str(numFidsInList2AfterRestore)
- raise Exception(exceptionMessage)
+ exceptionMessage = "After restoring list 2, id = " + fid2AfterRestore.GetID()
+ exceptionMessage += ", expected " + str(numFidsInList2) + " but got "
+ exceptionMessage += str(numFidsInList2AfterRestore)
+ raise Exception(exceptionMessage)
# check the displayable manager for the right number of widgets/seeds
lm = slicer.app.layoutManager()
@@ -74,29 +74,29 @@
print('Helper = ', h)
for markupsNode in [fid1AfterRestore, fid2AfterRestore]:
- markupsWidget = h.GetWidget(markupsNode)
- rep = markupsWidget.GetRepresentation()
- controlPointsPoly = rep.GetControlPointsPolyData(rep.Selected)
- numberOfControlPoints = controlPointsPoly.GetNumberOfPoints()
- print(f"Markups widget {markupsNode.GetName()} has number of control points = {numberOfControlPoints}")
- if numberOfControlPoints != markupsNode.GetNumberOfControlPoints():
- exceptionMessage = "After restoring " + markupsNode.GetName() + ", expected widget to have "
- exceptionMessage += str(markupsNode.GetNumberOfControlPoints()) + " points, but it has "
- exceptionMessage += str(numberOfControlPoints)
- raise Exception(exceptionMessage)
- # check positions
- for s in range(markupsNode.GetNumberOfControlPoints()):
- worldPos = controlPointsPoly.GetPoint(s)
- print("control point ", s, " world position = ", worldPos)
- fidPos = [0.0, 0.0, 0.0]
- markupsNode.GetNthControlPointPosition(s, fidPos)
- xdiff = fidPos[0] - worldPos[0]
- ydiff = fidPos[1] - worldPos[1]
- zdiff = fidPos[2] - worldPos[2]
- diffTotal = xdiff + ydiff + zdiff
- if diffTotal > 0.1:
- exceptionMessage = markupsNode.GetName() + ": Difference between control point position " + str(s)
- exceptionMessage += " and representation point position totals = " + str(diffTotal)
- raise Exception(exceptionMessage)
- # Release reference to VTK widget, otherwise application could crash on exit
- del markupsWidget
+ markupsWidget = h.GetWidget(markupsNode)
+ rep = markupsWidget.GetRepresentation()
+ controlPointsPoly = rep.GetControlPointsPolyData(rep.Selected)
+ numberOfControlPoints = controlPointsPoly.GetNumberOfPoints()
+ print(f"Markups widget {markupsNode.GetName()} has number of control points = {numberOfControlPoints}")
+ if numberOfControlPoints != markupsNode.GetNumberOfControlPoints():
+ exceptionMessage = "After restoring " + markupsNode.GetName() + ", expected widget to have "
+ exceptionMessage += str(markupsNode.GetNumberOfControlPoints()) + " points, but it has "
+ exceptionMessage += str(numberOfControlPoints)
+ raise Exception(exceptionMessage)
+ # check positions
+ for s in range(markupsNode.GetNumberOfControlPoints()):
+ worldPos = controlPointsPoly.GetPoint(s)
+ print("control point ", s, " world position = ", worldPos)
+ fidPos = [0.0, 0.0, 0.0]
+ markupsNode.GetNthControlPointPosition(s, fidPos)
+ xdiff = fidPos[0] - worldPos[0]
+ ydiff = fidPos[1] - worldPos[1]
+ zdiff = fidPos[2] - worldPos[2]
+ diffTotal = xdiff + ydiff + zdiff
+ if diffTotal > 0.1:
+ exceptionMessage = markupsNode.GetName() + ": Difference between control point position " + str(s)
+ exceptionMessage += " and representation point position totals = " + str(diffTotal)
+ raise Exception(exceptionMessage)
+ # Release reference to VTK widget, otherwise application could crash on exit
+ del markupsWidget
diff --git a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py
index 8ef954996d2..a603874f230 100644
--- a/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py
+++ b/Modules/Loadable/Markups/Testing/Python/MarkupsSceneViewRestoreTestSimple.py
@@ -42,5 +42,5 @@
diffTotal = xdiff + ydiff + zdiff
if diffTotal > 0.1:
- exceptionMessage = "Difference between coordinate values total = " + str(diffTotal)
- raise Exception(exceptionMessage)
+ exceptionMessage = "Difference between coordinate values total = " + str(diffTotal)
+ raise Exception(exceptionMessage)
diff --git a/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py
index db7ee0ff6e6..4eeb403fe1f 100644
--- a/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/NeurosurgicalPlanningTutorialMarkupsSelfTest.py
@@ -13,14 +13,14 @@
#
class NeurosurgicalPlanningTutorialMarkupsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "NeurosurgicalPlanningTutorialMarkupsSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = ["Segmentations"]
- self.parent.contributors = ["Nicole Aucoin (BWH), Andras Lasso (PerkLab, Queen's)"]
- self.parent.helpText = """This is a test case that exercises the fiducials used in the Neurosurgical Planning tutorial."""
- parent.acknowledgementText = """This file was originally developed by Nicole Aucoin, BWH
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "NeurosurgicalPlanningTutorialMarkupsSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = ["Segmentations"]
+ self.parent.contributors = ["Nicole Aucoin (BWH), Andras Lasso (PerkLab, Queen's)"]
+ self.parent.helpText = """This is a test case that exercises the fiducials used in the Neurosurgical Planning tutorial."""
+ parent.acknowledgementText = """This file was originally developed by Nicole Aucoin, BWH
and was partially funded by NIH grant 3P41RR013218-12S1. The test was updated to use Segment editor by
Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program
of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
@@ -31,62 +31,62 @@ def __init__(self, parent):
#
class NeurosurgicalPlanningTutorialMarkupsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Instantiate and connect widgets ...
-
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
-
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- #
- # check box to trigger taking screen shots for later use in tutorials
- #
- self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
- self.enableScreenshotsFlagCheckBox.checked = 0
- self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
- parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
-
- #
- # scale factor for screen shots
- #
- self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget()
- self.screenshotScaleFactorSliderWidget.singleStep = 1.0
- self.screenshotScaleFactorSliderWidget.minimum = 1.0
- self.screenshotScaleFactorSliderWidget.maximum = 50.0
- self.screenshotScaleFactorSliderWidget.value = 1.0
- self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.")
- parametersFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget)
-
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
-
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- def cleanup(self):
- pass
-
- def onApplyButton(self):
- logging.debug("Execute logic.run() method")
- logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic()
- logic.enableScreenshots = self.enableScreenshotsFlagCheckBox.checked
- logic.screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value)
- logic.run()
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Instantiate and connect widgets ...
+
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
+
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ #
+ # check box to trigger taking screen shots for later use in tutorials
+ #
+ self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
+ self.enableScreenshotsFlagCheckBox.checked = 0
+ self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
+ parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
+
+ #
+ # scale factor for screen shots
+ #
+ self.screenshotScaleFactorSliderWidget = ctk.ctkSliderWidget()
+ self.screenshotScaleFactorSliderWidget.singleStep = 1.0
+ self.screenshotScaleFactorSliderWidget.minimum = 1.0
+ self.screenshotScaleFactorSliderWidget.maximum = 50.0
+ self.screenshotScaleFactorSliderWidget.value = 1.0
+ self.screenshotScaleFactorSliderWidget.setToolTip("Set scale factor for the screen shots.")
+ parametersFormLayout.addRow("Screenshot scale factor", self.screenshotScaleFactorSliderWidget)
+
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
+
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ def cleanup(self):
+ pass
+
+ def onApplyButton(self):
+ logging.debug("Execute logic.run() method")
+ logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic()
+ logic.enableScreenshots = self.enableScreenshotsFlagCheckBox.checked
+ logic.screenshotScaleFactor = int(self.screenshotScaleFactorSliderWidget.value)
+ logic.run()
#
@@ -94,295 +94,295 @@ def onApplyButton(self):
#
class NeurosurgicalPlanningTutorialMarkupsSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
-
- def __init__(self):
- ScriptedLoadableModuleLogic.__init__(self)
-
- #
- # for the red slice widget, convert the background volume's RAS
- # coordinates to display coordinates for painting
- #
- def rasToDisplay(self, r, a, s):
- displayCoords = [0, 0, 0, 1]
-
- # get the slice node
- lm = slicer.app.layoutManager()
- sliceWidget = lm.sliceWidget('Red')
- sliceLogic = sliceWidget.sliceLogic()
- sliceNode = sliceLogic.GetSliceNode()
-
- xyToRASMatrix = sliceNode.GetXYToRAS()
- rasToXyMatrix = vtk.vtkMatrix4x4()
- rasToXyMatrix.Invert(xyToRASMatrix, rasToXyMatrix)
-
- worldCoords = [r, a, s, 1.0]
- rasToXyMatrix.MultiplyPoint(worldCoords, displayCoords)
-
- return (int(displayCoords[0]), int(displayCoords[1]))
-
-
-class NeurosurgicalPlanningTutorialMarkupsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
- # reset to conventional layout
- lm = slicer.app.layoutManager()
- lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
"""
- self.setUp()
- self.test_NeurosurgicalPlanningTutorialMarkupsSelfTest1()
-
- def test_NeurosurgicalPlanningTutorialMarkupsSelfTest1(self):
-
- self.delayDisplay("Starting the Neurosurgical Planning Tutorial Markups test")
-
- # start in the welcome module
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('Welcome')
-
- logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic()
- self.delayDisplay('Running test of the Neurosurgical Planning tutorial')
-
- # conventional layout
- lm = slicer.app.layoutManager()
- lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
- #
- # first load the data
- #
- if self.enableScreenshots:
- # for the tutorial, do it through the welcome module
- slicer.util.selectModule('Welcome')
- self.delayDisplay("Screenshot")
- self.takeScreenshot('NeurosurgicalPlanning-Welcome', 'Welcome module')
- else:
- # otherwise show the sample data module
- slicer.util.selectModule('SampleData')
-
- # use the sample data module logic to load data for the self test
- self.delayDisplay("Getting Baseline volume")
- import SampleData
- baselineVolume = SampleData.downloadSample('BaselineVolume')
-
- self.takeScreenshot('NeurosurgicalPlanning-Loaded', 'Data loaded')
+ def __init__(self):
+ ScriptedLoadableModuleLogic.__init__(self)
#
- # link the viewers
+ # for the red slice widget, convert the background volume's RAS
+ # coordinates to display coordinates for painting
#
+ def rasToDisplay(self, r, a, s):
+ displayCoords = [0, 0, 0, 1]
- if self.enableScreenshots:
- # for the tutorial, pop up the linking control
- sliceController = slicer.app.layoutManager().sliceWidget("Red").sliceController()
- popupWidget = sliceController.findChild("ctkPopupWidget")
- if popupWidget is not None:
- popupWidget.pinPopup(1)
- self.takeScreenshot('NeurosurgicalPlanning-Link', 'Link slice viewers')
- popupWidget.pinPopup(0)
+ # get the slice node
+ lm = slicer.app.layoutManager()
+ sliceWidget = lm.sliceWidget('Red')
+ sliceLogic = sliceWidget.sliceLogic()
+ sliceNode = sliceLogic.GetSliceNode()
- sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
- compositeNode = sliceLogic.GetSliceCompositeNode()
- compositeNode.SetLinkedControl(1)
+ xyToRASMatrix = sliceNode.GetXYToRAS()
+ rasToXyMatrix = vtk.vtkMatrix4x4()
+ rasToXyMatrix.Invert(xyToRASMatrix, rasToXyMatrix)
- #
- # baseline in the background
- #
- sliceLogic.StartSliceCompositeNodeInteraction(1)
- compositeNode.SetBackgroundVolumeID(baselineVolume.GetID())
- slicer.app.processEvents()
- sliceLogic.FitSliceToAll()
- sliceLogic.EndSliceCompositeNodeInteraction()
- self.takeScreenshot('NeurosurgicalPlanning-Baseline', 'Baseline in background')
+ worldCoords = [r, a, s, 1.0]
+ rasToXyMatrix.MultiplyPoint(worldCoords, displayCoords)
- #
- # adjust window level on baseline
- #
- slicer.util.selectModule('Volumes')
- baselineDisplay = baselineVolume.GetDisplayNode()
- baselineDisplay.SetAutoWindowLevel(0)
- baselineDisplay.SetWindow(2600)
- baselineDisplay.SetLevel(1206)
- self.takeScreenshot('NeurosurgicalPlanning-WindowLevel', 'Set W/L on baseline')
+ return (int(displayCoords[0]), int(displayCoords[1]))
- #
- # switch to red slice only
- #
- lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)
- slicer.app.processEvents()
- sliceLogic.FitSliceToAll()
- self.takeScreenshot('NeurosurgicalPlanning-RedSliceOnly', 'Set layout to Red Slice only')
-
- #
- # segmentation of tumor
- #
- # Create segmentation
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", baselineVolume.GetName() + '-segmentation')
- segmentationNode.CreateDefaultDisplayNodes()
- segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(baselineVolume)
-
- #
- # segment editor module
- #
- slicer.util.selectModule('SegmentEditor')
- segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
- segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
- segmentEditorNode.SetAndObserveSegmentationNode(segmentationNode)
- segmentEditorNode.SetAndObserveMasterVolumeNode(baselineVolume)
-
- # create segments
- region1SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-cystic")
- region2SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-solid")
- backgroundSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Background")
- ventriclesSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Ventricles")
- segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
- # Make segmentation results visible in 3D
- segmentationNode.CreateClosedSurfaceRepresentation()
-
- self.takeScreenshot('NeurosurgicalPlanning-Editor', 'Showing Editor Module')
-
- # set the slice offset so drawing is right
- sliceNode = sliceLogic.GetSliceNode()
- sliceOffset = 58.7
- sliceNode.SetSliceOffset(sliceOffset)
-
- #
- # paint
- #
- segmentEditorWidget.setActiveEffectByName("Paint")
- paintEffect = segmentEditorWidget.activeEffect()
- paintEffect.setParameter("BrushDiameterIsRelative", 0)
- paintEffect.setParameter("BrushAbsoluteDiameter", 4.0)
-
- self.takeScreenshot('NeurosurgicalPlanning-Paint', 'Paint tool in Editor Module')
-
- #
- # paint in cystic part of tumor, using conversion from RAS coords to
- # avoid slice widget size differences
- #
- segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
-
- clickCoordsList = [
- [-7.4, 71, sliceOffset],
- [-11, 73, sliceOffset],
- [-12, 85, sliceOffset],
- [-13, 91, sliceOffset],
- [-15, 78, sliceOffset]]
- sliceWidget = lm.sliceWidget('Red')
- currentCoords = None
- for clickCoords in clickCoordsList:
- if currentCoords:
- slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=10)
- currentCoords = clickCoords
-
- self.takeScreenshot('NeurosurgicalPlanning-PaintCystic', 'Paint cystic part of tumor')
-
- #
- # paint in solid part of tumor
- #
- segmentEditorNode.SetSelectedSegmentID(region2SegmentId)
- slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(-0.5, 118.5, sliceOffset), end=logic.rasToDisplay(-7.4, 116, sliceOffset), steps=10)
- self.takeScreenshot('NeurosurgicalPlanning-PaintSolid', 'Paint solid part of tumor')
-
- #
- # paint around the tumor
- #
- segmentEditorNode.SetSelectedSegmentID(backgroundSegmentId)
- clickCoordsList = [
- [-40, 50, sliceOffset],
- [30, 50, sliceOffset],
- [30, 145, sliceOffset],
- [-40, 145, sliceOffset],
- [-40, 50, sliceOffset]]
- sliceWidget = lm.sliceWidget('Red')
- currentCoords = None
- for clickCoords in clickCoordsList:
- if currentCoords:
- slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=30)
- currentCoords = clickCoords
- self.takeScreenshot('NeurosurgicalPlanning-PaintAround', 'Paint around tumor')
-
- #
- # Grow cut
- #
- segmentEditorWidget.setActiveEffectByName("Grow from seeds")
- effect = segmentEditorWidget.activeEffect()
- effect.self().onPreview()
- effect.self().onApply()
- self.takeScreenshot('NeurosurgicalPlanning-Growcut', 'Growcut')
-
- # segmentationNode.RemoveSegment(backgroundSegmentId)
-
- #
- # go to the data module
- #
- slicer.util.selectModule('Data')
- self.takeScreenshot('NeurosurgicalPlanning-GrowCutData', 'GrowCut segmentation results in Data')
-
- #
- # Ventricles Segmentation
- #
-
- slicer.util.selectModule('SegmentEditor')
-
- segmentEditorNode.SetSelectedSegmentID(ventriclesSegmentId)
- segmentEditorNode.SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteNone)
-
- # Thresholding
- segmentEditorWidget.setActiveEffectByName("Threshold")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("MinimumThreshold", "1700")
- # effect.setParameter("MaximumThreshold","695")
- effect.self().onApply()
- self.takeScreenshot('NeurosurgicalPlanning-Ventricles', 'Ventricles segmentation')
-
- #
- # Save Islands
- #
- segmentEditorWidget.setActiveEffectByName("Islands")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("Operation", "KEEP_SELECTED_ISLAND")
- slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(25.3, 5.8, sliceOffset), end=logic.rasToDisplay(25.3, 5.8, sliceOffset), steps=1)
- self.takeScreenshot('NeurosurgicalPlanning-SaveIsland', 'Ventricles save island')
-
- #
- # switch to conventional layout
- #
- lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
- self.takeScreenshot('NeurosurgicalPlanning-MergeAndBuild', 'Merged and built models')
-
- #
- # Smoothing
- #
- segmentEditorNode.SetSelectedSegmentID(region2SegmentId)
- segmentEditorWidget.setActiveEffectByName("Smoothing")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("SmoothingMethod", "MEDIAN")
- effect.setParameter("KernelSizeMm", 5)
- effect.self().onApply()
- self.takeScreenshot('NeurosurgicalPlanning-Smoothed', 'Smoothed cystic region')
+class NeurosurgicalPlanningTutorialMarkupsSelfTestTest(ScriptedLoadableModuleTest):
+ """
+ This is the test case for your scripted module.
+ """
- #
- # Dilation
- #
- segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
- segmentEditorWidget.setActiveEffectByName("Margin")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("MarginSizeMm", 3.0)
- effect.self().onApply()
- self.takeScreenshot('NeurosurgicalPlanning-Dilated', 'Dilated tumor')
-
- self.delayDisplay('Test passed!')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+ # reset to conventional layout
+ lm = slicer.app.layoutManager()
+ lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_NeurosurgicalPlanningTutorialMarkupsSelfTest1()
+
+ def test_NeurosurgicalPlanningTutorialMarkupsSelfTest1(self):
+
+ self.delayDisplay("Starting the Neurosurgical Planning Tutorial Markups test")
+
+ # start in the welcome module
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('Welcome')
+
+ logic = NeurosurgicalPlanningTutorialMarkupsSelfTestLogic()
+ self.delayDisplay('Running test of the Neurosurgical Planning tutorial')
+
+ # conventional layout
+ lm = slicer.app.layoutManager()
+ lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
+
+ #
+ # first load the data
+ #
+ if self.enableScreenshots:
+ # for the tutorial, do it through the welcome module
+ slicer.util.selectModule('Welcome')
+ self.delayDisplay("Screenshot")
+ self.takeScreenshot('NeurosurgicalPlanning-Welcome', 'Welcome module')
+ else:
+ # otherwise show the sample data module
+ slicer.util.selectModule('SampleData')
+
+ # use the sample data module logic to load data for the self test
+ self.delayDisplay("Getting Baseline volume")
+ import SampleData
+ baselineVolume = SampleData.downloadSample('BaselineVolume')
+
+ self.takeScreenshot('NeurosurgicalPlanning-Loaded', 'Data loaded')
+
+ #
+ # link the viewers
+ #
+
+ if self.enableScreenshots:
+ # for the tutorial, pop up the linking control
+ sliceController = slicer.app.layoutManager().sliceWidget("Red").sliceController()
+ popupWidget = sliceController.findChild("ctkPopupWidget")
+ if popupWidget is not None:
+ popupWidget.pinPopup(1)
+ self.takeScreenshot('NeurosurgicalPlanning-Link', 'Link slice viewers')
+ popupWidget.pinPopup(0)
+
+ sliceLogic = slicer.app.layoutManager().sliceWidget('Red').sliceLogic()
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ compositeNode.SetLinkedControl(1)
+
+ #
+ # baseline in the background
+ #
+ sliceLogic.StartSliceCompositeNodeInteraction(1)
+ compositeNode.SetBackgroundVolumeID(baselineVolume.GetID())
+ slicer.app.processEvents()
+ sliceLogic.FitSliceToAll()
+ sliceLogic.EndSliceCompositeNodeInteraction()
+ self.takeScreenshot('NeurosurgicalPlanning-Baseline', 'Baseline in background')
+
+ #
+ # adjust window level on baseline
+ #
+ slicer.util.selectModule('Volumes')
+ baselineDisplay = baselineVolume.GetDisplayNode()
+ baselineDisplay.SetAutoWindowLevel(0)
+ baselineDisplay.SetWindow(2600)
+ baselineDisplay.SetLevel(1206)
+ self.takeScreenshot('NeurosurgicalPlanning-WindowLevel', 'Set W/L on baseline')
+
+ #
+ # switch to red slice only
+ #
+ lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)
+ slicer.app.processEvents()
+ sliceLogic.FitSliceToAll()
+ self.takeScreenshot('NeurosurgicalPlanning-RedSliceOnly', 'Set layout to Red Slice only')
+
+ #
+ # segmentation of tumor
+ #
+
+ # Create segmentation
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", baselineVolume.GetName() + '-segmentation')
+ segmentationNode.CreateDefaultDisplayNodes()
+ segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(baselineVolume)
+
+ #
+ # segment editor module
+ #
+ slicer.util.selectModule('SegmentEditor')
+ segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
+ segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
+ segmentEditorNode.SetAndObserveSegmentationNode(segmentationNode)
+ segmentEditorNode.SetAndObserveMasterVolumeNode(baselineVolume)
+
+ # create segments
+ region1SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-cystic")
+ region2SegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Tumor-solid")
+ backgroundSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Background")
+ ventriclesSegmentId = segmentationNode.GetSegmentation().AddEmptySegment("Ventricles")
+ segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
+ # Make segmentation results visible in 3D
+ segmentationNode.CreateClosedSurfaceRepresentation()
+
+ self.takeScreenshot('NeurosurgicalPlanning-Editor', 'Showing Editor Module')
+
+ # set the slice offset so drawing is right
+ sliceNode = sliceLogic.GetSliceNode()
+ sliceOffset = 58.7
+ sliceNode.SetSliceOffset(sliceOffset)
+
+ #
+ # paint
+ #
+ segmentEditorWidget.setActiveEffectByName("Paint")
+ paintEffect = segmentEditorWidget.activeEffect()
+ paintEffect.setParameter("BrushDiameterIsRelative", 0)
+ paintEffect.setParameter("BrushAbsoluteDiameter", 4.0)
+
+ self.takeScreenshot('NeurosurgicalPlanning-Paint', 'Paint tool in Editor Module')
+
+ #
+ # paint in cystic part of tumor, using conversion from RAS coords to
+ # avoid slice widget size differences
+ #
+ segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
+
+ clickCoordsList = [
+ [-7.4, 71, sliceOffset],
+ [-11, 73, sliceOffset],
+ [-12, 85, sliceOffset],
+ [-13, 91, sliceOffset],
+ [-15, 78, sliceOffset]]
+ sliceWidget = lm.sliceWidget('Red')
+ currentCoords = None
+ for clickCoords in clickCoordsList:
+ if currentCoords:
+ slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=10)
+ currentCoords = clickCoords
+
+ self.takeScreenshot('NeurosurgicalPlanning-PaintCystic', 'Paint cystic part of tumor')
+
+ #
+ # paint in solid part of tumor
+ #
+ segmentEditorNode.SetSelectedSegmentID(region2SegmentId)
+ slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(-0.5, 118.5, sliceOffset), end=logic.rasToDisplay(-7.4, 116, sliceOffset), steps=10)
+ self.takeScreenshot('NeurosurgicalPlanning-PaintSolid', 'Paint solid part of tumor')
+
+ #
+ # paint around the tumor
+ #
+ segmentEditorNode.SetSelectedSegmentID(backgroundSegmentId)
+ clickCoordsList = [
+ [-40, 50, sliceOffset],
+ [30, 50, sliceOffset],
+ [30, 145, sliceOffset],
+ [-40, 145, sliceOffset],
+ [-40, 50, sliceOffset]]
+ sliceWidget = lm.sliceWidget('Red')
+ currentCoords = None
+ for clickCoords in clickCoordsList:
+ if currentCoords:
+ slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(*currentCoords), end=logic.rasToDisplay(*clickCoords), steps=30)
+ currentCoords = clickCoords
+ self.takeScreenshot('NeurosurgicalPlanning-PaintAround', 'Paint around tumor')
+
+ #
+ # Grow cut
+ #
+ segmentEditorWidget.setActiveEffectByName("Grow from seeds")
+ effect = segmentEditorWidget.activeEffect()
+ effect.self().onPreview()
+ effect.self().onApply()
+ self.takeScreenshot('NeurosurgicalPlanning-Growcut', 'Growcut')
+
+ # segmentationNode.RemoveSegment(backgroundSegmentId)
+
+ #
+ # go to the data module
+ #
+ slicer.util.selectModule('Data')
+ self.takeScreenshot('NeurosurgicalPlanning-GrowCutData', 'GrowCut segmentation results in Data')
+
+ #
+ # Ventricles Segmentation
+ #
+
+ slicer.util.selectModule('SegmentEditor')
+
+ segmentEditorNode.SetSelectedSegmentID(ventriclesSegmentId)
+ segmentEditorNode.SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteNone)
+
+ # Thresholding
+ segmentEditorWidget.setActiveEffectByName("Threshold")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("MinimumThreshold", "1700")
+ # effect.setParameter("MaximumThreshold","695")
+ effect.self().onApply()
+ self.takeScreenshot('NeurosurgicalPlanning-Ventricles', 'Ventricles segmentation')
+
+ #
+ # Save Islands
+ #
+ segmentEditorWidget.setActiveEffectByName("Islands")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("Operation", "KEEP_SELECTED_ISLAND")
+ slicer.util.clickAndDrag(sliceWidget, start=logic.rasToDisplay(25.3, 5.8, sliceOffset), end=logic.rasToDisplay(25.3, 5.8, sliceOffset), steps=1)
+ self.takeScreenshot('NeurosurgicalPlanning-SaveIsland', 'Ventricles save island')
+
+ #
+ # switch to conventional layout
+ #
+ lm.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
+ self.takeScreenshot('NeurosurgicalPlanning-MergeAndBuild', 'Merged and built models')
+
+ #
+ # Smoothing
+ #
+ segmentEditorNode.SetSelectedSegmentID(region2SegmentId)
+ segmentEditorWidget.setActiveEffectByName("Smoothing")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("SmoothingMethod", "MEDIAN")
+ effect.setParameter("KernelSizeMm", 5)
+ effect.self().onApply()
+ self.takeScreenshot('NeurosurgicalPlanning-Smoothed', 'Smoothed cystic region')
+
+ #
+ # Dilation
+ #
+ segmentEditorNode.SetSelectedSegmentID(region1SegmentId)
+ segmentEditorWidget.setActiveEffectByName("Margin")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("MarginSizeMm", 3.0)
+ effect.self().onApply()
+ self.takeScreenshot('NeurosurgicalPlanning-Dilated', 'Dilated tumor')
+
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py b/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py
index a1115d88510..9125874539f 100644
--- a/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py
+++ b/Modules/Loadable/Markups/Testing/Python/PluggableMarkupsSelfTest.py
@@ -11,18 +11,18 @@
# PluggableMarkupsSelfTest
#
class PluggableMarkupsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "PluggableMarkupsSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = []
- self.parent.contributors = ["Rafael Palomar (OUS)"]
- self.parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "PluggableMarkupsSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Rafael Palomar (OUS)"]
+ self.parent.helpText = """
This is a test case for the markups pluggable architecture.
It unregisters the markups provided by the Markups module and
registers them again.
"""
- self.parent.acknowledgementText = """
+ self.parent.acknowledgementText = """
This file was originally developed by Rafael Palomar (OUS) and was funded by
the Research Council of Norway (grant nr. 311393).
"""
@@ -33,240 +33,240 @@ def __init__(self, parent):
#
class PluggableMarkupsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the test."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the test."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- logging.debug("Execute logic.run() method")
- logic = PluggableMarkupsSelfTestLogic()
- logic.run()
+ def onApplyButton(self):
+ logging.debug("Execute logic.run() method")
+ logic = PluggableMarkupsSelfTestLogic()
+ logic.run()
class PluggableMarkupsSelfTestLogic(ScriptedLoadableModuleLogic):
- def __init__(self):
- ScriptedLoadableModuleLogic.__init__(self)
-
- def setUp(self):
- #
- # Step 1: Register all available markups nodes
- #
-
- markupsWidget = slicer.modules.markups.widgetRepresentation()
- if markupsWidget is None:
- raise Exception("Couldn't get the Markups module widget")
-
- markupsLogic = slicer.modules.markups.logic()
- if markupsLogic is None:
- raise Exception("Couldn't get the Markups module logic")
-
- markupsNodes = self.markupsNodes()
-
- # Check Markups module standard nodes are registered
- for markupNode in markupsNodes:
- markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode])
-
- #
- # Step 2: Register all available additional options widgets
- #
- additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
- for additionalOptionsWidget in self.additionalOptionsWidgets():
- additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget)
-
- def __checkPushButtonExists(self, widget, name):
- pushButtonObjectName = "Create%sPushButton" % name
- # slicer.util.delayDisplay("Checking whether '%s' exists" % pushButtonObjectName)
- if widget.findChild(qt.QPushButton, pushButtonObjectName):
- return True
- return False
-
- def __checkWidgetExists(self, widget, name):
- # slicer.util.delayDisplay("Checking whether '%s' exists" % name)
- if widget.findChild(qt.QWidget, name):
- return True
- return False
-
- def __checkWidgetVisibility(self, widget, name):
- # slicer.util.delayDisplay("Checking whether '%s' is visible" % pushButtonObjectName)
- w = widget.findChild(qt.QWidget, name)
- return w.isVisible()
-
- def markupsNodes(self):
- return {
- slicer.vtkMRMLMarkupsAngleNode(): slicer.vtkSlicerAngleWidget(),
- slicer.vtkMRMLMarkupsClosedCurveNode(): slicer.vtkSlicerCurveWidget(),
- slicer.vtkMRMLMarkupsCurveNode(): slicer.vtkSlicerCurveWidget(),
- slicer.vtkMRMLMarkupsFiducialNode(): slicer.vtkSlicerPointsWidget(),
- slicer.vtkMRMLMarkupsLineNode(): slicer.vtkSlicerLineWidget(),
- slicer.vtkMRMLMarkupsPlaneNode(): slicer.vtkSlicerPlaneWidget(),
- slicer.vtkMRMLMarkupsROINode(): slicer.vtkSlicerROIWidget(),
- slicer.vtkMRMLMarkupsTestLineNode(): slicer.vtkSlicerTestLineWidget()
- }
-
- def additionalOptionsWidgets(self):
- return [
- slicer.qMRMLMarkupsCurveSettingsWidget(),
- slicer.qMRMLMarkupsAngleMeasurementsWidget(),
- slicer.qMRMLMarkupsPlaneWidget(),
- slicer.qMRMLMarkupsROIWidget(),
- slicer.qMRMLMarkupsTestLineWidget()
- ]
-
- def test_unregister_existing_markups(self):
- """
- This unregisters existing registered markups
- """
-
- markupsWidget = slicer.modules.markups.widgetRepresentation()
- if markupsWidget is None:
- raise Exception("Couldn't get the Markups module widget")
-
- # Check Markups module standard nodes are registered
- for markupNode in self.markupsNodes():
- if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None:
- raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType())
-
- markupsLogic = slicer.modules.markups.logic()
- if markupsLogic is None:
- raise Exception("Couldn't get the Markups module logic")
-
- # Unregister Markups and check the buttons are gone
- for markupNode in self.markupsNodes():
- slicer.util.delayDisplay("Unregistering %s" % markupNode.GetMarkupType())
- markupsLogic.UnregisterMarkupsNode(markupNode)
- if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()):
- raise Exception("Create PushButton for %s is present after unregistration" % markupNode.GetMarkupType())
-
- def test_register_markups(self):
- """
- This registers all known markups
- """
- markupsWidget = slicer.modules.markups.widgetRepresentation()
- if markupsWidget is None:
- raise Exception("Couldn't get the Markups module widget")
-
- markupsLogic = slicer.modules.markups.logic()
- if markupsLogic is None:
- raise Exception("Couldn't get the Markups module logic")
-
- markupsNodes = self.markupsNodes()
-
- # Check Markups module standard nodes are registered
- for markupNode in markupsNodes:
- slicer.util.delayDisplay("Registering %s" % markupNode.GetMarkupType())
- markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode])
- if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None:
- raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType())
-
- def test_unregister_additional_options_widgets(self):
- """
- This unregisters all the additional options widgets
- """
- markupsWidget = slicer.modules.markups.widgetRepresentation()
- if markupsWidget is None:
- raise Exception("Couldn't get the Markups module widget")
-
- additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
- for additionalOptionsWidget in self.additionalOptionsWidgets():
+ def __init__(self):
+ ScriptedLoadableModuleLogic.__init__(self)
+
+ def setUp(self):
+ #
+ # Step 1: Register all available markups nodes
+ #
+
+ markupsWidget = slicer.modules.markups.widgetRepresentation()
+ if markupsWidget is None:
+ raise Exception("Couldn't get the Markups module widget")
+
+ markupsLogic = slicer.modules.markups.logic()
+ if markupsLogic is None:
+ raise Exception("Couldn't get the Markups module logic")
+
+ markupsNodes = self.markupsNodes()
+
+ # Check Markups module standard nodes are registered
+ for markupNode in markupsNodes:
+ markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode])
+
+ #
+ # Step 2: Register all available additional options widgets
+ #
+ additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
+ for additionalOptionsWidget in self.additionalOptionsWidgets():
+ additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget)
+
+ def __checkPushButtonExists(self, widget, name):
+ pushButtonObjectName = "Create%sPushButton" % name
+ # slicer.util.delayDisplay("Checking whether '%s' exists" % pushButtonObjectName)
+ if widget.findChild(qt.QPushButton, pushButtonObjectName):
+ return True
+ return False
+
+ def __checkWidgetExists(self, widget, name):
+ # slicer.util.delayDisplay("Checking whether '%s' exists" % name)
+ if widget.findChild(qt.QWidget, name):
+ return True
+ return False
+
+ def __checkWidgetVisibility(self, widget, name):
+ # slicer.util.delayDisplay("Checking whether '%s' is visible" % pushButtonObjectName)
+ w = widget.findChild(qt.QWidget, name)
+ return w.isVisible()
+
+ def markupsNodes(self):
+ return {
+ slicer.vtkMRMLMarkupsAngleNode(): slicer.vtkSlicerAngleWidget(),
+ slicer.vtkMRMLMarkupsClosedCurveNode(): slicer.vtkSlicerCurveWidget(),
+ slicer.vtkMRMLMarkupsCurveNode(): slicer.vtkSlicerCurveWidget(),
+ slicer.vtkMRMLMarkupsFiducialNode(): slicer.vtkSlicerPointsWidget(),
+ slicer.vtkMRMLMarkupsLineNode(): slicer.vtkSlicerLineWidget(),
+ slicer.vtkMRMLMarkupsPlaneNode(): slicer.vtkSlicerPlaneWidget(),
+ slicer.vtkMRMLMarkupsROINode(): slicer.vtkSlicerROIWidget(),
+ slicer.vtkMRMLMarkupsTestLineNode(): slicer.vtkSlicerTestLineWidget()
+ }
+
+ def additionalOptionsWidgets(self):
+ return [
+ slicer.qMRMLMarkupsCurveSettingsWidget(),
+ slicer.qMRMLMarkupsAngleMeasurementsWidget(),
+ slicer.qMRMLMarkupsPlaneWidget(),
+ slicer.qMRMLMarkupsROIWidget(),
+ slicer.qMRMLMarkupsTestLineWidget()
+ ]
+
+ def test_unregister_existing_markups(self):
+ """
+ This unregisters existing registered markups
+ """
+
+ markupsWidget = slicer.modules.markups.widgetRepresentation()
+ if markupsWidget is None:
+ raise Exception("Couldn't get the Markups module widget")
+
+ # Check Markups module standard nodes are registered
+ for markupNode in self.markupsNodes():
+ if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None:
+ raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType())
+
+ markupsLogic = slicer.modules.markups.logic()
+ if markupsLogic is None:
+ raise Exception("Couldn't get the Markups module logic")
+
+ # Unregister Markups and check the buttons are gone
+ for markupNode in self.markupsNodes():
+ slicer.util.delayDisplay("Unregistering %s" % markupNode.GetMarkupType())
+ markupsLogic.UnregisterMarkupsNode(markupNode)
+ if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()):
+ raise Exception("Create PushButton for %s is present after unregistration" % markupNode.GetMarkupType())
+
+ def test_register_markups(self):
+ """
+ This registers all known markups
+ """
+ markupsWidget = slicer.modules.markups.widgetRepresentation()
+ if markupsWidget is None:
+ raise Exception("Couldn't get the Markups module widget")
+
+ markupsLogic = slicer.modules.markups.logic()
+ if markupsLogic is None:
+ raise Exception("Couldn't get the Markups module logic")
+
+ markupsNodes = self.markupsNodes()
+
+ # Check Markups module standard nodes are registered
+ for markupNode in markupsNodes:
+ slicer.util.delayDisplay("Registering %s" % markupNode.GetMarkupType())
+ markupsLogic.RegisterMarkupsNode(markupNode, markupsNodes[markupNode])
+ if self.__checkPushButtonExists(markupsWidget, markupNode.GetMarkupType()) is None:
+ raise Exception("Create PushButton for %s is not present" % markupNode.GetMarkupType())
+
+ def test_unregister_additional_options_widgets(self):
+ """
+ This unregisters all the additional options widgets
+ """
+ markupsWidget = slicer.modules.markups.widgetRepresentation()
+ if markupsWidget is None:
+ raise Exception("Couldn't get the Markups module widget")
+
+ additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
+ for additionalOptionsWidget in self.additionalOptionsWidgets():
+
+ # Check the widget exists
+ if not self.__checkWidgetExists(markupsWidget, additionalOptionsWidget.objectName):
+ raise Exception("%s does not exist" % additionalOptionsWidget.objectName)
+
+ # NOTE: since the widget will get destroyed, we take note of the name for the checking step
+ objectName = additionalOptionsWidget.objectName
+
+ # Unregister widget
+ additionalOptionsWidgetsFactory.unregisterOptionsWidget(additionalOptionsWidget.className)
+
+ # Check the widget does not exist
+ if self.__checkWidgetExists(markupsWidget, objectName):
+ raise Exception("%s does still exist" % objectName)
+
+ def test_register_additional_options_widgets(self):
+ """
+ This reigisters additional options widgets
+ """
+
+ additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
+
+ markupsWidget = slicer.modules.markups.widgetRepresentation()
+ if markupsWidget is None:
+ raise Exception("Couldn't get the Markups module widget")
+
+ for additionalOptionsWidget in self.additionalOptionsWidgets():
+ name = additionalOptionsWidget.objectName
+ slicer.util.delayDisplay("Registering %s" % additionalOptionsWidget.objectName)
+ additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget)
+
+ # Check the widget exists
+ if not self.__checkWidgetExists(markupsWidget, name):
+ raise Exception("%s does not exist" % additionalOptionsWidget.objectName)
+
+ def run(self):
+ """
+ Run the tests
+ """
+ slicer.util.delayDisplay('Running integration tests for Pluggable Markups')
+
+ self.test_unregister_existing_markups()
+ self.test_register_markups()
+ # self.test_unregister_additional_options_widgets()
+ self.test_register_additional_options_widgets()
+
+ logging.info('Process completed')
- # Check the widget exists
- if not self.__checkWidgetExists(markupsWidget, additionalOptionsWidget.objectName):
- raise Exception("%s does not exist" % additionalOptionsWidget.objectName)
- # NOTE: since the widget will get destroyed, we take note of the name for the checking step
- objectName = additionalOptionsWidget.objectName
-
- # Unregister widget
- additionalOptionsWidgetsFactory.unregisterOptionsWidget(additionalOptionsWidget.className)
-
- # Check the widget does not exist
- if self.__checkWidgetExists(markupsWidget, objectName):
- raise Exception("%s does still exist" % objectName)
-
- def test_register_additional_options_widgets(self):
- """
- This reigisters additional options widgets
- """
-
- additionalOptionsWidgetsFactory = slicer.qMRMLMarkupsOptionsWidgetsFactory().instance()
-
- markupsWidget = slicer.modules.markups.widgetRepresentation()
- if markupsWidget is None:
- raise Exception("Couldn't get the Markups module widget")
-
- for additionalOptionsWidget in self.additionalOptionsWidgets():
- name = additionalOptionsWidget.objectName
- slicer.util.delayDisplay("Registering %s" % additionalOptionsWidget.objectName)
- additionalOptionsWidgetsFactory.registerOptionsWidget(additionalOptionsWidget)
-
- # Check the widget exists
- if not self.__checkWidgetExists(markupsWidget, name):
- raise Exception("%s does not exist" % additionalOptionsWidget.objectName)
-
- def run(self):
+class PluggableMarkupsSelfTestTest(ScriptedLoadableModuleTest):
"""
- Run the tests
+ This is the test case
"""
- slicer.util.delayDisplay('Running integration tests for Pluggable Markups')
-
- self.test_unregister_existing_markups()
- self.test_register_markups()
- # self.test_unregister_additional_options_widgets()
- self.test_register_additional_options_widgets()
-
- logging.info('Process completed')
-
-
-class PluggableMarkupsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case
- """
- def setUp(self):
- logic = PluggableMarkupsSelfTestLogic()
- logic.setUp()
+ def setUp(self):
+ logic = PluggableMarkupsSelfTestLogic()
+ logic.setUp()
- def runTest(self):
- self.setUp()
- self.test_PluggableMarkupsSelfTest1()
+ def runTest(self):
+ self.setUp()
+ self.test_PluggableMarkupsSelfTest1()
- def test_PluggableMarkupsSelfTest1(self):
+ def test_PluggableMarkupsSelfTest1(self):
- self.delayDisplay("Starting the Pluggable Markups Test")
+ self.delayDisplay("Starting the Pluggable Markups Test")
- # Open the markups module
- slicer.util.mainWindow().moduleSelector().selectModule('Markups')
- self.delayDisplay('In Markups module')
+ # Open the markups module
+ slicer.util.mainWindow().moduleSelector().selectModule('Markups')
+ self.delayDisplay('In Markups module')
- logic = PluggableMarkupsSelfTestLogic()
- logic.run()
+ logic = PluggableMarkupsSelfTestLogic()
+ logic.run()
- self.delayDisplay('Test passed!')
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py b/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py
index e6bc5c0aef2..95d41823312 100644
--- a/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py
+++ b/Modules/Loadable/Markups/Widgets/Testing/Python/MarkupsWidgetsSelfTest.py
@@ -9,14 +9,14 @@
#
class MarkupsWidgetsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "MarkupsWidgetsSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = ["Markups"]
- self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
- self.parent.helpText = """This is a self test for Markups widgets."""
- self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "MarkupsWidgetsSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = ["Markups"]
+ self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
+ self.parent.helpText = """This is a self test for Markups widgets."""
+ self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
#
@@ -24,8 +24,8 @@ def __init__(self, parent):
#
class MarkupsWidgetsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
#
@@ -33,170 +33,170 @@ def setup(self):
#
class MarkupsWidgetsSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
+ """
- def __init__(self):
- pass
+ def __init__(self):
+ pass
class MarkupsWidgetsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- self.delayMs = 700
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_MarkupsWidgetsSelfTest_FullTest1()
-
- # ------------------------------------------------------------------------------
- def test_MarkupsWidgetsSelfTest_FullTest1(self):
- # Check for Tables module
- self.assertTrue(slicer.modules.tables)
-
- self.section_SetupPathsAndNames()
- self.section_CreateMarkups()
- self.section_SimpleMarkupsWidget()
- self.section_MarkupsPlaceWidget()
- self.delayDisplay("Test passed", self.delayMs)
-
- # ------------------------------------------------------------------------------
- def section_SetupPathsAndNames(self):
- # Set constants
- self.sampleMarkupsNodeName1 = 'SampleMarkups1'
- self.sampleMarkupsNodeName2 = 'SampleMarkups2'
-
- # ------------------------------------------------------------------------------
- def section_CreateMarkups(self):
- self.delayDisplay("Create markup nodes", self.delayMs)
-
- self.markupsLogic = slicer.modules.markups.logic()
-
- # Create sample markups node
- self.markupsNode1 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode())
- self.markupsNode1.SetName(self.sampleMarkupsNodeName1)
-
- self.markupsNode2 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode())
- self.markupsNode2.SetName(self.sampleMarkupsNodeName2)
-
- # ------------------------------------------------------------------------------
- def section_SimpleMarkupsWidget(self):
- self.delayDisplay("Test SimpleMarkupsWidget", self.delayMs)
-
- simpleMarkupsWidget = slicer.qSlicerSimpleMarkupsWidget()
- nodeSelector = slicer.util.findChildren(simpleMarkupsWidget, "MarkupsNodeComboBox")[0]
- self.assertIsNone(simpleMarkupsWidget.interactionNode())
- simpleMarkupsWidget.setMRMLScene(slicer.mrmlScene)
- simpleMarkupsWidget.show()
-
- placeWidget = simpleMarkupsWidget.markupsPlaceWidget()
- self.assertIsNotNone(placeWidget)
-
- simpleMarkupsWidget.setCurrentNode(None)
- simpleMarkupsWidget.enterPlaceModeOnNodeChange = False
- placeWidget.placeModeEnabled = False
- nodeSelector.setCurrentNode(self.markupsNode1)
- self.assertFalse(placeWidget.placeModeEnabled)
-
- simpleMarkupsWidget.enterPlaceModeOnNodeChange = True
- nodeSelector.setCurrentNode(self.markupsNode2)
- self.assertTrue(placeWidget.placeModeEnabled)
-
- simpleMarkupsWidget.jumpToSliceEnabled = True
- self.assertTrue(simpleMarkupsWidget.jumpToSliceEnabled)
- simpleMarkupsWidget.jumpToSliceEnabled = False
- self.assertFalse(simpleMarkupsWidget.jumpToSliceEnabled)
-
- simpleMarkupsWidget.nodeSelectorVisible = False
- self.assertFalse(simpleMarkupsWidget.nodeSelectorVisible)
- simpleMarkupsWidget.nodeSelectorVisible = True
- self.assertTrue(simpleMarkupsWidget.nodeSelectorVisible)
-
- simpleMarkupsWidget.optionsVisible = False
- self.assertFalse(simpleMarkupsWidget.optionsVisible)
- simpleMarkupsWidget.optionsVisible = True
- self.assertTrue(simpleMarkupsWidget.optionsVisible)
-
- defaultColor = qt.QColor(0, 255, 0)
- simpleMarkupsWidget.defaultNodeColor = defaultColor
- self.assertEqual(simpleMarkupsWidget.defaultNodeColor, defaultColor)
-
- self.markupsNode3 = nodeSelector.addNode()
- displayNode3 = self.markupsNode3.GetDisplayNode()
- color3 = displayNode3.GetColor()
- self.assertEqual(color3[0] * 255, defaultColor.red())
- self.assertEqual(color3[1] * 255, defaultColor.green())
- self.assertEqual(color3[2] * 255, defaultColor.blue())
-
- numberOfFiducialsAdded = 5
- for i in range(numberOfFiducialsAdded):
- self.markupsNode3.AddControlPoint([i * 20, i * 15, i * 5])
-
- tableWidget = simpleMarkupsWidget.tableWidget()
- self.assertEqual(tableWidget.rowCount, numberOfFiducialsAdded)
-
- self.assertEqual(simpleMarkupsWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode())
- otherInteractionNode = slicer.vtkMRMLInteractionNode()
- otherInteractionNode.SetSingletonOff()
- slicer.mrmlScene.AddNode(otherInteractionNode)
- simpleMarkupsWidget.setInteractionNode(otherInteractionNode)
- self.assertEqual(simpleMarkupsWidget.interactionNode(), otherInteractionNode)
-
- # ------------------------------------------------------------------------------
- def section_MarkupsPlaceWidget(self):
- self.delayDisplay("Test MarkupsPlaceWidget", self.delayMs)
-
- placeWidget = slicer.qSlicerMarkupsPlaceWidget()
- self.assertIsNone(placeWidget.interactionNode())
- placeWidget.setMRMLScene(slicer.mrmlScene)
- placeWidget.setCurrentNode(self.markupsNode1)
- placeWidget.show()
-
- placeWidget.buttonsVisible = False
- self.assertFalse(placeWidget.buttonsVisible)
- placeWidget.buttonsVisible = True
- self.assertTrue(placeWidget.buttonsVisible)
-
- placeWidget.deleteAllControlPointsOptionVisible = False
- self.assertFalse(placeWidget.deleteAllControlPointsOptionVisible)
- placeWidget.deleteAllControlPointsOptionVisible = True
- self.assertTrue(placeWidget.deleteAllControlPointsOptionVisible)
-
- placeWidget.unsetLastControlPointOptionVisible = False
- self.assertFalse(placeWidget.unsetLastControlPointOptionVisible)
- placeWidget.unsetLastControlPointOptionVisible = True
- self.assertTrue(placeWidget.unsetLastControlPointOptionVisible)
-
- placeWidget.unsetAllControlPointsOptionVisible = False
- self.assertFalse(placeWidget.unsetAllControlPointsOptionVisible)
- placeWidget.unsetAllControlPointsOptionVisible = True
- self.assertTrue(placeWidget.unsetAllControlPointsOptionVisible)
-
- placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceSingleMarkup
- placeWidget.placeModeEnabled = True
- self.assertFalse(placeWidget.placeModePersistency)
-
- placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceMultipleMarkups
- placeWidget.placeModeEnabled = False
- placeWidget.placeModeEnabled = True
- self.assertTrue(placeWidget.placeModePersistency)
-
- self.assertEqual(placeWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode())
- otherInteractionNode = slicer.vtkMRMLInteractionNode()
- otherInteractionNode.SetSingletonOff()
- slicer.mrmlScene.AddNode(otherInteractionNode)
- placeWidget.setInteractionNode(otherInteractionNode)
- self.assertEqual(placeWidget.interactionNode(), otherInteractionNode)
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ self.delayMs = 700
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_MarkupsWidgetsSelfTest_FullTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_MarkupsWidgetsSelfTest_FullTest1(self):
+ # Check for Tables module
+ self.assertTrue(slicer.modules.tables)
+
+ self.section_SetupPathsAndNames()
+ self.section_CreateMarkups()
+ self.section_SimpleMarkupsWidget()
+ self.section_MarkupsPlaceWidget()
+ self.delayDisplay("Test passed", self.delayMs)
+
+ # ------------------------------------------------------------------------------
+ def section_SetupPathsAndNames(self):
+ # Set constants
+ self.sampleMarkupsNodeName1 = 'SampleMarkups1'
+ self.sampleMarkupsNodeName2 = 'SampleMarkups2'
+
+ # ------------------------------------------------------------------------------
+ def section_CreateMarkups(self):
+ self.delayDisplay("Create markup nodes", self.delayMs)
+
+ self.markupsLogic = slicer.modules.markups.logic()
+
+ # Create sample markups node
+ self.markupsNode1 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode())
+ self.markupsNode1.SetName(self.sampleMarkupsNodeName1)
+
+ self.markupsNode2 = slicer.mrmlScene.GetNodeByID(self.markupsLogic.AddNewFiducialNode())
+ self.markupsNode2.SetName(self.sampleMarkupsNodeName2)
+
+ # ------------------------------------------------------------------------------
+ def section_SimpleMarkupsWidget(self):
+ self.delayDisplay("Test SimpleMarkupsWidget", self.delayMs)
+
+ simpleMarkupsWidget = slicer.qSlicerSimpleMarkupsWidget()
+ nodeSelector = slicer.util.findChildren(simpleMarkupsWidget, "MarkupsNodeComboBox")[0]
+ self.assertIsNone(simpleMarkupsWidget.interactionNode())
+ simpleMarkupsWidget.setMRMLScene(slicer.mrmlScene)
+ simpleMarkupsWidget.show()
+
+ placeWidget = simpleMarkupsWidget.markupsPlaceWidget()
+ self.assertIsNotNone(placeWidget)
+
+ simpleMarkupsWidget.setCurrentNode(None)
+ simpleMarkupsWidget.enterPlaceModeOnNodeChange = False
+ placeWidget.placeModeEnabled = False
+ nodeSelector.setCurrentNode(self.markupsNode1)
+ self.assertFalse(placeWidget.placeModeEnabled)
+
+ simpleMarkupsWidget.enterPlaceModeOnNodeChange = True
+ nodeSelector.setCurrentNode(self.markupsNode2)
+ self.assertTrue(placeWidget.placeModeEnabled)
+
+ simpleMarkupsWidget.jumpToSliceEnabled = True
+ self.assertTrue(simpleMarkupsWidget.jumpToSliceEnabled)
+ simpleMarkupsWidget.jumpToSliceEnabled = False
+ self.assertFalse(simpleMarkupsWidget.jumpToSliceEnabled)
+
+ simpleMarkupsWidget.nodeSelectorVisible = False
+ self.assertFalse(simpleMarkupsWidget.nodeSelectorVisible)
+ simpleMarkupsWidget.nodeSelectorVisible = True
+ self.assertTrue(simpleMarkupsWidget.nodeSelectorVisible)
+
+ simpleMarkupsWidget.optionsVisible = False
+ self.assertFalse(simpleMarkupsWidget.optionsVisible)
+ simpleMarkupsWidget.optionsVisible = True
+ self.assertTrue(simpleMarkupsWidget.optionsVisible)
+
+ defaultColor = qt.QColor(0, 255, 0)
+ simpleMarkupsWidget.defaultNodeColor = defaultColor
+ self.assertEqual(simpleMarkupsWidget.defaultNodeColor, defaultColor)
+
+ self.markupsNode3 = nodeSelector.addNode()
+ displayNode3 = self.markupsNode3.GetDisplayNode()
+ color3 = displayNode3.GetColor()
+ self.assertEqual(color3[0] * 255, defaultColor.red())
+ self.assertEqual(color3[1] * 255, defaultColor.green())
+ self.assertEqual(color3[2] * 255, defaultColor.blue())
+
+ numberOfFiducialsAdded = 5
+ for i in range(numberOfFiducialsAdded):
+ self.markupsNode3.AddControlPoint([i * 20, i * 15, i * 5])
+
+ tableWidget = simpleMarkupsWidget.tableWidget()
+ self.assertEqual(tableWidget.rowCount, numberOfFiducialsAdded)
+
+ self.assertEqual(simpleMarkupsWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode())
+ otherInteractionNode = slicer.vtkMRMLInteractionNode()
+ otherInteractionNode.SetSingletonOff()
+ slicer.mrmlScene.AddNode(otherInteractionNode)
+ simpleMarkupsWidget.setInteractionNode(otherInteractionNode)
+ self.assertEqual(simpleMarkupsWidget.interactionNode(), otherInteractionNode)
+
+ # ------------------------------------------------------------------------------
+ def section_MarkupsPlaceWidget(self):
+ self.delayDisplay("Test MarkupsPlaceWidget", self.delayMs)
+
+ placeWidget = slicer.qSlicerMarkupsPlaceWidget()
+ self.assertIsNone(placeWidget.interactionNode())
+ placeWidget.setMRMLScene(slicer.mrmlScene)
+ placeWidget.setCurrentNode(self.markupsNode1)
+ placeWidget.show()
+
+ placeWidget.buttonsVisible = False
+ self.assertFalse(placeWidget.buttonsVisible)
+ placeWidget.buttonsVisible = True
+ self.assertTrue(placeWidget.buttonsVisible)
+
+ placeWidget.deleteAllControlPointsOptionVisible = False
+ self.assertFalse(placeWidget.deleteAllControlPointsOptionVisible)
+ placeWidget.deleteAllControlPointsOptionVisible = True
+ self.assertTrue(placeWidget.deleteAllControlPointsOptionVisible)
+
+ placeWidget.unsetLastControlPointOptionVisible = False
+ self.assertFalse(placeWidget.unsetLastControlPointOptionVisible)
+ placeWidget.unsetLastControlPointOptionVisible = True
+ self.assertTrue(placeWidget.unsetLastControlPointOptionVisible)
+
+ placeWidget.unsetAllControlPointsOptionVisible = False
+ self.assertFalse(placeWidget.unsetAllControlPointsOptionVisible)
+ placeWidget.unsetAllControlPointsOptionVisible = True
+ self.assertTrue(placeWidget.unsetAllControlPointsOptionVisible)
+
+ placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceSingleMarkup
+ placeWidget.placeModeEnabled = True
+ self.assertFalse(placeWidget.placeModePersistency)
+
+ placeWidget.placeMultipleMarkups = slicer.qSlicerMarkupsPlaceWidget.ForcePlaceMultipleMarkups
+ placeWidget.placeModeEnabled = False
+ placeWidget.placeModeEnabled = True
+ self.assertTrue(placeWidget.placeModePersistency)
+
+ self.assertEqual(placeWidget.interactionNode(), slicer.app.applicationLogic().GetInteractionNode())
+ otherInteractionNode = slicer.vtkMRMLInteractionNode()
+ otherInteractionNode.SetSingletonOff()
+ slicer.mrmlScene.AddNode(otherInteractionNode)
+ placeWidget.setInteractionNode(otherInteractionNode)
+ self.assertEqual(placeWidget.interactionNode(), otherInteractionNode)
diff --git a/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py b/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py
index ff6c42dfd3c..7e1616e7e1d 100644
--- a/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py
+++ b/Modules/Loadable/Plots/Testing/Python/PlotsSelfTest.py
@@ -9,14 +9,14 @@
#
class PlotsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "PlotsSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = ["Plots"]
- self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
- self.parent.helpText = """This is a self test for plot nodes and widgets."""
- parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "PlotsSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = ["Plots"]
+ self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
+ self.parent.helpText = """This is a self test for plot nodes and widgets."""
+ parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University
and was supported through Canada CANARIE's Research Software Program."""
@@ -25,8 +25,8 @@ def __init__(self, parent):
#
class PlotsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
#
@@ -34,141 +34,141 @@ def setup(self):
#
class PlotsSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
+ """
- def __init__(self):
- pass
+ def __init__(self):
+ pass
class PlotsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_PlotsSelfTest_FullTest1()
-
- # ------------------------------------------------------------------------------
- def test_PlotsSelfTest_FullTest1(self):
- # Check for Plots module
- self.assertTrue(slicer.modules.plots)
-
- self.section_SetupPathsAndNames()
- self.section_CreateTable()
- self.section_CreatePlots()
- self.section_TestPlotView()
- self.delayDisplay("Test passed")
-
- # ------------------------------------------------------------------------------
- def section_SetupPathsAndNames(self):
- # Set constants
- self.tableName = 'SampleTable'
- self.xColumnName = 'x'
- self.y1ColumnName = 'cos'
- self.y2ColumnName = 'sin'
-
- self.series1Name = "Cosine"
- self.series2Name = "Sine"
-
- self.chartName = "My Chart"
-
- # ------------------------------------------------------------------------------
- def section_CreateTable(self):
- self.delayDisplay("Create table")
-
- tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", self.tableName)
- self.assertIsNotNone(tableNode)
- table = tableNode.GetTable()
- self.assertIsNotNone(table)
-
- # Create X, Y1, and Y2 series
-
- arrX = vtk.vtkFloatArray()
- arrX.SetName(self.xColumnName)
- table.AddColumn(arrX)
-
- arrY1 = vtk.vtkFloatArray()
- arrY1.SetName(self.y1ColumnName)
- table.AddColumn(arrY1)
-
- arrY2 = vtk.vtkFloatArray()
- arrY2.SetName(self.y2ColumnName)
- table.AddColumn(arrY2)
-
- # Fill in the table with some example values
- import math
- numPoints = 69
- inc = 7.5 / (numPoints - 1)
- table.SetNumberOfRows(numPoints)
- for i in range(numPoints):
- table.SetValue(i, 0, i * inc)
- table.SetValue(i, 1, math.cos(i * inc))
- table.SetValue(i, 2, math.sin(i * inc))
-
- # ------------------------------------------------------------------------------
- def section_CreatePlots(self):
- self.delayDisplay("Create plots")
-
- tableNode = slicer.util.getNode(self.tableName)
-
- # Create plot data series nodes
-
- plotSeriesNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series1Name)
- plotSeriesNode1.SetAndObserveTableNodeID(tableNode.GetID())
- plotSeriesNode1.SetXColumnName(self.xColumnName)
- plotSeriesNode1.SetYColumnName(self.y1ColumnName)
- plotSeriesNode1.SetLineStyle(slicer.vtkMRMLPlotSeriesNode.LineStyleDash)
- plotSeriesNode1.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleSquare)
-
- plotSeriesNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series2Name)
- plotSeriesNode2.SetAndObserveTableNodeID(tableNode.GetID())
- plotSeriesNode2.SetXColumnName(self.xColumnName)
- plotSeriesNode2.SetYColumnName(self.y2ColumnName)
- plotSeriesNode2.SetUniqueColor()
-
- # Create plot chart node
- plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode", self.chartName)
- plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode1.GetID())
- plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode2.GetID())
- plotChartNode.SetTitle('A simple plot with 2 curves')
- plotChartNode.SetXAxisTitle('A simple plot with 2 curves')
- plotChartNode.SetYAxisTitle('This is the Y axis')
-
- # ------------------------------------------------------------------------------
- def section_TestPlotView(self):
- self.delayDisplay("Test plot view")
-
- plotChartNode = slicer.util.getNode(self.chartName)
-
- # Create plot view node
- plotViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode")
- plotViewNode.SetPlotChartNodeID(plotChartNode.GetID())
-
- # Create plotWidget
- plotWidget = slicer.qMRMLPlotWidget()
- plotWidget.setMRMLScene(slicer.mrmlScene)
- plotWidget.setMRMLPlotViewNode(plotViewNode)
- plotWidget.show()
-
- # Create plotView
- plotView = slicer.qMRMLPlotView()
- plotView.setMRMLScene(slicer.mrmlScene)
- plotView.setMRMLPlotViewNode(plotViewNode)
- plotView.show()
-
- # Save variables into slicer namespace for debugging
- slicer.plotWidget = plotWidget
- slicer.plotView = plotView
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_PlotsSelfTest_FullTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_PlotsSelfTest_FullTest1(self):
+ # Check for Plots module
+ self.assertTrue(slicer.modules.plots)
+
+ self.section_SetupPathsAndNames()
+ self.section_CreateTable()
+ self.section_CreatePlots()
+ self.section_TestPlotView()
+ self.delayDisplay("Test passed")
+
+ # ------------------------------------------------------------------------------
+ def section_SetupPathsAndNames(self):
+ # Set constants
+ self.tableName = 'SampleTable'
+ self.xColumnName = 'x'
+ self.y1ColumnName = 'cos'
+ self.y2ColumnName = 'sin'
+
+ self.series1Name = "Cosine"
+ self.series2Name = "Sine"
+
+ self.chartName = "My Chart"
+
+ # ------------------------------------------------------------------------------
+ def section_CreateTable(self):
+ self.delayDisplay("Create table")
+
+ tableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", self.tableName)
+ self.assertIsNotNone(tableNode)
+ table = tableNode.GetTable()
+ self.assertIsNotNone(table)
+
+ # Create X, Y1, and Y2 series
+
+ arrX = vtk.vtkFloatArray()
+ arrX.SetName(self.xColumnName)
+ table.AddColumn(arrX)
+
+ arrY1 = vtk.vtkFloatArray()
+ arrY1.SetName(self.y1ColumnName)
+ table.AddColumn(arrY1)
+
+ arrY2 = vtk.vtkFloatArray()
+ arrY2.SetName(self.y2ColumnName)
+ table.AddColumn(arrY2)
+
+ # Fill in the table with some example values
+ import math
+ numPoints = 69
+ inc = 7.5 / (numPoints - 1)
+ table.SetNumberOfRows(numPoints)
+ for i in range(numPoints):
+ table.SetValue(i, 0, i * inc)
+ table.SetValue(i, 1, math.cos(i * inc))
+ table.SetValue(i, 2, math.sin(i * inc))
+
+ # ------------------------------------------------------------------------------
+ def section_CreatePlots(self):
+ self.delayDisplay("Create plots")
+
+ tableNode = slicer.util.getNode(self.tableName)
+
+ # Create plot data series nodes
+
+ plotSeriesNode1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series1Name)
+ plotSeriesNode1.SetAndObserveTableNodeID(tableNode.GetID())
+ plotSeriesNode1.SetXColumnName(self.xColumnName)
+ plotSeriesNode1.SetYColumnName(self.y1ColumnName)
+ plotSeriesNode1.SetLineStyle(slicer.vtkMRMLPlotSeriesNode.LineStyleDash)
+ plotSeriesNode1.SetMarkerStyle(slicer.vtkMRMLPlotSeriesNode.MarkerStyleSquare)
+
+ plotSeriesNode2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotSeriesNode", self.series2Name)
+ plotSeriesNode2.SetAndObserveTableNodeID(tableNode.GetID())
+ plotSeriesNode2.SetXColumnName(self.xColumnName)
+ plotSeriesNode2.SetYColumnName(self.y2ColumnName)
+ plotSeriesNode2.SetUniqueColor()
+
+ # Create plot chart node
+ plotChartNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotChartNode", self.chartName)
+ plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode1.GetID())
+ plotChartNode.AddAndObservePlotSeriesNodeID(plotSeriesNode2.GetID())
+ plotChartNode.SetTitle('A simple plot with 2 curves')
+ plotChartNode.SetXAxisTitle('A simple plot with 2 curves')
+ plotChartNode.SetYAxisTitle('This is the Y axis')
+
+ # ------------------------------------------------------------------------------
+ def section_TestPlotView(self):
+ self.delayDisplay("Test plot view")
+
+ plotChartNode = slicer.util.getNode(self.chartName)
+
+ # Create plot view node
+ plotViewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLPlotViewNode")
+ plotViewNode.SetPlotChartNodeID(plotChartNode.GetID())
+
+ # Create plotWidget
+ plotWidget = slicer.qMRMLPlotWidget()
+ plotWidget.setMRMLScene(slicer.mrmlScene)
+ plotWidget.setMRMLPlotViewNode(plotViewNode)
+ plotWidget.show()
+
+ # Create plotView
+ plotView = slicer.qMRMLPlotView()
+ plotView.setMRMLScene(slicer.mrmlScene)
+ plotView.setMRMLPlotViewNode(plotViewNode)
+ plotView.show()
+
+ # Save variables into slicer namespace for debugging
+ slicer.plotWidget = plotWidget
+ slicer.plotView = plotView
diff --git a/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py b/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py
index f3d8df8ddbb..dddf5cb5e39 100644
--- a/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py
+++ b/Modules/Loadable/SceneViews/Testing/Python/AddStorableDataAfterSceneViewTest.py
@@ -12,23 +12,23 @@
#
class AddStorableDataAfterSceneViewTest(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Add Storable Data After Scene View Test"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = []
- self.parent.contributors = ["Nicole Aucoin (BWH)"]
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Add Storable Data After Scene View Test"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Nicole Aucoin (BWH)"]
+ self.parent.helpText = """
This self test adds some data, creates a scene view, then adds more storable data.
It tests Slicer's functionality after the scene view is restored, is the new storable
node still present? With the current implementation it only passes if the new storable
node is NOT present.
"""
- self.parent.acknowledgementText = """
+ self.parent.acknowledgementText = """
This file was originally developed by Nicole Aucoin, BWH, and was partially funded by NIH grant 3P41RR013218-12S1.
"""
@@ -38,54 +38,54 @@ def __init__(self, parent):
#
class AddStorableDataAfterSceneViewTestWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- #
- # check box to trigger taking screen shots for later use in tutorials
- #
- self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
- self.enableScreenshotsFlagCheckBox.checked = 0
- self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
- parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
+ #
+ # check box to trigger taking screen shots for later use in tutorials
+ #
+ self.enableScreenshotsFlagCheckBox = qt.QCheckBox()
+ self.enableScreenshotsFlagCheckBox.checked = 0
+ self.enableScreenshotsFlagCheckBox.setToolTip("If checked, take screen shots for tutorials. Use Save Data to write them to disk.")
+ parametersFormLayout.addRow("Enable Screenshots", self.enableScreenshotsFlagCheckBox)
- #
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the test."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ #
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the test."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- logic = AddStorableDataAfterSceneViewTestLogic()
- enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked
- logic.run(enableScreenshotsFlag)
+ def onApplyButton(self):
+ logic = AddStorableDataAfterSceneViewTestLogic()
+ enableScreenshotsFlag = self.enableScreenshotsFlagCheckBox.checked
+ logic.run(enableScreenshotsFlag)
#
@@ -93,112 +93,112 @@ def onApplyButton(self):
#
class AddStorableDataAfterSceneViewTestLogic(ScriptedLoadableModuleLogic):
- """
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def run(self, enableScreenshots=0):
"""
- Run the test via GUI
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- logging.info('Processing started')
+ def run(self, enableScreenshots=0):
+ """
+ Run the test via GUI
+ """
- try:
- evalString = 'AddStorableDataAfterSceneViewTestTest()'
- tester = eval(evalString)
- tester.runTest()
- except Exception as e:
- import traceback
- traceback.print_exc()
- errorMessage = "Add storable data after scene view test: Exception!\n\n" + str(e) + "\n\nSee Python Console for Stack Trace"
- slicer.util.errorDisplay(errorMessage)
+ logging.info('Processing started')
- logging.info('Processing completed')
+ try:
+ evalString = 'AddStorableDataAfterSceneViewTestTest()'
+ tester = eval(evalString)
+ tester.runTest()
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ errorMessage = "Add storable data after scene view test: Exception!\n\n" + str(e) + "\n\nSee Python Console for Stack Trace"
+ slicer.util.errorDisplay(errorMessage)
- return True
+ logging.info('Processing completed')
+
+ return True
class AddStorableDataAfterSceneViewTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.setUp()
- self.test_AddStorableDataAfterSceneViewTest1()
-
- def test_AddStorableDataAfterSceneViewTest1(self):
-
- slicer.util.delayDisplay("Starting the test")
-
- #
- # add a markups control point list
- #
-
- pointList = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode')
- pointList.AddControlPoint([10, 20, 15])
-
- #
- # save a scene view
- #
- sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode())
- sv.StoreScene()
-
- #
- # add another storable node, a volume
- #
- slicer.util.delayDisplay("Adding a new storable node, after creating a scene view")
- import SampleData
- mrHeadVolume = SampleData.downloadSample("MRHead")
- mrHeadID = mrHeadVolume.GetID()
-
- #
- # restore the scene view
- #
- slicer.util.delayDisplay("Restoring the scene view")
- sv.RestoreScene()
-
- #
- # Is the new storable data still present?
- #
- restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID)
-
- # for now, the non scene view storable data is removed
- self.assertIsNone(restoredData)
- slicer.util.delayDisplay('Success: extra storable node removed with scene view restore')
-
- #
- # add new storable again
- mrHeadVolume = SampleData.downloadSample("MRHead")
- mrHeadID = mrHeadVolume.GetID()
-
- #
- # restore the scene view, but error on removing nodes
- #
- slicer.util.delayDisplay("Restoring the scene view with check for removed nodes")
- sv.RestoreScene(0)
-
- #
- # Is the new storable data still present?
- #
- restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID)
-
- # in this case the non scene view storable data is kept' scene is not changed
- self.assertIsNotNone(restoredData)
- slicer.util.delayDisplay('Success: extra storable node NOT removed with scene view restore')
-
- print('Scene error code = ' + str(slicer.mrmlScene.GetErrorCode()))
- print('\t' + slicer.mrmlScene.GetErrorMessage())
-
- slicer.util.delayDisplay('Test passed!')
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_AddStorableDataAfterSceneViewTest1()
+
+ def test_AddStorableDataAfterSceneViewTest1(self):
+
+ slicer.util.delayDisplay("Starting the test")
+
+ #
+ # add a markups control point list
+ #
+
+ pointList = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsFiducialNode')
+ pointList.AddControlPoint([10, 20, 15])
+
+ #
+ # save a scene view
+ #
+ sv = slicer.mrmlScene.AddNode(slicer.vtkMRMLSceneViewNode())
+ sv.StoreScene()
+
+ #
+ # add another storable node, a volume
+ #
+ slicer.util.delayDisplay("Adding a new storable node, after creating a scene view")
+ import SampleData
+ mrHeadVolume = SampleData.downloadSample("MRHead")
+ mrHeadID = mrHeadVolume.GetID()
+
+ #
+ # restore the scene view
+ #
+ slicer.util.delayDisplay("Restoring the scene view")
+ sv.RestoreScene()
+
+ #
+ # Is the new storable data still present?
+ #
+ restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID)
+
+ # for now, the non scene view storable data is removed
+ self.assertIsNone(restoredData)
+ slicer.util.delayDisplay('Success: extra storable node removed with scene view restore')
+
+ #
+ # add new storable again
+ mrHeadVolume = SampleData.downloadSample("MRHead")
+ mrHeadID = mrHeadVolume.GetID()
+
+ #
+ # restore the scene view, but error on removing nodes
+ #
+ slicer.util.delayDisplay("Restoring the scene view with check for removed nodes")
+ sv.RestoreScene(0)
+
+ #
+ # Is the new storable data still present?
+ #
+ restoredData = slicer.mrmlScene.GetNodeByID(mrHeadID)
+
+ # in this case the non scene view storable data is kept' scene is not changed
+ self.assertIsNotNone(restoredData)
+ slicer.util.delayDisplay('Success: extra storable node NOT removed with scene view restore')
+
+ print('Scene error code = ' + str(slicer.mrmlScene.GetErrorCode()))
+ print('\t' + slicer.mrmlScene.GetErrorMessage())
+
+ slicer.util.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py
index 9d37a9e21f0..f15e21c3470 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorAutoCompleteEffect.py
@@ -19,541 +19,541 @@
#
class AbstractScriptedSegmentEditorAutoCompleteEffect(AbstractScriptedSegmentEditorEffect):
- """ AutoCompleteEffect is an effect that can create a full segmentation
- from a partial segmentation (not all slices are segmented or only
- part of the target structures are painted).
- """
-
- def __init__(self, scriptedEffect):
- # Indicates that effect does not operate on one segment, but the whole segmentation.
- # This means that while this effect is active, no segment can be selected
- scriptedEffect.perSegment = False
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- self.minimumNumberOfSegments = 1
- self.clippedMasterImageDataRequired = False
- self.clippedMaskImageDataRequired = False
-
- # Stores merged labelmap image geometry (voxel data is not allocated)
- self.mergedLabelmapGeometryImage = None
- self.selectedSegmentIds = None
- self.selectedSegmentModifiedTimes = {} # map from segment ID to ModifiedTime
- self.clippedMasterImageData = None
- self.clippedMaskImageData = None
-
- # Observation for auto-update
- self.observedSegmentation = None
- self.segmentationNodeObserverTags = []
-
- # Wait this much after the last modified event before starting aut-update:
- autoUpdateDelaySec = 1.0
- self.delayedAutoUpdateTimer = qt.QTimer()
- self.delayedAutoUpdateTimer.setSingleShot(True)
- self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000
- self.delayedAutoUpdateTimer.connect('timeout()', self.onPreview)
-
- self.extentGrowthRatio = 0.1 # extent of seed region will be grown outside by this much
- self.minimumExtentMargin = 3
-
- self.previewComputationInProgress = False
-
- def __del__(self, scriptedEffect):
- super(SegmentEditorAutoCompleteEffect, self).__del__()
- self.delayedAutoUpdateTimer.stop()
- self.observeSegmentation(False)
-
- @staticmethod
- def isBackgroundLabelmap(labelmapOrientedImageData, label=None):
- if labelmapOrientedImageData is None:
- return False
- # If five or more corner voxels of the image contain non-zero, then it is background
- extent = labelmapOrientedImageData.GetExtent()
- if extent[0] > extent[1] or extent[2] > extent[3] or extent[4] > extent[5]:
- return False
- numberOfFilledCorners = 0
- for i in [0, 1]:
- for j in [2, 3]:
- for k in [4, 5]:
- voxelValue = labelmapOrientedImageData.GetScalarComponentAsFloat(extent[i], extent[j], extent[k], 0)
- if label is None:
- if voxelValue > 0:
- numberOfFilledCorners += 1
- else:
- if voxelValue == label:
- numberOfFilledCorners += 1
- if numberOfFilledCorners > 4:
- return True
- return False
-
- def setupOptionsFrame(self):
- self.autoUpdateCheckBox = qt.QCheckBox("Auto-update")
- self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.")
- self.autoUpdateCheckBox.setChecked(True)
- self.autoUpdateCheckBox.setEnabled(False)
-
- self.previewButton = qt.QPushButton("Initialize")
- self.previewButton.objectName = self.__class__.__name__ + 'Preview'
- self.previewButton.setToolTip("Preview complete segmentation")
- # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
- # fails on some systems, therefore set the policies using separate method calls
- qSize = qt.QSizePolicy()
- qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
- self.previewButton.setSizePolicy(qSize)
-
- previewFrame = qt.QHBoxLayout()
- previewFrame.addWidget(self.autoUpdateCheckBox)
- previewFrame.addWidget(self.previewButton)
- self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame)
-
- self.previewOpacitySlider = ctk.ctkSliderWidget()
- self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.")
- self.previewOpacitySlider.minimum = 0
- self.previewOpacitySlider.maximum = 1.0
- self.previewOpacitySlider.value = 0.0
- self.previewOpacitySlider.singleStep = 0.05
- self.previewOpacitySlider.pageStep = 0.1
- self.previewOpacitySlider.spinBoxVisible = False
-
- self.previewShow3DButton = qt.QPushButton("Show 3D")
- self.previewShow3DButton.setToolTip("Preview results in 3D.")
- self.previewShow3DButton.setCheckable(True)
-
- displayFrame = qt.QHBoxLayout()
- displayFrame.addWidget(qt.QLabel("inputs"))
- displayFrame.addWidget(self.previewOpacitySlider)
- displayFrame.addWidget(qt.QLabel("results"))
- displayFrame.addWidget(self.previewShow3DButton)
- self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame)
-
- self.cancelButton = qt.QPushButton("Cancel")
- self.cancelButton.objectName = self.__class__.__name__ + 'Cancel'
- self.cancelButton.setToolTip("Clear preview and cancel auto-complete")
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Replace segments by previewed result")
-
- finishFrame = qt.QHBoxLayout()
- finishFrame.addWidget(self.cancelButton)
- finishFrame.addWidget(self.applyButton)
- self.scriptedEffect.addOptionsWidget(finishFrame)
-
- self.previewButton.connect('clicked()', self.onPreview)
- self.cancelButton.connect('clicked()', self.onCancel)
- self.applyButton.connect('clicked()', self.onApply)
- self.previewOpacitySlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.previewShow3DButton.connect("toggled(bool)", self.updateMRMLFromGUI)
- self.autoUpdateCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("AutoUpdate", "1")
-
- def onSegmentationModified(self, caller, event):
- if not self.autoUpdateCheckBox.isChecked():
- # just in case a queued request comes through
- return
-
- import vtkSegmentationCorePython as vtkSegmentationCore
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentation = segmentationNode.GetSegmentation()
-
- updateNeeded = False
- for segmentIndex in range(self.selectedSegmentIds.GetNumberOfValues()):
- segmentID = self.selectedSegmentIds.GetValue(segmentIndex)
- segment = segmentation.GetSegment(segmentID)
- if not segment:
- # selected segment was deleted, cancel segmentation
- logging.debug("Segmentation cancelled because an input segment was deleted")
- self.onCancel()
- return
- segmentLabelmap = segment.GetRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
- if segmentID in self.selectedSegmentModifiedTimes \
- and segmentLabelmap and segmentLabelmap.GetMTime() == self.selectedSegmentModifiedTimes[segmentID]:
- # this segment has not changed since last update
- continue
- if segmentLabelmap:
- self.selectedSegmentModifiedTimes[segmentID] = segmentLabelmap.GetMTime()
- elif segmentID in self.selectedSegmentModifiedTimes:
- self.selectedSegmentModifiedTimes.pop(segmentID)
- updateNeeded = True
- # continue so that all segment modified times are updated
-
- if not updateNeeded:
- return
-
- logging.debug("Segmentation update requested")
- # There could be multiple update events for a single paint operation (e.g., one segment overwrites the other)
- # therefore don't update directly, just set up/reset a timer that will perform the update when it elapses.
- if not self.previewComputationInProgress:
- self.delayedAutoUpdateTimer.start()
-
- def observeSegmentation(self, observationEnabled):
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- parameterSetNode = self.scriptedEffect.parameterSetNode()
- segmentationNode = None
- if parameterSetNode:
- segmentationNode = parameterSetNode.GetSegmentationNode()
-
- segmentation = None
- if segmentationNode:
- segmentation = segmentationNode.GetSegmentation()
-
- if observationEnabled and self.observedSegmentation == segmentation:
- return
- if not observationEnabled and not self.observedSegmentation:
- return
- # Need to update the observer
- # Remove old observer
- if self.observedSegmentation:
- for tag in self.segmentationNodeObserverTags:
- self.observedSegmentation.RemoveObserver(tag)
- self.segmentationNodeObserverTags = []
- self.observedSegmentation = None
- # Add new observer
- if observationEnabled and segmentation is not None:
- self.observedSegmentation = segmentation
- observedEvents = [
- vtkSegmentationCore.vtkSegmentation.SegmentAdded,
- vtkSegmentationCore.vtkSegmentation.SegmentRemoved,
- vtkSegmentationCore.vtkSegmentation.SegmentModified,
- vtkSegmentationCore.vtkSegmentation.MasterRepresentationModified]
- for eventId in observedEvents:
- self.segmentationNodeObserverTags.append(self.observedSegmentation.AddObserver(eventId, self.onSegmentationModified))
-
- def getPreviewNode(self):
- previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole)
- if previewNode and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name:
- # another effect owns this preview node
- return None
- return previewNode
-
- def updateGUIFromMRML(self):
-
- previewNode = self.getPreviewNode()
-
- self.cancelButton.setEnabled(previewNode is not None)
- self.applyButton.setEnabled(previewNode is not None)
-
- self.previewOpacitySlider.setEnabled(previewNode is not None)
- if previewNode:
- wasBlocked = self.previewOpacitySlider.blockSignals(True)
- self.previewOpacitySlider.value = self.getPreviewOpacity()
- self.previewOpacitySlider.blockSignals(wasBlocked)
- self.previewButton.text = "Update"
- self.previewShow3DButton.setEnabled(True)
- self.previewShow3DButton.setChecked(self.getPreviewShow3D())
- self.autoUpdateCheckBox.setEnabled(True)
- self.observeSegmentation(self.autoUpdateCheckBox.isChecked())
- else:
- self.previewButton.text = "Initialize"
- self.autoUpdateCheckBox.setEnabled(False)
- self.previewShow3DButton.setEnabled(False)
- self.delayedAutoUpdateTimer.stop()
- self.observeSegmentation(False)
-
- autoUpdate = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("AutoUpdate") == 0 else qt.Qt.Checked
- wasBlocked = self.autoUpdateCheckBox.blockSignals(True)
- self.autoUpdateCheckBox.setCheckState(autoUpdate)
- self.autoUpdateCheckBox.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- previewNode = self.getPreviewNode()
- if previewNode:
- self.setPreviewOpacity(self.previewOpacitySlider.value)
- self.setPreviewShow3D(self.previewShow3DButton.checked)
-
- autoUpdate = 1 if self.autoUpdateCheckBox.isChecked() else 0
- self.scriptedEffect.setParameter("AutoUpdate", autoUpdate)
-
- def onPreview(self):
- if self.previewComputationInProgress:
- return
- self.previewComputationInProgress = True
-
- slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000)
- try:
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- self.preview()
- finally:
- qt.QApplication.restoreOverrideCursor()
-
- self.previewComputationInProgress = False
-
- def reset(self):
- self.delayedAutoUpdateTimer.stop()
- self.observeSegmentation(False)
- previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole)
- if previewNode:
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, None)
- slicer.mrmlScene.RemoveNode(previewNode)
- self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", "")
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationNode.GetDisplayNode().SetOpacity(1.0)
- self.mergedLabelmapGeometryImage = None
- self.selectedSegmentIds = None
- self.selectedSegmentModifiedTimes = {}
- self.clippedMasterImageData = None
- self.clippedMaskImageData = None
- self.updateGUIFromMRML()
-
- def onCancel(self):
- self.reset()
-
- def onApply(self):
- self.delayedAutoUpdateTimer.stop()
- self.observeSegmentation(False)
-
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationDisplayNode = segmentationNode.GetDisplayNode()
- previewNode = self.getPreviewNode()
-
- self.scriptedEffect.saveStateForUndo()
-
- previewContainsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation(
- slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
-
- # Move segments from preview into current segmentation
- segmentIDs = vtk.vtkStringArray()
- previewNode.GetSegmentation().GetSegmentIDs(segmentIDs)
- for index in range(segmentIDs.GetNumberOfValues()):
- segmentID = segmentIDs.GetValue(index)
- previewSegmentLabelmap = slicer.vtkOrientedImageData()
- previewNode.GetBinaryLabelmapRepresentation(segmentID, previewSegmentLabelmap)
- self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, previewSegmentLabelmap,
- slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
- if segmentationDisplayNode is not None and self.isBackgroundLabelmap(previewSegmentLabelmap):
- # Automatically hide result segments that are background (all eight corners are non-zero)
- segmentationDisplayNode.SetSegmentVisibility(segmentID, False)
- previewNode.GetSegmentation().RemoveSegment(segmentID) # delete now to limit memory usage
-
- if previewContainsClosedSurfaceRepresentation:
- segmentationNode.CreateClosedSurfaceRepresentation()
-
- self.reset()
-
- def setPreviewOpacity(self, opacity):
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationNode.GetDisplayNode().SetOpacity(1.0 - opacity)
- previewNode = self.getPreviewNode()
- if previewNode:
- previewNode.GetDisplayNode().SetOpacity(opacity)
- previewNode.GetDisplayNode().SetOpacity3D(opacity)
-
- # Make sure the GUI is up-to-date
- wasBlocked = self.previewOpacitySlider.blockSignals(True)
- self.previewOpacitySlider.value = opacity
- self.previewOpacitySlider.blockSignals(wasBlocked)
-
- def getPreviewOpacity(self):
- previewNode = self.getPreviewNode()
- return previewNode.GetDisplayNode().GetOpacity() if previewNode else 0.6 # default opacity for preview
-
- def setPreviewShow3D(self, show):
- previewNode = self.getPreviewNode()
- if previewNode:
- if show:
- previewNode.CreateClosedSurfaceRepresentation()
- else:
- previewNode.RemoveClosedSurfaceRepresentation()
-
- # Make sure the GUI is up-to-date
- wasBlocked = self.previewShow3DButton.blockSignals(True)
- self.previewShow3DButton.checked = show
- self.previewShow3DButton.blockSignals(wasBlocked)
-
- def getPreviewShow3D(self):
- previewNode = self.getPreviewNode()
- if not previewNode:
- return False
- containsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation(
- slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
- return containsClosedSurfaceRepresentation
-
- def effectiveExtentChanged(self):
- if self.getPreviewNode() is None:
- return True
- if self.mergedLabelmapGeometryImage is None:
- return True
- if self.selectedSegmentIds is None:
- return True
-
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
-
- # The effective extent for the current input segments
- effectiveGeometryImage = slicer.vtkOrientedImageData()
- effectiveGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds)
- if effectiveGeometryString is None:
- return True
- vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(effectiveGeometryString, effectiveGeometryImage)
-
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- masterImageExtent = masterImageData.GetExtent()
-
- # The effective extent of the selected segments
- effectiveLabelExtent = effectiveGeometryImage.GetExtent()
- # Current extent used for auto-complete preview
- currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent()
-
- # Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent)
- return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or
- (masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or
- (masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or
- (masterImageExtent[3] != currentLabelExtent[3] and currentLabelExtent[3] < effectiveLabelExtent[3] + self.minimumExtentMargin) or
- (masterImageExtent[4] != currentLabelExtent[4] and currentLabelExtent[4] > effectiveLabelExtent[4] - self.minimumExtentMargin) or
- (masterImageExtent[5] != currentLabelExtent[5] and currentLabelExtent[5] < effectiveLabelExtent[5] + self.minimumExtentMargin))
-
- def preview(self):
- # Get master volume image data
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- # Get segmentation
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
-
- previewNode = self.getPreviewNode()
- previewOpacity = self.getPreviewOpacity()
- previewShow3D = self.getPreviewShow3D()
-
- # If the selectedSegmentIds have been specified, then they shouldn't be overwritten here
- currentSelectedSegmentIds = self.selectedSegmentIds
-
- if self.effectiveExtentChanged():
- self.reset()
-
- # Restore the selectedSegmentIds
- self.selectedSegmentIds = currentSelectedSegmentIds
- if self.selectedSegmentIds is None:
- self.selectedSegmentIds = vtk.vtkStringArray()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(self.selectedSegmentIds)
- if self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments:
- logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegments} visible segments are required")
+ """ AutoCompleteEffect is an effect that can create a full segmentation
+ from a partial segmentation (not all slices are segmented or only
+ part of the target structures are painted).
+ """
+
+ def __init__(self, scriptedEffect):
+ # Indicates that effect does not operate on one segment, but the whole segmentation.
+ # This means that while this effect is active, no segment can be selected
+ scriptedEffect.perSegment = False
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ self.minimumNumberOfSegments = 1
+ self.clippedMasterImageDataRequired = False
+ self.clippedMaskImageDataRequired = False
+
+ # Stores merged labelmap image geometry (voxel data is not allocated)
+ self.mergedLabelmapGeometryImage = None
+ self.selectedSegmentIds = None
+ self.selectedSegmentModifiedTimes = {} # map from segment ID to ModifiedTime
+ self.clippedMasterImageData = None
+ self.clippedMaskImageData = None
+
+ # Observation for auto-update
+ self.observedSegmentation = None
+ self.segmentationNodeObserverTags = []
+
+ # Wait this much after the last modified event before starting aut-update:
+ autoUpdateDelaySec = 1.0
+ self.delayedAutoUpdateTimer = qt.QTimer()
+ self.delayedAutoUpdateTimer.setSingleShot(True)
+ self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000
+ self.delayedAutoUpdateTimer.connect('timeout()', self.onPreview)
+
+ self.extentGrowthRatio = 0.1 # extent of seed region will be grown outside by this much
+ self.minimumExtentMargin = 3
+
+ self.previewComputationInProgress = False
+
+ def __del__(self, scriptedEffect):
+ super(SegmentEditorAutoCompleteEffect, self).__del__()
+ self.delayedAutoUpdateTimer.stop()
+ self.observeSegmentation(False)
+
+ @staticmethod
+ def isBackgroundLabelmap(labelmapOrientedImageData, label=None):
+ if labelmapOrientedImageData is None:
+ return False
+ # If five or more corner voxels of the image contain non-zero, then it is background
+ extent = labelmapOrientedImageData.GetExtent()
+ if extent[0] > extent[1] or extent[2] > extent[3] or extent[4] > extent[5]:
+ return False
+ numberOfFilledCorners = 0
+ for i in [0, 1]:
+ for j in [2, 3]:
+ for k in [4, 5]:
+ voxelValue = labelmapOrientedImageData.GetScalarComponentAsFloat(extent[i], extent[j], extent[k], 0)
+ if label is None:
+ if voxelValue > 0:
+ numberOfFilledCorners += 1
+ else:
+ if voxelValue == label:
+ numberOfFilledCorners += 1
+ if numberOfFilledCorners > 4:
+ return True
+ return False
+
+ def setupOptionsFrame(self):
+ self.autoUpdateCheckBox = qt.QCheckBox("Auto-update")
+ self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.")
+ self.autoUpdateCheckBox.setChecked(True)
+ self.autoUpdateCheckBox.setEnabled(False)
+
+ self.previewButton = qt.QPushButton("Initialize")
+ self.previewButton.objectName = self.__class__.__name__ + 'Preview'
+ self.previewButton.setToolTip("Preview complete segmentation")
+ # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ # fails on some systems, therefore set the policies using separate method calls
+ qSize = qt.QSizePolicy()
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
+ self.previewButton.setSizePolicy(qSize)
+
+ previewFrame = qt.QHBoxLayout()
+ previewFrame.addWidget(self.autoUpdateCheckBox)
+ previewFrame.addWidget(self.previewButton)
+ self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame)
+
+ self.previewOpacitySlider = ctk.ctkSliderWidget()
+ self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.")
+ self.previewOpacitySlider.minimum = 0
+ self.previewOpacitySlider.maximum = 1.0
+ self.previewOpacitySlider.value = 0.0
+ self.previewOpacitySlider.singleStep = 0.05
+ self.previewOpacitySlider.pageStep = 0.1
+ self.previewOpacitySlider.spinBoxVisible = False
+
+ self.previewShow3DButton = qt.QPushButton("Show 3D")
+ self.previewShow3DButton.setToolTip("Preview results in 3D.")
+ self.previewShow3DButton.setCheckable(True)
+
+ displayFrame = qt.QHBoxLayout()
+ displayFrame.addWidget(qt.QLabel("inputs"))
+ displayFrame.addWidget(self.previewOpacitySlider)
+ displayFrame.addWidget(qt.QLabel("results"))
+ displayFrame.addWidget(self.previewShow3DButton)
+ self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame)
+
+ self.cancelButton = qt.QPushButton("Cancel")
+ self.cancelButton.objectName = self.__class__.__name__ + 'Cancel'
+ self.cancelButton.setToolTip("Clear preview and cancel auto-complete")
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Replace segments by previewed result")
+
+ finishFrame = qt.QHBoxLayout()
+ finishFrame.addWidget(self.cancelButton)
+ finishFrame.addWidget(self.applyButton)
+ self.scriptedEffect.addOptionsWidget(finishFrame)
+
+ self.previewButton.connect('clicked()', self.onPreview)
+ self.cancelButton.connect('clicked()', self.onCancel)
+ self.applyButton.connect('clicked()', self.onApply)
+ self.previewOpacitySlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.previewShow3DButton.connect("toggled(bool)", self.updateMRMLFromGUI)
+ self.autoUpdateCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("AutoUpdate", "1")
+
+ def onSegmentationModified(self, caller, event):
+ if not self.autoUpdateCheckBox.isChecked():
+ # just in case a queued request comes through
+ return
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentation = segmentationNode.GetSegmentation()
+
+ updateNeeded = False
+ for segmentIndex in range(self.selectedSegmentIds.GetNumberOfValues()):
+ segmentID = self.selectedSegmentIds.GetValue(segmentIndex)
+ segment = segmentation.GetSegment(segmentID)
+ if not segment:
+ # selected segment was deleted, cancel segmentation
+ logging.debug("Segmentation cancelled because an input segment was deleted")
+ self.onCancel()
+ return
+ segmentLabelmap = segment.GetRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
+ if segmentID in self.selectedSegmentModifiedTimes \
+ and segmentLabelmap and segmentLabelmap.GetMTime() == self.selectedSegmentModifiedTimes[segmentID]:
+ # this segment has not changed since last update
+ continue
+ if segmentLabelmap:
+ self.selectedSegmentModifiedTimes[segmentID] = segmentLabelmap.GetMTime()
+ elif segmentID in self.selectedSegmentModifiedTimes:
+ self.selectedSegmentModifiedTimes.pop(segmentID)
+ updateNeeded = True
+ # continue so that all segment modified times are updated
+
+ if not updateNeeded:
+ return
+
+ logging.debug("Segmentation update requested")
+ # There could be multiple update events for a single paint operation (e.g., one segment overwrites the other)
+ # therefore don't update directly, just set up/reset a timer that will perform the update when it elapses.
+ if not self.previewComputationInProgress:
+ self.delayedAutoUpdateTimer.start()
+
+ def observeSegmentation(self, observationEnabled):
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ parameterSetNode = self.scriptedEffect.parameterSetNode()
+ segmentationNode = None
+ if parameterSetNode:
+ segmentationNode = parameterSetNode.GetSegmentationNode()
+
+ segmentation = None
+ if segmentationNode:
+ segmentation = segmentationNode.GetSegmentation()
+
+ if observationEnabled and self.observedSegmentation == segmentation:
+ return
+ if not observationEnabled and not self.observedSegmentation:
+ return
+ # Need to update the observer
+ # Remove old observer
+ if self.observedSegmentation:
+ for tag in self.segmentationNodeObserverTags:
+ self.observedSegmentation.RemoveObserver(tag)
+ self.segmentationNodeObserverTags = []
+ self.observedSegmentation = None
+ # Add new observer
+ if observationEnabled and segmentation is not None:
+ self.observedSegmentation = segmentation
+ observedEvents = [
+ vtkSegmentationCore.vtkSegmentation.SegmentAdded,
+ vtkSegmentationCore.vtkSegmentation.SegmentRemoved,
+ vtkSegmentationCore.vtkSegmentation.SegmentModified,
+ vtkSegmentationCore.vtkSegmentation.MasterRepresentationModified]
+ for eventId in observedEvents:
+ self.segmentationNodeObserverTags.append(self.observedSegmentation.AddObserver(eventId, self.onSegmentationModified))
+
+ def getPreviewNode(self):
+ previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole)
+ if previewNode and self.scriptedEffect.parameter("SegmentationResultPreviewOwnerEffect") != self.scriptedEffect.name:
+ # another effect owns this preview node
+ return None
+ return previewNode
+
+ def updateGUIFromMRML(self):
+
+ previewNode = self.getPreviewNode()
+
+ self.cancelButton.setEnabled(previewNode is not None)
+ self.applyButton.setEnabled(previewNode is not None)
+
+ self.previewOpacitySlider.setEnabled(previewNode is not None)
+ if previewNode:
+ wasBlocked = self.previewOpacitySlider.blockSignals(True)
+ self.previewOpacitySlider.value = self.getPreviewOpacity()
+ self.previewOpacitySlider.blockSignals(wasBlocked)
+ self.previewButton.text = "Update"
+ self.previewShow3DButton.setEnabled(True)
+ self.previewShow3DButton.setChecked(self.getPreviewShow3D())
+ self.autoUpdateCheckBox.setEnabled(True)
+ self.observeSegmentation(self.autoUpdateCheckBox.isChecked())
+ else:
+ self.previewButton.text = "Initialize"
+ self.autoUpdateCheckBox.setEnabled(False)
+ self.previewShow3DButton.setEnabled(False)
+ self.delayedAutoUpdateTimer.stop()
+ self.observeSegmentation(False)
+
+ autoUpdate = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("AutoUpdate") == 0 else qt.Qt.Checked
+ wasBlocked = self.autoUpdateCheckBox.blockSignals(True)
+ self.autoUpdateCheckBox.setCheckState(autoUpdate)
+ self.autoUpdateCheckBox.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ previewNode = self.getPreviewNode()
+ if previewNode:
+ self.setPreviewOpacity(self.previewOpacitySlider.value)
+ self.setPreviewShow3D(self.previewShow3DButton.checked)
+
+ autoUpdate = 1 if self.autoUpdateCheckBox.isChecked() else 0
+ self.scriptedEffect.setParameter("AutoUpdate", autoUpdate)
+
+ def onPreview(self):
+ if self.previewComputationInProgress:
+ return
+ self.previewComputationInProgress = True
+
+ slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000)
+ try:
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ self.preview()
+ finally:
+ qt.QApplication.restoreOverrideCursor()
+
+ self.previewComputationInProgress = False
+
+ def reset(self):
+ self.delayedAutoUpdateTimer.stop()
+ self.observeSegmentation(False)
+ previewNode = self.scriptedEffect.parameterSetNode().GetNodeReference(ResultPreviewNodeReferenceRole)
+ if previewNode:
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, None)
+ slicer.mrmlScene.RemoveNode(previewNode)
+ self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", "")
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationNode.GetDisplayNode().SetOpacity(1.0)
+ self.mergedLabelmapGeometryImage = None
self.selectedSegmentIds = None
- return
-
- # Compute merged labelmap extent (effective extent slightly expanded)
- if not self.mergedLabelmapGeometryImage:
- self.mergedLabelmapGeometryImage = slicer.vtkOrientedImageData()
- commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds)
- if not commonGeometryString:
- logging.info("Auto-complete operation skipped: all visible segments are empty")
- return
- vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, self.mergedLabelmapGeometryImage)
-
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- masterImageExtent = masterImageData.GetExtent()
- labelsEffectiveExtent = self.mergedLabelmapGeometryImage.GetExtent()
- # Margin size is relative to combined seed region size, but minimum of 3 voxels
- print(f"self.extentGrowthRatio = {self.extentGrowthRatio}")
- margin = [
- int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[1] - labelsEffectiveExtent[0]))),
- int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[3] - labelsEffectiveExtent[2]))),
- int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[5] - labelsEffectiveExtent[4])))]
- labelsExpandedExtent = [
- max(masterImageExtent[0], labelsEffectiveExtent[0] - margin[0]),
- min(masterImageExtent[1], labelsEffectiveExtent[1] + margin[0]),
- max(masterImageExtent[2], labelsEffectiveExtent[2] - margin[1]),
- min(masterImageExtent[3], labelsEffectiveExtent[3] + margin[1]),
- max(masterImageExtent[4], labelsEffectiveExtent[4] - margin[2]),
- min(masterImageExtent[5], labelsEffectiveExtent[5] + margin[2])]
- print("masterImageExtent = " + repr(masterImageExtent))
- print("labelsEffectiveExtent = " + repr(labelsEffectiveExtent))
- print("labelsExpandedExtent = " + repr(labelsExpandedExtent))
- self.mergedLabelmapGeometryImage.SetExtent(labelsExpandedExtent)
-
- # Create and setup preview node
- previewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
- previewNode.CreateDefaultDisplayNodes()
- previewNode.GetDisplayNode().SetVisibility2DOutline(False)
- if segmentationNode.GetParentTransformNode():
- previewNode.SetAndObserveTransformNodeID(segmentationNode.GetParentTransformNode().GetID())
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, previewNode.GetID())
- self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", self.scriptedEffect.name)
- self.setPreviewOpacity(0.6)
-
- # Disable smoothing for closed surface generation to make it fast
- previewNode.GetSegmentation().SetConversionParameter(
- slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule.GetSmoothingFactorParameterName(),
- "-0.5")
-
- inputContainsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
- slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
-
- self.setPreviewShow3D(inputContainsClosedSurfaceRepresentation)
-
- if self.clippedMasterImageDataRequired:
- self.clippedMasterImageData = slicer.vtkOrientedImageData()
- masterImageClipper = vtk.vtkImageConstantPad()
- masterImageClipper.SetInputData(masterImageData)
- masterImageClipper.SetOutputWholeExtent(self.mergedLabelmapGeometryImage.GetExtent())
- masterImageClipper.Update()
- self.clippedMasterImageData.ShallowCopy(masterImageClipper.GetOutput())
- self.clippedMasterImageData.CopyDirections(self.mergedLabelmapGeometryImage)
-
- self.clippedMaskImageData = None
- if self.clippedMaskImageDataRequired:
- self.clippedMaskImageData = slicer.vtkOrientedImageData()
- intensityBasedMasking = self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMask()
- success = segmentationNode.GenerateEditMask(self.clippedMaskImageData,
- self.scriptedEffect.parameterSetNode().GetMaskMode(),
- self.clippedMasterImageData, # reference geometry
- "", # edited segment ID
- self.scriptedEffect.parameterSetNode().GetMaskSegmentID() if self.scriptedEffect.parameterSetNode().GetMaskSegmentID() else "",
- self.clippedMasterImageData if intensityBasedMasking else None,
- self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMaskRange() if intensityBasedMasking else None)
- if not success:
- logging.error("Failed to create edit mask")
- self.clippedMaskImageData = None
-
- previewNode.SetName(segmentationNode.GetName() + " preview")
- previewNode.RemoveClosedSurfaceRepresentation() # Force the closed surface representation to update
- # TODO: This will no longer be required when we can use the segment editor to set multiple segments
- # as the closed surfaces will be converted as necessary by the segmentation logic.
-
- mergedImage = slicer.vtkOrientedImageData()
- segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)
-
- outputLabelmap = slicer.vtkOrientedImageData()
- self.computePreviewLabelmap(mergedImage, outputLabelmap)
-
- if previewNode.GetSegmentation().GetNumberOfSegments() != self.selectedSegmentIds.GetNumberOfValues():
- # first update (or number of segments changed), need a full reinitialization
- previewNode.GetSegmentation().RemoveAllSegments()
-
- for index in range(self.selectedSegmentIds.GetNumberOfValues()):
- segmentID = self.selectedSegmentIds.GetValue(index)
-
- previewSegment = previewNode.GetSegmentation().GetSegment(segmentID)
- if not previewSegment:
- inputSegment = segmentationNode.GetSegmentation().GetSegment(segmentID)
-
- previewSegment = vtkSegmentationCore.vtkSegment()
- previewSegment.SetName(inputSegment.GetName())
- previewSegment.SetColor(inputSegment.GetColor())
- previewNode.GetSegmentation().AddSegment(previewSegment, segmentID)
-
- labelValue = index + 1 # n-th segment label value = n + 1 (background label value is 0)
- previewSegment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), outputLabelmap)
- previewSegment.SetLabelValue(labelValue)
-
- # Automatically hide result segments that are background (all eight corners are non-zero)
- previewNode.GetDisplayNode().SetSegmentVisibility3D(segmentID, not self.isBackgroundLabelmap(outputLabelmap, labelValue))
-
- # If the preview was reset, we need to restore the visibility options
- self.setPreviewOpacity(previewOpacity)
- self.setPreviewShow3D(previewShow3D)
-
- self.updateGUIFromMRML()
+ self.selectedSegmentModifiedTimes = {}
+ self.clippedMasterImageData = None
+ self.clippedMaskImageData = None
+ self.updateGUIFromMRML()
+
+ def onCancel(self):
+ self.reset()
+
+ def onApply(self):
+ self.delayedAutoUpdateTimer.stop()
+ self.observeSegmentation(False)
+
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationDisplayNode = segmentationNode.GetDisplayNode()
+ previewNode = self.getPreviewNode()
+
+ self.scriptedEffect.saveStateForUndo()
+
+ previewContainsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation(
+ slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
+
+ # Move segments from preview into current segmentation
+ segmentIDs = vtk.vtkStringArray()
+ previewNode.GetSegmentation().GetSegmentIDs(segmentIDs)
+ for index in range(segmentIDs.GetNumberOfValues()):
+ segmentID = segmentIDs.GetValue(index)
+ previewSegmentLabelmap = slicer.vtkOrientedImageData()
+ previewNode.GetBinaryLabelmapRepresentation(segmentID, previewSegmentLabelmap)
+ self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, previewSegmentLabelmap,
+ slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+ if segmentationDisplayNode is not None and self.isBackgroundLabelmap(previewSegmentLabelmap):
+ # Automatically hide result segments that are background (all eight corners are non-zero)
+ segmentationDisplayNode.SetSegmentVisibility(segmentID, False)
+ previewNode.GetSegmentation().RemoveSegment(segmentID) # delete now to limit memory usage
+
+ if previewContainsClosedSurfaceRepresentation:
+ segmentationNode.CreateClosedSurfaceRepresentation()
+
+ self.reset()
+
+ def setPreviewOpacity(self, opacity):
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationNode.GetDisplayNode().SetOpacity(1.0 - opacity)
+ previewNode = self.getPreviewNode()
+ if previewNode:
+ previewNode.GetDisplayNode().SetOpacity(opacity)
+ previewNode.GetDisplayNode().SetOpacity3D(opacity)
+
+ # Make sure the GUI is up-to-date
+ wasBlocked = self.previewOpacitySlider.blockSignals(True)
+ self.previewOpacitySlider.value = opacity
+ self.previewOpacitySlider.blockSignals(wasBlocked)
+
+ def getPreviewOpacity(self):
+ previewNode = self.getPreviewNode()
+ return previewNode.GetDisplayNode().GetOpacity() if previewNode else 0.6 # default opacity for preview
+
+ def setPreviewShow3D(self, show):
+ previewNode = self.getPreviewNode()
+ if previewNode:
+ if show:
+ previewNode.CreateClosedSurfaceRepresentation()
+ else:
+ previewNode.RemoveClosedSurfaceRepresentation()
+
+ # Make sure the GUI is up-to-date
+ wasBlocked = self.previewShow3DButton.blockSignals(True)
+ self.previewShow3DButton.checked = show
+ self.previewShow3DButton.blockSignals(wasBlocked)
+
+ def getPreviewShow3D(self):
+ previewNode = self.getPreviewNode()
+ if not previewNode:
+ return False
+ containsClosedSurfaceRepresentation = previewNode.GetSegmentation().ContainsRepresentation(
+ slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
+ return containsClosedSurfaceRepresentation
+
+ def effectiveExtentChanged(self):
+ if self.getPreviewNode() is None:
+ return True
+ if self.mergedLabelmapGeometryImage is None:
+ return True
+ if self.selectedSegmentIds is None:
+ return True
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+
+ # The effective extent for the current input segments
+ effectiveGeometryImage = slicer.vtkOrientedImageData()
+ effectiveGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds)
+ if effectiveGeometryString is None:
+ return True
+ vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(effectiveGeometryString, effectiveGeometryImage)
+
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ masterImageExtent = masterImageData.GetExtent()
+
+ # The effective extent of the selected segments
+ effectiveLabelExtent = effectiveGeometryImage.GetExtent()
+ # Current extent used for auto-complete preview
+ currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent()
+
+ # Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent)
+ return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or
+ (masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or
+ (masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or
+ (masterImageExtent[3] != currentLabelExtent[3] and currentLabelExtent[3] < effectiveLabelExtent[3] + self.minimumExtentMargin) or
+ (masterImageExtent[4] != currentLabelExtent[4] and currentLabelExtent[4] > effectiveLabelExtent[4] - self.minimumExtentMargin) or
+ (masterImageExtent[5] != currentLabelExtent[5] and currentLabelExtent[5] < effectiveLabelExtent[5] + self.minimumExtentMargin))
+
+ def preview(self):
+ # Get master volume image data
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ # Get segmentation
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+
+ previewNode = self.getPreviewNode()
+ previewOpacity = self.getPreviewOpacity()
+ previewShow3D = self.getPreviewShow3D()
+
+ # If the selectedSegmentIds have been specified, then they shouldn't be overwritten here
+ currentSelectedSegmentIds = self.selectedSegmentIds
+
+ if self.effectiveExtentChanged():
+ self.reset()
+
+ # Restore the selectedSegmentIds
+ self.selectedSegmentIds = currentSelectedSegmentIds
+ if self.selectedSegmentIds is None:
+ self.selectedSegmentIds = vtk.vtkStringArray()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(self.selectedSegmentIds)
+ if self.selectedSegmentIds.GetNumberOfValues() < self.minimumNumberOfSegments:
+ logging.error(f"Auto-complete operation skipped: at least {self.minimumNumberOfSegments} visible segments are required")
+ self.selectedSegmentIds = None
+ return
+
+ # Compute merged labelmap extent (effective extent slightly expanded)
+ if not self.mergedLabelmapGeometryImage:
+ self.mergedLabelmapGeometryImage = slicer.vtkOrientedImageData()
+ commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.selectedSegmentIds)
+ if not commonGeometryString:
+ logging.info("Auto-complete operation skipped: all visible segments are empty")
+ return
+ vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, self.mergedLabelmapGeometryImage)
+
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ masterImageExtent = masterImageData.GetExtent()
+ labelsEffectiveExtent = self.mergedLabelmapGeometryImage.GetExtent()
+ # Margin size is relative to combined seed region size, but minimum of 3 voxels
+ print(f"self.extentGrowthRatio = {self.extentGrowthRatio}")
+ margin = [
+ int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[1] - labelsEffectiveExtent[0]))),
+ int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[3] - labelsEffectiveExtent[2]))),
+ int(max(3, self.extentGrowthRatio * (labelsEffectiveExtent[5] - labelsEffectiveExtent[4])))]
+ labelsExpandedExtent = [
+ max(masterImageExtent[0], labelsEffectiveExtent[0] - margin[0]),
+ min(masterImageExtent[1], labelsEffectiveExtent[1] + margin[0]),
+ max(masterImageExtent[2], labelsEffectiveExtent[2] - margin[1]),
+ min(masterImageExtent[3], labelsEffectiveExtent[3] + margin[1]),
+ max(masterImageExtent[4], labelsEffectiveExtent[4] - margin[2]),
+ min(masterImageExtent[5], labelsEffectiveExtent[5] + margin[2])]
+ print("masterImageExtent = " + repr(masterImageExtent))
+ print("labelsEffectiveExtent = " + repr(labelsEffectiveExtent))
+ print("labelsExpandedExtent = " + repr(labelsExpandedExtent))
+ self.mergedLabelmapGeometryImage.SetExtent(labelsExpandedExtent)
+
+ # Create and setup preview node
+ previewNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
+ previewNode.CreateDefaultDisplayNodes()
+ previewNode.GetDisplayNode().SetVisibility2DOutline(False)
+ if segmentationNode.GetParentTransformNode():
+ previewNode.SetAndObserveTransformNodeID(segmentationNode.GetParentTransformNode().GetID())
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID(ResultPreviewNodeReferenceRole, previewNode.GetID())
+ self.scriptedEffect.setCommonParameter("SegmentationResultPreviewOwnerEffect", self.scriptedEffect.name)
+ self.setPreviewOpacity(0.6)
+
+ # Disable smoothing for closed surface generation to make it fast
+ previewNode.GetSegmentation().SetConversionParameter(
+ slicer.vtkBinaryLabelmapToClosedSurfaceConversionRule.GetSmoothingFactorParameterName(),
+ "-0.5")
+
+ inputContainsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
+ slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
+
+ self.setPreviewShow3D(inputContainsClosedSurfaceRepresentation)
+
+ if self.clippedMasterImageDataRequired:
+ self.clippedMasterImageData = slicer.vtkOrientedImageData()
+ masterImageClipper = vtk.vtkImageConstantPad()
+ masterImageClipper.SetInputData(masterImageData)
+ masterImageClipper.SetOutputWholeExtent(self.mergedLabelmapGeometryImage.GetExtent())
+ masterImageClipper.Update()
+ self.clippedMasterImageData.ShallowCopy(masterImageClipper.GetOutput())
+ self.clippedMasterImageData.CopyDirections(self.mergedLabelmapGeometryImage)
+
+ self.clippedMaskImageData = None
+ if self.clippedMaskImageDataRequired:
+ self.clippedMaskImageData = slicer.vtkOrientedImageData()
+ intensityBasedMasking = self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMask()
+ success = segmentationNode.GenerateEditMask(self.clippedMaskImageData,
+ self.scriptedEffect.parameterSetNode().GetMaskMode(),
+ self.clippedMasterImageData, # reference geometry
+ "", # edited segment ID
+ self.scriptedEffect.parameterSetNode().GetMaskSegmentID() if self.scriptedEffect.parameterSetNode().GetMaskSegmentID() else "",
+ self.clippedMasterImageData if intensityBasedMasking else None,
+ self.scriptedEffect.parameterSetNode().GetMasterVolumeIntensityMaskRange() if intensityBasedMasking else None)
+ if not success:
+ logging.error("Failed to create edit mask")
+ self.clippedMaskImageData = None
+
+ previewNode.SetName(segmentationNode.GetName() + " preview")
+ previewNode.RemoveClosedSurfaceRepresentation() # Force the closed surface representation to update
+ # TODO: This will no longer be required when we can use the segment editor to set multiple segments
+ # as the closed surfaces will be converted as necessary by the segmentation logic.
+
+ mergedImage = slicer.vtkOrientedImageData()
+ segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)
+
+ outputLabelmap = slicer.vtkOrientedImageData()
+ self.computePreviewLabelmap(mergedImage, outputLabelmap)
+
+ if previewNode.GetSegmentation().GetNumberOfSegments() != self.selectedSegmentIds.GetNumberOfValues():
+ # first update (or number of segments changed), need a full reinitialization
+ previewNode.GetSegmentation().RemoveAllSegments()
+
+ for index in range(self.selectedSegmentIds.GetNumberOfValues()):
+ segmentID = self.selectedSegmentIds.GetValue(index)
+
+ previewSegment = previewNode.GetSegmentation().GetSegment(segmentID)
+ if not previewSegment:
+ inputSegment = segmentationNode.GetSegmentation().GetSegment(segmentID)
+
+ previewSegment = vtkSegmentationCore.vtkSegment()
+ previewSegment.SetName(inputSegment.GetName())
+ previewSegment.SetColor(inputSegment.GetColor())
+ previewNode.GetSegmentation().AddSegment(previewSegment, segmentID)
+
+ labelValue = index + 1 # n-th segment label value = n + 1 (background label value is 0)
+ previewSegment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(), outputLabelmap)
+ previewSegment.SetLabelValue(labelValue)
+
+ # Automatically hide result segments that are background (all eight corners are non-zero)
+ previewNode.GetDisplayNode().SetSegmentVisibility3D(segmentID, not self.isBackgroundLabelmap(outputLabelmap, labelValue))
+
+ # If the preview was reset, we need to restore the visibility options
+ self.setPreviewOpacity(previewOpacity)
+ self.setPreviewShow3D(previewShow3D)
+
+ self.updateGUIFromMRML()
ResultPreviewNodeReferenceRole = "SegmentationResultPreview"
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py
index 00e17e29a1c..8cfd7d59b5f 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorEffect.py
@@ -8,86 +8,86 @@
#
class AbstractScriptedSegmentEditorEffect:
- """ Abstract scripted segment editor effects for effects implemented in python
-
- USAGE:
- 1. Instantiation and registration
- Instantiate segment editor effect adaptor class from
- module (e.g. from setup function), and set python source:
- > import qSlicerSegmentationsEditorEffectsPythonQt as effects
- > scriptedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- > scriptedEffect.setPythonSource(MyEffect.filePath)
- > scriptedEffect.self().register()
- If effect name is added to slicer.modules.segmenteditorscriptedeffectnames
- list then the above instantiation and registration steps are not necessary,
- as the SegmentEditor module do all these.
-
- 2. Call host C++ implementation using
- > self.scriptedEffect.functionName()
-
- 2.a. Most frequently used such methods are:
- Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
- Add options widget: addOptionsWidget
- Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
- Convenience getters: renderWindow, renderer, viewNode
-
- 2.b. Always call API functions (the ones that are defined in the adaptor
- class qSlicerSegmentEditorScriptedEffect) using the adaptor accessor:
- > self.scriptedEffect.updateGUIFromMRML()
-
- 3. To prevent deactivation of an effect by clicking place fiducial toolbar button,
- override interactionNodeModified(self, interactionNode)
-
- An example for a generic effect is the ThresholdEffect
-
- """
-
- def __init__(self, scriptedEffect):
- self.scriptedEffect = scriptedEffect
-
- def register(self):
- effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance()
- effectFactorySingleton.registerEffect(self.scriptedEffect)
-
- #
- # Utility functions for convenient coordinate transformations
- #
- def rasToXy(self, ras, viewWidget):
- rasVector = qt.QVector3D(ras[0], ras[1], ras[2])
- xyPoint = self.scriptedEffect.rasToXy(rasVector, viewWidget)
- return [xyPoint.x(), xyPoint.y()]
-
- def xyzToRas(self, xyz, viewWidget):
- xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2])
- rasVector = self.scriptedEffect.xyzToRas(xyzVector, viewWidget)
- return [rasVector.x(), rasVector.y(), rasVector.z()]
-
- def xyToRas(self, xy, viewWidget):
- xyPoint = qt.QPoint(xy[0], xy[1])
- rasVector = self.scriptedEffect.xyToRas(xyPoint, viewWidget)
- return [rasVector.x(), rasVector.y(), rasVector.z()]
-
- def xyzToIjk(self, xyz, viewWidget, image, parentTransformNode=None):
- xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2])
- ijkVector = self.scriptedEffect.xyzToIjk(xyzVector, viewWidget, image, parentTransformNode)
- return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())]
-
- def xyToIjk(self, xy, viewWidget, image, parentTransformNode=None):
- xyPoint = qt.QPoint(xy[0], xy[1])
- ijkVector = self.scriptedEffect.xyToIjk(xyPoint, viewWidget, image, parentTransformNode)
- return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())]
-
- def setWidgetMinMaxStepFromImageSpacing(self, spinbox, imageData):
- # Set spinbox minimum, maximum, and step size from vtkImageData spacing:
- # Set widget minimum spacing and step size to be 1/10th or less than minimum spacing
- # Set widget minimum spacing to be 100x or more than minimum spacing
- if not imageData:
- return
- import math
- spinbox.unitAwareProperties &= ~(slicer.qMRMLSpinBox.MinimumValue | slicer.qMRMLSpinBox.MaximumValue | slicer.qMRMLSpinBox.Precision)
- stepSize = 10**(math.floor(math.log10(min(imageData.GetSpacing()) / 10.0)))
- spinbox.minimum = stepSize
- spinbox.maximum = 10**(math.ceil(math.log10(max(imageData.GetSpacing()) * 100.0)))
- spinbox.singleStep = stepSize
- # number of decimals is set to be able to show the step size (e.g., stepSize = 0.01 => decimals = 2)
- spinbox.decimals = max(int(-math.floor(math.log10(stepSize))), 0)
+ """ Abstract scripted segment editor effects for effects implemented in python
+
+ USAGE:
+ 1. Instantiation and registration
+ Instantiate segment editor effect adaptor class from
+ module (e.g. from setup function), and set python source:
+ > import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ > scriptedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ > scriptedEffect.setPythonSource(MyEffect.filePath)
+ > scriptedEffect.self().register()
+ If effect name is added to slicer.modules.segmenteditorscriptedeffectnames
+ list then the above instantiation and registration steps are not necessary,
+ as the SegmentEditor module do all these.
+
+ 2. Call host C++ implementation using
+ > self.scriptedEffect.functionName()
+
+ 2.a. Most frequently used such methods are:
+ Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
+ Add options widget: addOptionsWidget
+ Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
+ Convenience getters: renderWindow, renderer, viewNode
+
+ 2.b. Always call API functions (the ones that are defined in the adaptor
+ class qSlicerSegmentEditorScriptedEffect) using the adaptor accessor:
+ > self.scriptedEffect.updateGUIFromMRML()
+
+ 3. To prevent deactivation of an effect by clicking place fiducial toolbar button,
+ override interactionNodeModified(self, interactionNode)
+
+ An example for a generic effect is the ThresholdEffect
+
+ """
+
+ def __init__(self, scriptedEffect):
+ self.scriptedEffect = scriptedEffect
+
+ def register(self):
+ effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance()
+ effectFactorySingleton.registerEffect(self.scriptedEffect)
+
+ #
+ # Utility functions for convenient coordinate transformations
+ #
+ def rasToXy(self, ras, viewWidget):
+ rasVector = qt.QVector3D(ras[0], ras[1], ras[2])
+ xyPoint = self.scriptedEffect.rasToXy(rasVector, viewWidget)
+ return [xyPoint.x(), xyPoint.y()]
+
+ def xyzToRas(self, xyz, viewWidget):
+ xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2])
+ rasVector = self.scriptedEffect.xyzToRas(xyzVector, viewWidget)
+ return [rasVector.x(), rasVector.y(), rasVector.z()]
+
+ def xyToRas(self, xy, viewWidget):
+ xyPoint = qt.QPoint(xy[0], xy[1])
+ rasVector = self.scriptedEffect.xyToRas(xyPoint, viewWidget)
+ return [rasVector.x(), rasVector.y(), rasVector.z()]
+
+ def xyzToIjk(self, xyz, viewWidget, image, parentTransformNode=None):
+ xyzVector = qt.QVector3D(xyz[0], xyz[1], xyz[2])
+ ijkVector = self.scriptedEffect.xyzToIjk(xyzVector, viewWidget, image, parentTransformNode)
+ return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())]
+
+ def xyToIjk(self, xy, viewWidget, image, parentTransformNode=None):
+ xyPoint = qt.QPoint(xy[0], xy[1])
+ ijkVector = self.scriptedEffect.xyToIjk(xyPoint, viewWidget, image, parentTransformNode)
+ return [int(ijkVector.x()), int(ijkVector.y()), int(ijkVector.z())]
+
+ def setWidgetMinMaxStepFromImageSpacing(self, spinbox, imageData):
+ # Set spinbox minimum, maximum, and step size from vtkImageData spacing:
+ # Set widget minimum spacing and step size to be 1/10th or less than minimum spacing
+ # Set widget minimum spacing to be 100x or more than minimum spacing
+ if not imageData:
+ return
+ import math
+ spinbox.unitAwareProperties &= ~(slicer.qMRMLSpinBox.MinimumValue | slicer.qMRMLSpinBox.MaximumValue | slicer.qMRMLSpinBox.Precision)
+ stepSize = 10**(math.floor(math.log10(min(imageData.GetSpacing()) / 10.0)))
+ spinbox.minimum = stepSize
+ spinbox.maximum = 10**(math.ceil(math.log10(max(imageData.GetSpacing()) * 100.0)))
+ spinbox.singleStep = stepSize
+ # number of decimals is set to be able to show the step size (e.g., stepSize = 0.01 => decimals = 2)
+ spinbox.decimals = max(int(-math.floor(math.log10(stepSize))), 0)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py
index dd7d1806f30..8549ff5d31f 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorLabelEffect.py
@@ -11,34 +11,34 @@
#
class AbstractScriptedSegmentEditorLabelEffect(AbstractScriptedSegmentEditorEffect):
- """ Abstract scripted segment editor label effects for effects implemented in python
+ """ Abstract scripted segment editor label effects for effects implemented in python
- USAGE:
- 1. Instantiation and registration
- Instantiate segment editor label effect adaptor class from
- module (e.g. from setup function), and set python source:
- > import qSlicerSegmentationsEditorEffectsPythonQt as effects
- > scriptedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
- > scriptedEffect.setPythonSource(MyLabelEffect.filePath)
- Registration is automatic
+ USAGE:
+ 1. Instantiation and registration
+ Instantiate segment editor label effect adaptor class from
+ module (e.g. from setup function), and set python source:
+ > import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ > scriptedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
+ > scriptedEffect.setPythonSource(MyLabelEffect.filePath)
+ Registration is automatic
- 2. Call host C++ implementation using
- > self.scriptedEffect.functionName()
+ 2. Call host C++ implementation using
+ > self.scriptedEffect.functionName()
- 2.a. Most frequently used such methods are:
- Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
- Add options widget: addOptionsWidget
- Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
- Convenience getters: renderWindow, renderer, viewNode
- Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation)
+ 2.a. Most frequently used such methods are:
+ Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
+ Add options widget: addOptionsWidget
+ Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
+ Convenience getters: renderWindow, renderer, viewNode
+ Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation)
- 2.b. Always call API functions (the ones that are defined in the adaptor
- class qSlicerSegmentEditorScriptedLabelEffect) using the adaptor accessor:
- > self.scriptedEffect.updateGUIFromMRML()
+ 2.b. Always call API functions (the ones that are defined in the adaptor
+ class qSlicerSegmentEditorScriptedLabelEffect) using the adaptor accessor:
+ > self.scriptedEffect.updateGUIFromMRML()
- An example for a generic effect is the DrawEffect
+ An example for a generic effect is the DrawEffect
- """
+ """
- def __init__(self, scriptedEffect):
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py
index 6349624f96c..b5b207cbe94 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/AbstractScriptedSegmentEditorPaintEffect.py
@@ -11,34 +11,34 @@
#
class AbstractScriptedSegmentEditorPaintEffect(AbstractScriptedSegmentEditorEffect):
- """ Abstract scripted segment editor Paint effects for effects implemented in python
+ """ Abstract scripted segment editor Paint effects for effects implemented in python
- USAGE:
- 1. Instantiation and registration
- Instantiate segment editor paint effect adaptor class from
- module (e.g. from setup function), and set python source:
- > import qSlicerSegmentationsEditorEffectsPythonQt as effects
- > scriptedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None)
- > scriptedEffect.setPythonSource(MyPaintEffect.filePath)
- Registration is automatic
+ USAGE:
+ 1. Instantiation and registration
+ Instantiate segment editor paint effect adaptor class from
+ module (e.g. from setup function), and set python source:
+ > import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ > scriptedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None)
+ > scriptedEffect.setPythonSource(MyPaintEffect.filePath)
+ Registration is automatic
- 2. Call host C++ implementation using
- > self.scriptedEffect.functionName()
+ 2. Call host C++ implementation using
+ > self.scriptedEffect.functionName()
- 2.a. Most frequently used such methods are:
- Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
- Add options widget: addOptionsWidget
- Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
- Convenience getters: renderWindow, renderer, viewNode
- Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation)
+ 2.a. Most frequently used such methods are:
+ Parameter get/set: parameter, integerParameter, doubleParameter, setParameter
+ Add options widget: addOptionsWidget
+ Coordinate transforms: rasToXy, xyzToRas, xyToRas, xyzToIjk, xyToIjk
+ Convenience getters: renderWindow, renderer, viewNode
+ Geometry getters: imageToWorldMatrix (for volume node and for oriented image data with segmentation)
- 2.b. Always call API functions (the ones that are defined in the adaptor
- class qSlicerSegmentEditorScriptedPaintEffect) using the adaptor accessor:
- > self.scriptedEffect.updateGUIFromMRML()
+ 2.b. Always call API functions (the ones that are defined in the adaptor
+ class qSlicerSegmentEditorScriptedPaintEffect) using the adaptor accessor:
+ > self.scriptedEffect.updateGUIFromMRML()
- An example for a generic effect is the DrawEffect
+ An example for a generic effect is the DrawEffect
- """
+ """
- def __init__(self, scriptedEffect):
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py
index 9917878ec6c..4e9e0e84fe8 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorDrawEffect.py
@@ -10,29 +10,29 @@
class SegmentEditorDrawEffect(AbstractScriptedSegmentEditorLabelEffect):
- """ DrawEffect is a LabelEffect implementing the interactive draw
- tool in the segment editor
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Draw'
- self.drawPipelines = {}
- AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Draw.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Draw segment outline in slice viewers
.
+ """ DrawEffect is a LabelEffect implementing the interactive draw
+ tool in the segment editor
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Draw'
+ self.drawPipelines = {}
+ AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Draw.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Draw segment outline in slice viewers
.
- Left-click: add point.
- Left-button drag-and-drop: add multiple points.
@@ -40,283 +40,283 @@ def helpText(self):
- Double-left-click or right-click or a or enter: apply outline.
"""
- def deactivate(self):
- # Clear draw pipelines
- for sliceWidget, pipeline in self.drawPipelines.items():
- self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
- self.drawPipelines = {}
-
- def setupOptionsFrame(self):
- pass
-
- def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
- abortEvent = False
-
- # Only allow for slice views
- if viewWidget.className() != "qMRMLSliceWidget":
- return abortEvent
- # Get draw pipeline for current slice
- pipeline = self.pipelineForWidget(viewWidget)
- if pipeline is None:
- return abortEvent
-
- anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
-
- if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
- # Make sure the user wants to do the operation, even if the segment is not visible
- confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
- if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog:
- # If user had to move the mouse to click on the popup, so we cannot continue with painting
- # from the current mouse position. User will need to click again.
- # The dialog is not displayed again for the same segment.
+ def deactivate(self):
+ # Clear draw pipelines
+ for sliceWidget, pipeline in self.drawPipelines.items():
+ self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
+ self.drawPipelines = {}
+
+ def setupOptionsFrame(self):
+ pass
+
+ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
+ abortEvent = False
+
+ # Only allow for slice views
+ if viewWidget.className() != "qMRMLSliceWidget":
+ return abortEvent
+ # Get draw pipeline for current slice
+ pipeline = self.pipelineForWidget(viewWidget)
+ if pipeline is None:
+ return abortEvent
+
+ anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
+
+ if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ confirmedEditingAllowed = self.scriptedEffect.confirmCurrentSegmentVisible()
+ if confirmedEditingAllowed == self.scriptedEffect.NotConfirmed or confirmedEditingAllowed == self.scriptedEffect.ConfirmedWithDialog:
+ # If user had to move the mouse to click on the popup, so we cannot continue with painting
+ # from the current mouse position. User will need to click again.
+ # The dialog is not displayed again for the same segment.
+ return abortEvent
+ pipeline.actionState = "drawing"
+ self.scriptedEffect.cursorOff(viewWidget)
+ xy = callerInteractor.GetEventPosition()
+ ras = self.xyToRas(xy, viewWidget)
+ pipeline.addPoint(ras)
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
+ if pipeline.actionState == "drawing":
+ pipeline.actionState = "moving"
+ self.scriptedEffect.cursorOn(viewWidget)
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.RightButtonPressEvent and not anyModifierKeyPressed:
+ pipeline.actionState = "finishing"
+ sliceNode = viewWidget.sliceLogic().GetSliceNode()
+ pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime()
+ abortEvent = True
+ elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed):
+ abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1)
+ sliceNode = viewWidget.sliceLogic().GetSliceNode()
+ if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2:
+ pipeline.apply()
+ pipeline.actionState = ""
+ elif eventId == vtk.vtkCommand.MouseMoveEvent:
+ if pipeline.actionState == "drawing":
+ xy = callerInteractor.GetEventPosition()
+ ras = self.xyToRas(xy, viewWidget)
+ pipeline.addPoint(ras)
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.KeyPressEvent:
+ key = callerInteractor.GetKeySym()
+ if key == 'a' or key == 'Return':
+ pipeline.apply()
+ abortEvent = True
+ if key == 'x':
+ pipeline.deleteLastPoint()
+ abortEvent = True
+ else:
+ pass
+
+ pipeline.positionActors()
return abortEvent
- pipeline.actionState = "drawing"
- self.scriptedEffect.cursorOff(viewWidget)
- xy = callerInteractor.GetEventPosition()
- ras = self.xyToRas(xy, viewWidget)
- pipeline.addPoint(ras)
- abortEvent = True
- elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
- if pipeline.actionState == "drawing":
- pipeline.actionState = "moving"
- self.scriptedEffect.cursorOn(viewWidget)
- abortEvent = True
- elif eventId == vtk.vtkCommand.RightButtonPressEvent and not anyModifierKeyPressed:
- pipeline.actionState = "finishing"
- sliceNode = viewWidget.sliceLogic().GetSliceNode()
- pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime()
- abortEvent = True
- elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed):
- abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1)
- sliceNode = viewWidget.sliceLogic().GetSliceNode()
- if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2:
- pipeline.apply()
- pipeline.actionState = ""
- elif eventId == vtk.vtkCommand.MouseMoveEvent:
- if pipeline.actionState == "drawing":
- xy = callerInteractor.GetEventPosition()
- ras = self.xyToRas(xy, viewWidget)
- pipeline.addPoint(ras)
- abortEvent = True
- elif eventId == vtk.vtkCommand.KeyPressEvent:
- key = callerInteractor.GetKeySym()
- if key == 'a' or key == 'Return':
- pipeline.apply()
- abortEvent = True
- if key == 'x':
- pipeline.deleteLastPoint()
- abortEvent = True
- else:
- pass
-
- pipeline.positionActors()
- return abortEvent
-
- def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
- if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'):
- # Get draw pipeline for current slice
- pipeline = self.pipelineForWidget(viewWidget)
- if pipeline is None:
- logging.error('processViewNodeEvents: Invalid pipeline')
- return
-
- # Make sure all points are on the current slice plane.
- # If the SliceToRAS has been modified, then we're on a different plane
- sliceLogic = viewWidget.sliceLogic()
- lineMode = "solid"
- currentSliceOffset = sliceLogic.GetSliceOffset()
- if pipeline.activeSliceOffset:
- offset = abs(currentSliceOffset - pipeline.activeSliceOffset)
- if offset > 0.01:
- lineMode = "dashed"
- pipeline.setLineMode(lineMode)
- pipeline.positionActors()
-
- def pipelineForWidget(self, sliceWidget):
- if sliceWidget in self.drawPipelines:
- return self.drawPipelines[sliceWidget]
-
- # Create pipeline if does not yet exist
- pipeline = DrawPipeline(self.scriptedEffect, sliceWidget)
-
- # Add actor
- renderer = self.scriptedEffect.renderer(sliceWidget)
- if renderer is None:
- logging.error("pipelineForWidget: Failed to get renderer!")
- return None
- self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
-
- self.drawPipelines[sliceWidget] = pipeline
- return pipeline
+
+ def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
+ if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'):
+ # Get draw pipeline for current slice
+ pipeline = self.pipelineForWidget(viewWidget)
+ if pipeline is None:
+ logging.error('processViewNodeEvents: Invalid pipeline')
+ return
+
+ # Make sure all points are on the current slice plane.
+ # If the SliceToRAS has been modified, then we're on a different plane
+ sliceLogic = viewWidget.sliceLogic()
+ lineMode = "solid"
+ currentSliceOffset = sliceLogic.GetSliceOffset()
+ if pipeline.activeSliceOffset:
+ offset = abs(currentSliceOffset - pipeline.activeSliceOffset)
+ if offset > 0.01:
+ lineMode = "dashed"
+ pipeline.setLineMode(lineMode)
+ pipeline.positionActors()
+
+ def pipelineForWidget(self, sliceWidget):
+ if sliceWidget in self.drawPipelines:
+ return self.drawPipelines[sliceWidget]
+
+ # Create pipeline if does not yet exist
+ pipeline = DrawPipeline(self.scriptedEffect, sliceWidget)
+
+ # Add actor
+ renderer = self.scriptedEffect.renderer(sliceWidget)
+ if renderer is None:
+ logging.error("pipelineForWidget: Failed to get renderer!")
+ return None
+ self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
+
+ self.drawPipelines[sliceWidget] = pipeline
+ return pipeline
#
# DrawPipeline
#
class DrawPipeline:
- """ Visualization objects and pipeline for each slice view for drawing
- """
-
- def __init__(self, scriptedEffect, sliceWidget):
- self.scriptedEffect = scriptedEffect
- self.sliceWidget = sliceWidget
- self.activeSliceOffset = None
- self.lastInsertSliceNodeMTime = None
- self.actionState = None
-
- self.xyPoints = vtk.vtkPoints()
- self.rasPoints = vtk.vtkPoints()
- self.polyData = self.createPolyData()
-
- self.mapper = vtk.vtkPolyDataMapper2D()
- self.actor = vtk.vtkTexturedActor2D()
- self.mapper.SetInputData(self.polyData)
- self.actor.SetMapper(self.mapper)
- actorProperty = self.actor.GetProperty()
- actorProperty.SetColor(1, 1, 0)
- actorProperty.SetLineWidth(1)
-
- self.createStippleTexture(0xAAAA, 8)
-
- def createStippleTexture(self, lineStipplePattern, lineStippleRepeat):
- self.tcoords = vtk.vtkDoubleArray()
- self.texture = vtk.vtkTexture()
-
- # Create texture
- dimension = 16 * lineStippleRepeat
-
- image = vtk.vtkImageData()
- image.SetDimensions(dimension, 1, 1)
- image.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 4)
- image.SetExtent(0, dimension - 1, 0, 0, 0, 0)
- on = 255
- off = 0
- i_dim = 0
- while i_dim < dimension:
- for i in range(0, 16):
- mask = (1 << i)
- bit = (lineStipplePattern & mask) >> i
- value = bit
- if value == 0:
- for j in range(0, lineStippleRepeat):
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, off)
- i_dim += 1
- else:
- for j in range(0, lineStippleRepeat):
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
- image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, on)
- i_dim += 1
- self.texture.SetInputData(image)
- self.texture.InterpolateOff()
- self.texture.RepeatOn()
-
- def createPolyData(self):
- # Make an empty single-polyline polydata
- polyData = vtk.vtkPolyData()
- polyData.SetPoints(self.xyPoints)
- lines = vtk.vtkCellArray()
- polyData.SetLines(lines)
- return polyData
-
- def addPoint(self, ras):
- # Add a world space point to the current outline
-
- # Store active slice when first point is added
- sliceLogic = self.sliceWidget.sliceLogic()
- currentSliceOffset = sliceLogic.GetSliceOffset()
- if not self.activeSliceOffset:
- self.activeSliceOffset = currentSliceOffset
- self.setLineMode("solid")
-
- # Don't allow adding points on except on the active slice
- # (where first point was laid down)
- if self.activeSliceOffset != currentSliceOffset: return
-
- # Keep track of node state (in case of pan/zoom)
- sliceNode = sliceLogic.GetSliceNode()
- self.lastInsertSliceNodeMTime = sliceNode.GetMTime()
-
- p = self.rasPoints.InsertNextPoint(ras)
- if p > 0:
- idList = vtk.vtkIdList()
- idList.InsertNextId(p - 1)
- idList.InsertNextId(p)
- self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
-
- def setLineMode(self, mode="solid"):
- actorProperty = self.actor.GetProperty()
- if mode == "solid":
- self.polyData.GetPointData().SetTCoords(None)
- self.actor.SetTexture(None)
- elif mode == "dashed":
- # Create texture coordinates
- self.tcoords.SetNumberOfComponents(1)
- self.tcoords.SetNumberOfTuples(self.polyData.GetNumberOfPoints())
- for i in range(0, self.polyData.GetNumberOfPoints()):
- value = i * 0.5
- self.tcoords.SetTypedTuple(i, [value])
- self.polyData.GetPointData().SetTCoords(self.tcoords)
- self.actor.SetTexture(self.texture)
-
- def positionActors(self):
- # Update draw feedback to follow slice node
- sliceLogic = self.sliceWidget.sliceLogic()
- sliceNode = sliceLogic.GetSliceNode()
- rasToXY = vtk.vtkTransform()
- rasToXY.SetMatrix(sliceNode.GetXYToRAS())
- rasToXY.Inverse()
- self.xyPoints.Reset()
- rasToXY.TransformPoints(self.rasPoints, self.xyPoints)
- self.polyData.Modified()
- self.sliceWidget.sliceView().scheduleRender()
-
- def apply(self):
- lines = self.polyData.GetLines()
- lineExists = lines.GetNumberOfCells() > 0
- if lineExists:
- # Close the polyline back to the first point
- idList = vtk.vtkIdList()
- idList.InsertNextId(self.polyData.GetNumberOfPoints() - 1)
- idList.InsertNextId(0)
- self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
-
- # Get modifier labelmap
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
-
- # Apply poly data on modifier labelmap
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- self.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)
-
- self.resetPolyData()
- if lineExists:
- self.scriptedEffect.saveStateForUndo()
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
-
- def resetPolyData(self):
- # Return the polyline to initial state with no points
- lines = self.polyData.GetLines()
- lines.Initialize()
- self.xyPoints.Reset()
- self.rasPoints.Reset()
- self.activeSliceOffset = None
-
- def deleteLastPoint(self):
- # Unwind through addPoint list back to empty polydata
- pcount = self.rasPoints.GetNumberOfPoints()
- if pcount <= 0:
- return
-
- pcount = pcount - 1
- self.rasPoints.SetNumberOfPoints(pcount)
-
- cellCount = self.polyData.GetNumberOfCells()
- if cellCount > 0:
- self.polyData.DeleteCell(cellCount - 1)
- self.polyData.RemoveDeletedCells()
-
- self.positionActors()
+ """ Visualization objects and pipeline for each slice view for drawing
+ """
+
+ def __init__(self, scriptedEffect, sliceWidget):
+ self.scriptedEffect = scriptedEffect
+ self.sliceWidget = sliceWidget
+ self.activeSliceOffset = None
+ self.lastInsertSliceNodeMTime = None
+ self.actionState = None
+
+ self.xyPoints = vtk.vtkPoints()
+ self.rasPoints = vtk.vtkPoints()
+ self.polyData = self.createPolyData()
+
+ self.mapper = vtk.vtkPolyDataMapper2D()
+ self.actor = vtk.vtkTexturedActor2D()
+ self.mapper.SetInputData(self.polyData)
+ self.actor.SetMapper(self.mapper)
+ actorProperty = self.actor.GetProperty()
+ actorProperty.SetColor(1, 1, 0)
+ actorProperty.SetLineWidth(1)
+
+ self.createStippleTexture(0xAAAA, 8)
+
+ def createStippleTexture(self, lineStipplePattern, lineStippleRepeat):
+ self.tcoords = vtk.vtkDoubleArray()
+ self.texture = vtk.vtkTexture()
+
+ # Create texture
+ dimension = 16 * lineStippleRepeat
+
+ image = vtk.vtkImageData()
+ image.SetDimensions(dimension, 1, 1)
+ image.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 4)
+ image.SetExtent(0, dimension - 1, 0, 0, 0, 0)
+ on = 255
+ off = 0
+ i_dim = 0
+ while i_dim < dimension:
+ for i in range(0, 16):
+ mask = (1 << i)
+ bit = (lineStipplePattern & mask) >> i
+ value = bit
+ if value == 0:
+ for j in range(0, lineStippleRepeat):
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, off)
+ i_dim += 1
+ else:
+ for j in range(0, lineStippleRepeat):
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 0, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 1, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 2, on)
+ image.SetScalarComponentFromFloat(i_dim, 0, 0, 3, on)
+ i_dim += 1
+ self.texture.SetInputData(image)
+ self.texture.InterpolateOff()
+ self.texture.RepeatOn()
+
+ def createPolyData(self):
+ # Make an empty single-polyline polydata
+ polyData = vtk.vtkPolyData()
+ polyData.SetPoints(self.xyPoints)
+ lines = vtk.vtkCellArray()
+ polyData.SetLines(lines)
+ return polyData
+
+ def addPoint(self, ras):
+ # Add a world space point to the current outline
+
+ # Store active slice when first point is added
+ sliceLogic = self.sliceWidget.sliceLogic()
+ currentSliceOffset = sliceLogic.GetSliceOffset()
+ if not self.activeSliceOffset:
+ self.activeSliceOffset = currentSliceOffset
+ self.setLineMode("solid")
+
+ # Don't allow adding points on except on the active slice
+ # (where first point was laid down)
+ if self.activeSliceOffset != currentSliceOffset: return
+
+ # Keep track of node state (in case of pan/zoom)
+ sliceNode = sliceLogic.GetSliceNode()
+ self.lastInsertSliceNodeMTime = sliceNode.GetMTime()
+
+ p = self.rasPoints.InsertNextPoint(ras)
+ if p > 0:
+ idList = vtk.vtkIdList()
+ idList.InsertNextId(p - 1)
+ idList.InsertNextId(p)
+ self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
+
+ def setLineMode(self, mode="solid"):
+ actorProperty = self.actor.GetProperty()
+ if mode == "solid":
+ self.polyData.GetPointData().SetTCoords(None)
+ self.actor.SetTexture(None)
+ elif mode == "dashed":
+ # Create texture coordinates
+ self.tcoords.SetNumberOfComponents(1)
+ self.tcoords.SetNumberOfTuples(self.polyData.GetNumberOfPoints())
+ for i in range(0, self.polyData.GetNumberOfPoints()):
+ value = i * 0.5
+ self.tcoords.SetTypedTuple(i, [value])
+ self.polyData.GetPointData().SetTCoords(self.tcoords)
+ self.actor.SetTexture(self.texture)
+
+ def positionActors(self):
+ # Update draw feedback to follow slice node
+ sliceLogic = self.sliceWidget.sliceLogic()
+ sliceNode = sliceLogic.GetSliceNode()
+ rasToXY = vtk.vtkTransform()
+ rasToXY.SetMatrix(sliceNode.GetXYToRAS())
+ rasToXY.Inverse()
+ self.xyPoints.Reset()
+ rasToXY.TransformPoints(self.rasPoints, self.xyPoints)
+ self.polyData.Modified()
+ self.sliceWidget.sliceView().scheduleRender()
+
+ def apply(self):
+ lines = self.polyData.GetLines()
+ lineExists = lines.GetNumberOfCells() > 0
+ if lineExists:
+ # Close the polyline back to the first point
+ idList = vtk.vtkIdList()
+ idList.InsertNextId(self.polyData.GetNumberOfPoints() - 1)
+ idList.InsertNextId(0)
+ self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
+
+ # Get modifier labelmap
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+
+ # Apply poly data on modifier labelmap
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ self.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)
+
+ self.resetPolyData()
+ if lineExists:
+ self.scriptedEffect.saveStateForUndo()
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
+
+ def resetPolyData(self):
+ # Return the polyline to initial state with no points
+ lines = self.polyData.GetLines()
+ lines.Initialize()
+ self.xyPoints.Reset()
+ self.rasPoints.Reset()
+ self.activeSliceOffset = None
+
+ def deleteLastPoint(self):
+ # Unwind through addPoint list back to empty polydata
+ pcount = self.rasPoints.GetNumberOfPoints()
+ if pcount <= 0:
+ return
+
+ pcount = pcount - 1
+ self.rasPoints.SetNumberOfPoints(pcount)
+
+ cellCount = self.polyData.GetNumberOfCells()
+ if cellCount > 0:
+ self.polyData.DeleteCell(cellCount - 1)
+ self.polyData.RemoveDeletedCells()
+
+ self.positionActors()
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py
index 62bcee186d1..7b881a8ec32 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorFillBetweenSlicesEffect.py
@@ -7,29 +7,29 @@
class SegmentEditorFillBetweenSlicesEffect(AbstractScriptedSegmentEditorAutoCompleteEffect):
- """ AutoCompleteEffect is an effect that can create a full segmentation
- from a partial segmentation (not all slices are segmented or only
- part of the target structures are painted).
- """
-
- def __init__(self, scriptedEffect):
- AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
- scriptedEffect.name = 'Fill between slices'
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/FillBetweenSlices.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Interpolate segmentation between slices
. Instructions:
+ """ AutoCompleteEffect is an effect that can create a full segmentation
+ from a partial segmentation (not all slices are segmented or only
+ part of the target structures are painted).
+ """
+
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
+ scriptedEffect.name = 'Fill between slices'
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/FillBetweenSlices.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Interpolate segmentation between slices
. Instructions:
- Create complete segmentation on selected slices using any editor effect.
Segmentation will only expanded if a slice is segmented but none of the direct neighbors are segmented, therefore
@@ -41,12 +41,12 @@ def helpText(self):
The effect uses morphological contour interpolation method.
"""
- def computePreviewLabelmap(self, mergedImage, outputLabelmap):
- import vtkITK
- interpolator = vtkITK.vtkITKMorphologicalContourInterpolator()
- interpolator.SetInputData(mergedImage)
- interpolator.Update()
- outputLabelmap.DeepCopy(interpolator.GetOutput())
- imageToWorld = vtk.vtkMatrix4x4()
- mergedImage.GetImageToWorldMatrix(imageToWorld)
- outputLabelmap.SetImageToWorldMatrix(imageToWorld)
+ def computePreviewLabelmap(self, mergedImage, outputLabelmap):
+ import vtkITK
+ interpolator = vtkITK.vtkITKMorphologicalContourInterpolator()
+ interpolator.SetInputData(mergedImage)
+ interpolator.Update()
+ outputLabelmap.DeepCopy(interpolator.GetOutput())
+ imageToWorld = vtk.vtkMatrix4x4()
+ mergedImage.GetImageToWorldMatrix(imageToWorld)
+ outputLabelmap.SetImageToWorldMatrix(imageToWorld)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py
index 4525bbad9a6..4f038469892 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorGrowFromSeedsEffect.py
@@ -11,33 +11,33 @@
class SegmentEditorGrowFromSeedsEffect(AbstractScriptedSegmentEditorAutoCompleteEffect):
- """ AutoCompleteEffect is an effect that can create a full segmentation
- from a partial segmentation (not all slices are segmented or only
- part of the target structures are painted).
- """
-
- def __init__(self, scriptedEffect):
- AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
- scriptedEffect.name = 'Grow from seeds'
- self.minimumNumberOfSegments = 2
- self.clippedMasterImageDataRequired = True # master volume intensities are used by this effect
- self.clippedMaskImageDataRequired = True # masking is used
- self.growCutFilter = None
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/GrowFromSeeds.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Growing segments to create complete segmentation
.
+ """ AutoCompleteEffect is an effect that can create a full segmentation
+ from a partial segmentation (not all slices are segmented or only
+ part of the target structures are painted).
+ """
+
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.__init__(self, scriptedEffect)
+ scriptedEffect.name = 'Grow from seeds'
+ self.minimumNumberOfSegments = 2
+ self.clippedMasterImageDataRequired = True # master volume intensities are used by this effect
+ self.clippedMaskImageDataRequired = True # masking is used
+ self.growCutFilter = None
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/GrowFromSeeds.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Growing segments to create complete segmentation
.
Location, size, and shape of initial segments and content of master volume are taken into account.
Final segment boundaries will be placed where master volume brightness changes abruptly. Instructions:
@@ -53,86 +53,86 @@ def helpText(self):
The effect uses fast grow-cut method.
"""
- def reset(self):
- self.growCutFilter = None
- AbstractScriptedSegmentEditorAutoCompleteEffect.reset(self)
- self.updateGUIFromMRML()
-
- def setupOptionsFrame(self):
- AbstractScriptedSegmentEditorAutoCompleteEffect.setupOptionsFrame(self)
-
- # Object scale slider
- self.seedLocalityFactorSlider = slicer.qMRMLSliderWidget()
- self.seedLocalityFactorSlider.setMRMLScene(slicer.mrmlScene)
- self.seedLocalityFactorSlider.minimum = 0
- self.seedLocalityFactorSlider.maximum = 10
- self.seedLocalityFactorSlider.value = 0.0
- self.seedLocalityFactorSlider.decimals = 1
- self.seedLocalityFactorSlider.singleStep = 0.1
- self.seedLocalityFactorSlider.pageStep = 1.0
- self.seedLocalityFactorSlider.setToolTip('Increasing this value makes the effect of seeds more localized,'
- ' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.'
- ' The value is specified as an additional "intensity level difference" per "unit distance."')
- self.scriptedEffect.addLabeledOptionsWidget("Seed locality:", self.seedLocalityFactorSlider)
- self.seedLocalityFactorSlider.connect('valueChanged(double)', self.updateAlgorithmParameterFromGUI)
-
- def setMRMLDefaults(self):
- AbstractScriptedSegmentEditorAutoCompleteEffect.setMRMLDefaults(self)
- self.scriptedEffect.setParameterDefault("SeedLocalityFactor", 0.0)
-
- def updateGUIFromMRML(self):
- AbstractScriptedSegmentEditorAutoCompleteEffect.updateGUIFromMRML(self)
- if self.scriptedEffect.parameterDefined("SeedLocalityFactor"):
- seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor")
- else:
- seedLocalityFactor = 0.0
- wasBlocked = self.seedLocalityFactorSlider.blockSignals(True)
- self.seedLocalityFactorSlider.value = abs(seedLocalityFactor)
- self.seedLocalityFactorSlider.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- AbstractScriptedSegmentEditorAutoCompleteEffect.updateMRMLFromGUI(self)
- self.scriptedEffect.setParameter("SeedLocalityFactor", self.seedLocalityFactorSlider.value)
-
- def updateAlgorithmParameterFromGUI(self):
- self.updateMRMLFromGUI()
-
- # Trigger preview update
- if self.getPreviewNode():
- self.delayedAutoUpdateTimer.start()
-
- def computePreviewLabelmap(self, mergedImage, outputLabelmap):
- import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic
-
- if not self.growCutFilter:
- self.growCutFilter = vtkSlicerSegmentationsModuleLogic.vtkImageGrowCutSegment()
- self.growCutFilter.SetIntensityVolume(self.clippedMasterImageData)
- self.growCutFilter.SetMaskVolume(self.clippedMaskImageData)
- maskExtent = self.clippedMaskImageData.GetExtent() if self.clippedMaskImageData else None
- if maskExtent is not None and maskExtent[0] <= maskExtent[1] and maskExtent[2] <= maskExtent[3] and maskExtent[4] <= maskExtent[5]:
- # Mask is used.
- # Grow the extent more, as background segment does not surround region of interest.
- self.extentGrowthRatio = 0.50
- else:
- # No masking is used.
- # Background segment is expected to surround region of interest, so narrower margin is enough.
- self.extentGrowthRatio = 0.20
-
- if self.scriptedEffect.parameterDefined("SeedLocalityFactor"):
- seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor")
- else:
- seedLocalityFactor = 0.0
- self.growCutFilter.SetDistancePenalty(seedLocalityFactor)
- self.growCutFilter.SetSeedLabelVolume(mergedImage)
- startTime = time.time()
- self.growCutFilter.Update()
- logging.info('Grow-cut operation on volume of {}x{}x{} voxels was completed in {:3.1f} seconds.'.format(
- self.clippedMasterImageData.GetDimensions()[0],
- self.clippedMasterImageData.GetDimensions()[1],
- self.clippedMasterImageData.GetDimensions()[2],
- time.time() - startTime))
-
- outputLabelmap.DeepCopy(self.growCutFilter.GetOutput())
- imageToWorld = vtk.vtkMatrix4x4()
- mergedImage.GetImageToWorldMatrix(imageToWorld)
- outputLabelmap.SetImageToWorldMatrix(imageToWorld)
+ def reset(self):
+ self.growCutFilter = None
+ AbstractScriptedSegmentEditorAutoCompleteEffect.reset(self)
+ self.updateGUIFromMRML()
+
+ def setupOptionsFrame(self):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.setupOptionsFrame(self)
+
+ # Object scale slider
+ self.seedLocalityFactorSlider = slicer.qMRMLSliderWidget()
+ self.seedLocalityFactorSlider.setMRMLScene(slicer.mrmlScene)
+ self.seedLocalityFactorSlider.minimum = 0
+ self.seedLocalityFactorSlider.maximum = 10
+ self.seedLocalityFactorSlider.value = 0.0
+ self.seedLocalityFactorSlider.decimals = 1
+ self.seedLocalityFactorSlider.singleStep = 0.1
+ self.seedLocalityFactorSlider.pageStep = 1.0
+ self.seedLocalityFactorSlider.setToolTip('Increasing this value makes the effect of seeds more localized,'
+ ' thereby reducing leaks, but requires seed regions to be more evenly distributed in the image.'
+ ' The value is specified as an additional "intensity level difference" per "unit distance."')
+ self.scriptedEffect.addLabeledOptionsWidget("Seed locality:", self.seedLocalityFactorSlider)
+ self.seedLocalityFactorSlider.connect('valueChanged(double)', self.updateAlgorithmParameterFromGUI)
+
+ def setMRMLDefaults(self):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.setMRMLDefaults(self)
+ self.scriptedEffect.setParameterDefault("SeedLocalityFactor", 0.0)
+
+ def updateGUIFromMRML(self):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.updateGUIFromMRML(self)
+ if self.scriptedEffect.parameterDefined("SeedLocalityFactor"):
+ seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor")
+ else:
+ seedLocalityFactor = 0.0
+ wasBlocked = self.seedLocalityFactorSlider.blockSignals(True)
+ self.seedLocalityFactorSlider.value = abs(seedLocalityFactor)
+ self.seedLocalityFactorSlider.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ AbstractScriptedSegmentEditorAutoCompleteEffect.updateMRMLFromGUI(self)
+ self.scriptedEffect.setParameter("SeedLocalityFactor", self.seedLocalityFactorSlider.value)
+
+ def updateAlgorithmParameterFromGUI(self):
+ self.updateMRMLFromGUI()
+
+ # Trigger preview update
+ if self.getPreviewNode():
+ self.delayedAutoUpdateTimer.start()
+
+ def computePreviewLabelmap(self, mergedImage, outputLabelmap):
+ import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic
+
+ if not self.growCutFilter:
+ self.growCutFilter = vtkSlicerSegmentationsModuleLogic.vtkImageGrowCutSegment()
+ self.growCutFilter.SetIntensityVolume(self.clippedMasterImageData)
+ self.growCutFilter.SetMaskVolume(self.clippedMaskImageData)
+ maskExtent = self.clippedMaskImageData.GetExtent() if self.clippedMaskImageData else None
+ if maskExtent is not None and maskExtent[0] <= maskExtent[1] and maskExtent[2] <= maskExtent[3] and maskExtent[4] <= maskExtent[5]:
+ # Mask is used.
+ # Grow the extent more, as background segment does not surround region of interest.
+ self.extentGrowthRatio = 0.50
+ else:
+ # No masking is used.
+ # Background segment is expected to surround region of interest, so narrower margin is enough.
+ self.extentGrowthRatio = 0.20
+
+ if self.scriptedEffect.parameterDefined("SeedLocalityFactor"):
+ seedLocalityFactor = self.scriptedEffect.doubleParameter("SeedLocalityFactor")
+ else:
+ seedLocalityFactor = 0.0
+ self.growCutFilter.SetDistancePenalty(seedLocalityFactor)
+ self.growCutFilter.SetSeedLabelVolume(mergedImage)
+ startTime = time.time()
+ self.growCutFilter.Update()
+ logging.info('Grow-cut operation on volume of {}x{}x{} voxels was completed in {:3.1f} seconds.'.format(
+ self.clippedMasterImageData.GetDimensions()[0],
+ self.clippedMasterImageData.GetDimensions()[1],
+ self.clippedMasterImageData.GetDimensions()[2],
+ time.time() - startTime))
+
+ outputLabelmap.DeepCopy(self.growCutFilter.GetOutput())
+ imageToWorld = vtk.vtkMatrix4x4()
+ mergedImage.GetImageToWorldMatrix(imageToWorld)
+ outputLabelmap.SetImageToWorldMatrix(imageToWorld)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py
index 03dc36ee0f3..371f59d2a0d 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorHollowEffect.py
@@ -11,249 +11,249 @@
class SegmentEditorHollowEffect(AbstractScriptedSegmentEditorEffect):
- """This effect makes a segment hollow by replacing it with a shell at the segment boundary"""
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Hollow'
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary."""
-
- def setupOptionsFrame(self):
-
- operationLayout = qt.QVBoxLayout()
-
- self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface")
- self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface")
- self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface")
- operationLayout.addWidget(self.insideSurfaceOptionRadioButton)
- operationLayout.addWidget(self.medialSurfaceOptionRadioButton)
- operationLayout.addWidget(self.outsideSurfaceOptionRadioButton)
- self.insideSurfaceOptionRadioButton.setChecked(True)
-
- self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout)
-
- self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox()
- self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene)
- self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.")
- self.shellThicknessMMSpinBox.quantity = "length"
- self.shellThicknessMMSpinBox.minimum = 0.0
- self.shellThicknessMMSpinBox.value = 3.0
- self.shellThicknessMMSpinBox.singleStep = 1.0
-
- self.shellThicknessLabel = qt.QLabel()
- self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.")
-
- shellThicknessFrame = qt.QHBoxLayout()
- shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox)
- self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame)
- self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel)
-
- self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
- self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply hollow effect to all visible segments in this segmentation node. \
+ """This effect makes a segment hollow by replacing it with a shell at the segment boundary"""
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Hollow'
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Hollow.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Make the selected segment hollow by replacing the segment with a uniform-thickness shell defined by the segment boundary."""
+
+ def setupOptionsFrame(self):
+
+ operationLayout = qt.QVBoxLayout()
+
+ self.insideSurfaceOptionRadioButton = qt.QRadioButton("inside surface")
+ self.medialSurfaceOptionRadioButton = qt.QRadioButton("medial surface")
+ self.outsideSurfaceOptionRadioButton = qt.QRadioButton("outside surface")
+ operationLayout.addWidget(self.insideSurfaceOptionRadioButton)
+ operationLayout.addWidget(self.medialSurfaceOptionRadioButton)
+ operationLayout.addWidget(self.outsideSurfaceOptionRadioButton)
+ self.insideSurfaceOptionRadioButton.setChecked(True)
+
+ self.scriptedEffect.addLabeledOptionsWidget("Use current segment as:", operationLayout)
+
+ self.shellThicknessMMSpinBox = slicer.qMRMLSpinBox()
+ self.shellThicknessMMSpinBox.setMRMLScene(slicer.mrmlScene)
+ self.shellThicknessMMSpinBox.setToolTip("Thickness of the hollow shell.")
+ self.shellThicknessMMSpinBox.quantity = "length"
+ self.shellThicknessMMSpinBox.minimum = 0.0
+ self.shellThicknessMMSpinBox.value = 3.0
+ self.shellThicknessMMSpinBox.singleStep = 1.0
+
+ self.shellThicknessLabel = qt.QLabel()
+ self.shellThicknessLabel.setToolTip("Closest achievable thickness. Constrained by the segmentation's binary labelmap representation spacing.")
+
+ shellThicknessFrame = qt.QHBoxLayout()
+ shellThicknessFrame.addWidget(self.shellThicknessMMSpinBox)
+ self.shellThicknessMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Shell thickness:", shellThicknessFrame)
+ self.scriptedEffect.addLabeledOptionsWidget("", self.shellThicknessLabel)
+
+ self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
+ self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply hollow effect to all visible segments in this segmentation node. \
This operation may take a while.")
- self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
- self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
-
- self.applyButton.connect('clicked()', self.onApply)
- self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled)
- self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled)
- self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled)
- self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
- self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE)
- self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0)
-
- def getShellThicknessPixel(self):
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
-
- shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
- shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)]
- return shellThicknessPixel
-
- def updateGUIFromMRML(self):
- shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm")
- wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True)
- self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
- self.shellThicknessMMSpinBox.value = abs(shellThicknessMM)
- self.shellThicknessMMSpinBox.blockSignals(wasBlocked)
-
- wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True)
- self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE)
- self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked)
-
- wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True)
- self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE)
- self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked)
-
- wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True)
- self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE)
- self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked)
-
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
- shellThicknessPixel = self.getShellThicknessPixel()
- if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1:
- self.shellThicknessLabel.text = "Not feasible at current resolution."
- self.applyButton.setEnabled(False)
- else:
- thicknessMM = self.getShellThicknessMM()
- self.shellThicknessLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*thicknessMM, *shellThicknessPixel)
- self.applyButton.setEnabled(True)
- else:
- self.shellThicknessLabel.text = "Empty segment"
-
- self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
-
- applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
- wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
- self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
- self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- # Operation is managed separately
- self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value)
- applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
- self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
-
- def insideSurfaceModeToggled(self, toggled):
- if toggled:
- self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE)
-
- def medialSurfaceModeToggled(self, toggled):
- if toggled:
- self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE)
-
- def outsideSurfaceModeToggled(self, toggled):
- if toggled:
- self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE)
-
- def getShellThicknessMM(self):
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
-
- shellThicknessPixel = self.getShellThicknessPixel()
- shellThicknessMM = [abs((shellThicknessPixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
- for i in range(3):
- if shellThicknessMM[i] > 0:
- shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))), 1))
- return shellThicknessMM
-
- def showStatusMessage(self, msg, timeoutMsec=500):
+ self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
+ self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Makes the segment hollow by replacing it with a thick shell at the segment boundary.")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+
+ self.applyButton.connect('clicked()', self.onApply)
+ self.shellThicknessMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.insideSurfaceOptionRadioButton.connect("toggled(bool)", self.insideSurfaceModeToggled)
+ self.medialSurfaceOptionRadioButton.connect("toggled(bool)", self.medialSurfaceModeToggled)
+ self.outsideSurfaceOptionRadioButton.connect("toggled(bool)", self.outsideSurfaceModeToggled)
+ self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
+ self.scriptedEffect.setParameterDefault("ShellMode", INSIDE_SURFACE)
+ self.scriptedEffect.setParameterDefault("ShellThicknessMm", 3.0)
+
+ def getShellThicknessPixel(self):
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+
+ shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
+ shellThicknessPixel = [int(math.floor(shellThicknessMM / selectedSegmentLabelmapSpacing[componentIndex])) for componentIndex in range(3)]
+ return shellThicknessPixel
+
+ def updateGUIFromMRML(self):
+ shellThicknessMM = self.scriptedEffect.doubleParameter("ShellThicknessMm")
+ wasBlocked = self.shellThicknessMMSpinBox.blockSignals(True)
+ self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
+ self.shellThicknessMMSpinBox.value = abs(shellThicknessMM)
+ self.shellThicknessMMSpinBox.blockSignals(wasBlocked)
+
+ wasBlocked = self.insideSurfaceOptionRadioButton.blockSignals(True)
+ self.insideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == INSIDE_SURFACE)
+ self.insideSurfaceOptionRadioButton.blockSignals(wasBlocked)
+
+ wasBlocked = self.medialSurfaceOptionRadioButton.blockSignals(True)
+ self.medialSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == MEDIAL_SURFACE)
+ self.medialSurfaceOptionRadioButton.blockSignals(wasBlocked)
+
+ wasBlocked = self.outsideSurfaceOptionRadioButton.blockSignals(True)
+ self.outsideSurfaceOptionRadioButton.setChecked(self.scriptedEffect.parameter("ShellMode") == OUTSIDE_SURFACE)
+ self.outsideSurfaceOptionRadioButton.blockSignals(wasBlocked)
+
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+ shellThicknessPixel = self.getShellThicknessPixel()
+ if shellThicknessPixel[0] < 1 or shellThicknessPixel[1] < 1 or shellThicknessPixel[2] < 1:
+ self.shellThicknessLabel.text = "Not feasible at current resolution."
+ self.applyButton.setEnabled(False)
+ else:
+ thicknessMM = self.getShellThicknessMM()
+ self.shellThicknessLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*thicknessMM, *shellThicknessPixel)
+ self.applyButton.setEnabled(True)
+ else:
+ self.shellThicknessLabel.text = "Empty segment"
+
+ self.setWidgetMinMaxStepFromImageSpacing(self.shellThicknessMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
+
+ applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
+ wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
+ self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
+ self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ # Operation is managed separately
+ self.scriptedEffect.setParameter("ShellThicknessMm", self.shellThicknessMMSpinBox.value)
+ applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
+ self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
+
+ def insideSurfaceModeToggled(self, toggled):
+ if toggled:
+ self.scriptedEffect.setParameter("ShellMode", INSIDE_SURFACE)
+
+ def medialSurfaceModeToggled(self, toggled):
+ if toggled:
+ self.scriptedEffect.setParameter("ShellMode", MEDIAL_SURFACE)
+
+ def outsideSurfaceModeToggled(self, toggled):
+ if toggled:
+ self.scriptedEffect.setParameter("ShellMode", OUTSIDE_SURFACE)
+
+ def getShellThicknessMM(self):
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+
+ shellThicknessPixel = self.getShellThicknessPixel()
+ shellThicknessMM = [abs((shellThicknessPixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
+ for i in range(3):
+ if shellThicknessMM[i] > 0:
+ shellThicknessMM[i] = round(shellThicknessMM[i], max(int(-math.floor(math.log10(shellThicknessMM[i]))), 1))
+ return shellThicknessMM
+
+ def showStatusMessage(self, msg, timeoutMsec=500):
slicer.util.showStatusMessage(msg, timeoutMsec)
slicer.app.processEvents()
- def processHollowing(self):
- # Get modifier labelmap and parameters
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(selectedSegmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
-
- shellMode = self.scriptedEffect.parameter("ShellMode")
- shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
- import vtkITK
- margin = vtkITK.vtkITKImageMargin()
- margin.SetInputConnection(thresh.GetOutputPort())
- margin.CalculateMarginInMMOn()
-
- spacing = selectedSegmentLabelmap.GetSpacing()
- voxelDiameter = min(selectedSegmentLabelmap.GetSpacing())
- if shellMode == MEDIAL_SURFACE:
- margin.SetOuterMarginMM(0.5 * shellThicknessMM)
- margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5 * voxelDiameter)
- elif shellMode == INSIDE_SURFACE:
- margin.SetOuterMarginMM(shellThicknessMM + 0.1 * voxelDiameter)
- margin.SetInnerMarginMM(0.0 + 0.1 * voxelDiameter) # Don't include the original border (0.0)
- elif shellMode == OUTSIDE_SURFACE:
- margin.SetOuterMarginMM(0.0)
- margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter)
-
- modifierLabelmap.DeepCopy(margin.GetOutput())
-
- margin.Update()
- modifierLabelmap.ShallowCopy(margin.GetOutput())
-
- # Apply changes
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
-
- def onApply(self):
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
-
- try:
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- self.scriptedEffect.saveStateForUndo()
-
- applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
- if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
-
- if applyToAllVisibleSegments:
- # Process all visible segments
- inputSegmentIDs = vtk.vtkStringArray()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
- segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
- segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
- # store which segment was selected before operation
- selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
- if inputSegmentIDs.GetNumberOfValues() == 0:
- logging.info("Hollow operation skipped: there are no visible segments.")
- return
- # select input segments one by one, process
- for index in range(inputSegmentIDs.GetNumberOfValues()):
- segmentID = inputSegmentIDs.GetValue(index)
- self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
- segmentEditorNode.SetSelectedSegmentID(segmentID)
- self.processHollowing()
- # restore segment selection
- segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
- else:
- self.processHollowing()
-
- finally:
- qt.QApplication.restoreOverrideCursor()
+ def processHollowing(self):
+ # Get modifier labelmap and parameters
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(selectedSegmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
+
+ shellMode = self.scriptedEffect.parameter("ShellMode")
+ shellThicknessMM = abs(self.scriptedEffect.doubleParameter("ShellThicknessMm"))
+ import vtkITK
+ margin = vtkITK.vtkITKImageMargin()
+ margin.SetInputConnection(thresh.GetOutputPort())
+ margin.CalculateMarginInMMOn()
+
+ spacing = selectedSegmentLabelmap.GetSpacing()
+ voxelDiameter = min(selectedSegmentLabelmap.GetSpacing())
+ if shellMode == MEDIAL_SURFACE:
+ margin.SetOuterMarginMM(0.5 * shellThicknessMM)
+ margin.SetInnerMarginMM(-0.5 * shellThicknessMM + 0.5 * voxelDiameter)
+ elif shellMode == INSIDE_SURFACE:
+ margin.SetOuterMarginMM(shellThicknessMM + 0.1 * voxelDiameter)
+ margin.SetInnerMarginMM(0.0 + 0.1 * voxelDiameter) # Don't include the original border (0.0)
+ elif shellMode == OUTSIDE_SURFACE:
+ margin.SetOuterMarginMM(0.0)
+ margin.SetInnerMarginMM(-shellThicknessMM + voxelDiameter)
+
+ modifierLabelmap.DeepCopy(margin.GetOutput())
+
+ margin.Update()
+ modifierLabelmap.ShallowCopy(margin.GetOutput())
+
+ # Apply changes
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+
+ def onApply(self):
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+
+ try:
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ self.scriptedEffect.saveStateForUndo()
+
+ applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
+ if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
+
+ if applyToAllVisibleSegments:
+ # Process all visible segments
+ inputSegmentIDs = vtk.vtkStringArray()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
+ segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
+ segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
+ # store which segment was selected before operation
+ selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
+ if inputSegmentIDs.GetNumberOfValues() == 0:
+ logging.info("Hollow operation skipped: there are no visible segments.")
+ return
+ # select input segments one by one, process
+ for index in range(inputSegmentIDs.GetNumberOfValues()):
+ segmentID = inputSegmentIDs.GetValue(index)
+ self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
+ segmentEditorNode.SetSelectedSegmentID(segmentID)
+ self.processHollowing()
+ # restore segment selection
+ segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
+ else:
+ self.processHollowing()
+
+ finally:
+ qt.QApplication.restoreOverrideCursor()
INSIDE_SURFACE = 'INSIDE_SURFACE'
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py
index 10da0ec69b5..1bb3f0884ec 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorIslandsEffect.py
@@ -11,390 +11,390 @@
class SegmentEditorIslandsEffect(AbstractScriptedSegmentEditorEffect):
- """ Operate on connected components (islands) within a segment
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Islands'
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
- self.widgetToOperationNameMap = {}
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Islands.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Edit islands (connected components) in a segment
. To get more information
+ """ Operate on connected components (islands) within a segment
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Islands'
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ self.widgetToOperationNameMap = {}
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Islands.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Edit islands (connected components) in a segment
. To get more information
about each operation, hover the mouse over the option and wait for the tooltip to appear."""
- def setupOptionsFrame(self):
- self.operationRadioButtons = []
-
- self.keepLargestOptionRadioButton = qt.QRadioButton("Keep largest island")
- self.keepLargestOptionRadioButton.setToolTip(
- "Keep only the largest island in selected segment, remove all other islands in the segment.")
- self.operationRadioButtons.append(self.keepLargestOptionRadioButton)
- self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND
-
- self.keepSelectedOptionRadioButton = qt.QRadioButton("Keep selected island")
- self.keepSelectedOptionRadioButton.setToolTip(
- "Click on an island in a slice view to keep that island and remove all other islands in selected segment.")
- self.operationRadioButtons.append(self.keepSelectedOptionRadioButton)
- self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND
-
- self.removeSmallOptionRadioButton = qt.QRadioButton("Remove small islands")
- self.removeSmallOptionRadioButton.setToolTip(
- "Remove all islands from the selected segment that are smaller than the specified minimum size.")
- self.operationRadioButtons.append(self.removeSmallOptionRadioButton)
- self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS
-
- self.removeSelectedOptionRadioButton = qt.QRadioButton("Remove selected island")
- self.removeSelectedOptionRadioButton.setToolTip(
- "Click on an island in a slice view to remove it from selected segment.")
- self.operationRadioButtons.append(self.removeSelectedOptionRadioButton)
- self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND
-
- self.addSelectedOptionRadioButton = qt.QRadioButton("Add selected island")
- self.addSelectedOptionRadioButton.setToolTip(
- "Click on a region in a slice view to add it to selected segment.")
- self.operationRadioButtons.append(self.addSelectedOptionRadioButton)
- self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND
-
- self.splitAllOptionRadioButton = qt.QRadioButton("Split islands to segments")
- self.splitAllOptionRadioButton.setToolTip(
- "Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. " +
- "Segments will be ordered by island size.")
- self.operationRadioButtons.append(self.splitAllOptionRadioButton)
- self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS
-
- operationLayout = qt.QGridLayout()
- operationLayout.addWidget(self.keepLargestOptionRadioButton, 0, 0)
- operationLayout.addWidget(self.removeSmallOptionRadioButton, 1, 0)
- operationLayout.addWidget(self.splitAllOptionRadioButton, 2, 0)
- operationLayout.addWidget(self.keepSelectedOptionRadioButton, 0, 1)
- operationLayout.addWidget(self.removeSelectedOptionRadioButton, 1, 1)
- operationLayout.addWidget(self.addSelectedOptionRadioButton, 2, 1)
-
- self.operationRadioButtons[0].setChecked(True)
- self.scriptedEffect.addOptionsWidget(operationLayout)
-
- self.minimumSizeSpinBox = qt.QSpinBox()
- self.minimumSizeSpinBox.setToolTip("Islands consisting of less voxels than this minimum size, will be deleted.")
- self.minimumSizeSpinBox.setMinimum(0)
- self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX)
- self.minimumSizeSpinBox.setValue(1000)
- self.minimumSizeSpinBox.suffix = " voxels"
- self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget("Minimum size:", self.minimumSizeSpinBox)
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.scriptedEffect.addOptionsWidget(self.applyButton)
-
- for operationRadioButton in self.operationRadioButtons:
- operationRadioButton.connect('toggled(bool)',
- lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle))
-
- self.minimumSizeSpinBox.connect('valueChanged(int)', self.updateMRMLFromGUI)
-
- self.applyButton.connect('clicked()', self.onApply)
-
- def onOperationSelectionChanged(self, operationName, toggle):
- if not toggle:
- return
- self.scriptedEffect.setParameter("Operation", operationName)
-
- def currentOperationRequiresSegmentSelection(self):
- operationName = self.scriptedEffect.parameter("Operation")
- return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND]
-
- def onApply(self):
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
- operationName = self.scriptedEffect.parameter("Operation")
- minimumSize = self.scriptedEffect.integerParameter("MinimumSize")
- if operationName == KEEP_LARGEST_ISLAND:
- self.splitSegments(minimumSize=minimumSize, maxNumberOfSegments=1)
- elif operationName == REMOVE_SMALL_ISLANDS:
- self.splitSegments(minimumSize=minimumSize, split=False)
- elif operationName == SPLIT_ISLANDS_TO_SEGMENTS:
- self.splitSegments(minimumSize=minimumSize)
-
- def splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True):
- """
- minimumSize: if 0 then it means that all islands are kept, regardless of size
- maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many
- """
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- self.scriptedEffect.saveStateForUndo()
-
- # Get modifier labelmap
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
-
- castIn = vtk.vtkImageCast()
- castIn.SetInputData(selectedSegmentLabelmap)
- castIn.SetOutputScalarTypeToUnsignedInt()
-
- # Identify the islands in the inverted volume and
- # find the pixel that corresponds to the background
- islandMath = vtkITK.vtkITKIslandMath()
- islandMath.SetInputConnection(castIn.GetOutputPort())
- islandMath.SetFullyConnected(False)
- islandMath.SetMinimumSize(minimumSize)
- islandMath.Update()
-
- islandImage = slicer.vtkOrientedImageData()
- islandImage.ShallowCopy(islandMath.GetOutput())
- selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
- selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
- islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
-
- islandCount = islandMath.GetNumberOfIslands()
- islandOrigCount = islandMath.GetOriginalNumberOfIslands()
- ignoredIslands = islandOrigCount - islandCount
- logging.info("%d islands created (%d ignored)" % (islandCount, ignoredIslands))
-
- baseSegmentName = "Label"
- selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- with slicer.util.NodeModify(segmentationNode):
- segmentation = segmentationNode.GetSegmentation()
- selectedSegment = segmentation.GetSegment(selectedSegmentID)
- selectedSegmentName = selectedSegment.GetName()
- if selectedSegmentName is not None and selectedSegmentName != "":
- baseSegmentName = selectedSegmentName
-
- labelValues = vtk.vtkIntArray()
- slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage)
-
- # Erase segment from in original labelmap.
- # Individuall islands will be added back later.
- threshold = vtk.vtkImageThreshold()
- threshold.SetInputData(selectedSegmentLabelmap)
- threshold.ThresholdBetween(0, 0)
- threshold.SetInValue(0)
- threshold.SetOutValue(0)
- threshold.Update()
- emptyLabelmap = slicer.vtkOrientedImageData()
- emptyLabelmap.ShallowCopy(threshold.GetOutput())
- emptyLabelmap.CopyDirections(selectedSegmentLabelmap)
- self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap,
- slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
-
- for i in range(labelValues.GetNumberOfTuples()):
- if (maxNumberOfSegments > 0 and i >= maxNumberOfSegments):
- # We only care about the segments up to maxNumberOfSegments.
- # If we do not want to split segments, we only care about the first.
- break
-
- labelValue = int(labelValues.GetTuple1(i))
- segment = selectedSegment
- segmentID = selectedSegmentID
- if i != 0 and split:
- segment = slicer.vtkSegment()
- name = baseSegmentName + "_" + str(i + 1)
- segment.SetName(name)
- segment.AddRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(),
- selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()))
- segmentation.AddSegment(segment)
- segmentID = segmentation.GetSegmentIdBySegment(segment)
- segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID))
-
- threshold = vtk.vtkImageThreshold()
- threshold.SetInputData(islandMath.GetOutput())
- if not split and maxNumberOfSegments <= 0:
- # no need to split segments and no limit on number of segments, so we can lump all islands into one segment
- threshold.ThresholdByLower(0)
- threshold.SetInValue(0)
- threshold.SetOutValue(1)
- else:
- # copy only selected islands; or copy islands into different segments
- threshold.ThresholdBetween(labelValue, labelValue)
- threshold.SetInValue(1)
- threshold.SetOutValue(0)
- threshold.Update()
-
- modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd
- if i == 0:
- modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet
-
- # Create oriented image data from output
- modifierImage = slicer.vtkOrientedImageData()
- modifierImage.DeepCopy(threshold.GetOutput())
+ def setupOptionsFrame(self):
+ self.operationRadioButtons = []
+
+ self.keepLargestOptionRadioButton = qt.QRadioButton("Keep largest island")
+ self.keepLargestOptionRadioButton.setToolTip(
+ "Keep only the largest island in selected segment, remove all other islands in the segment.")
+ self.operationRadioButtons.append(self.keepLargestOptionRadioButton)
+ self.widgetToOperationNameMap[self.keepLargestOptionRadioButton] = KEEP_LARGEST_ISLAND
+
+ self.keepSelectedOptionRadioButton = qt.QRadioButton("Keep selected island")
+ self.keepSelectedOptionRadioButton.setToolTip(
+ "Click on an island in a slice view to keep that island and remove all other islands in selected segment.")
+ self.operationRadioButtons.append(self.keepSelectedOptionRadioButton)
+ self.widgetToOperationNameMap[self.keepSelectedOptionRadioButton] = KEEP_SELECTED_ISLAND
+
+ self.removeSmallOptionRadioButton = qt.QRadioButton("Remove small islands")
+ self.removeSmallOptionRadioButton.setToolTip(
+ "Remove all islands from the selected segment that are smaller than the specified minimum size.")
+ self.operationRadioButtons.append(self.removeSmallOptionRadioButton)
+ self.widgetToOperationNameMap[self.removeSmallOptionRadioButton] = REMOVE_SMALL_ISLANDS
+
+ self.removeSelectedOptionRadioButton = qt.QRadioButton("Remove selected island")
+ self.removeSelectedOptionRadioButton.setToolTip(
+ "Click on an island in a slice view to remove it from selected segment.")
+ self.operationRadioButtons.append(self.removeSelectedOptionRadioButton)
+ self.widgetToOperationNameMap[self.removeSelectedOptionRadioButton] = REMOVE_SELECTED_ISLAND
+
+ self.addSelectedOptionRadioButton = qt.QRadioButton("Add selected island")
+ self.addSelectedOptionRadioButton.setToolTip(
+ "Click on a region in a slice view to add it to selected segment.")
+ self.operationRadioButtons.append(self.addSelectedOptionRadioButton)
+ self.widgetToOperationNameMap[self.addSelectedOptionRadioButton] = ADD_SELECTED_ISLAND
+
+ self.splitAllOptionRadioButton = qt.QRadioButton("Split islands to segments")
+ self.splitAllOptionRadioButton.setToolTip(
+ "Create a new segment for each island of selected segment. Islands smaller than minimum size will be removed. " +
+ "Segments will be ordered by island size.")
+ self.operationRadioButtons.append(self.splitAllOptionRadioButton)
+ self.widgetToOperationNameMap[self.splitAllOptionRadioButton] = SPLIT_ISLANDS_TO_SEGMENTS
+
+ operationLayout = qt.QGridLayout()
+ operationLayout.addWidget(self.keepLargestOptionRadioButton, 0, 0)
+ operationLayout.addWidget(self.removeSmallOptionRadioButton, 1, 0)
+ operationLayout.addWidget(self.splitAllOptionRadioButton, 2, 0)
+ operationLayout.addWidget(self.keepSelectedOptionRadioButton, 0, 1)
+ operationLayout.addWidget(self.removeSelectedOptionRadioButton, 1, 1)
+ operationLayout.addWidget(self.addSelectedOptionRadioButton, 2, 1)
+
+ self.operationRadioButtons[0].setChecked(True)
+ self.scriptedEffect.addOptionsWidget(operationLayout)
+
+ self.minimumSizeSpinBox = qt.QSpinBox()
+ self.minimumSizeSpinBox.setToolTip("Islands consisting of less voxels than this minimum size, will be deleted.")
+ self.minimumSizeSpinBox.setMinimum(0)
+ self.minimumSizeSpinBox.setMaximum(vtk.VTK_INT_MAX)
+ self.minimumSizeSpinBox.setValue(1000)
+ self.minimumSizeSpinBox.suffix = " voxels"
+ self.minimumSizeLabel = self.scriptedEffect.addLabeledOptionsWidget("Minimum size:", self.minimumSizeSpinBox)
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+
+ for operationRadioButton in self.operationRadioButtons:
+ operationRadioButton.connect('toggled(bool)',
+ lambda toggle, widget=self.widgetToOperationNameMap[operationRadioButton]: self.onOperationSelectionChanged(widget, toggle))
+
+ self.minimumSizeSpinBox.connect('valueChanged(int)', self.updateMRMLFromGUI)
+
+ self.applyButton.connect('clicked()', self.onApply)
+
+ def onOperationSelectionChanged(self, operationName, toggle):
+ if not toggle:
+ return
+ self.scriptedEffect.setParameter("Operation", operationName)
+
+ def currentOperationRequiresSegmentSelection(self):
+ operationName = self.scriptedEffect.parameter("Operation")
+ return operationName in [KEEP_SELECTED_ISLAND, REMOVE_SELECTED_ISLAND, ADD_SELECTED_ISLAND]
+
+ def onApply(self):
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+ operationName = self.scriptedEffect.parameter("Operation")
+ minimumSize = self.scriptedEffect.integerParameter("MinimumSize")
+ if operationName == KEEP_LARGEST_ISLAND:
+ self.splitSegments(minimumSize=minimumSize, maxNumberOfSegments=1)
+ elif operationName == REMOVE_SMALL_ISLANDS:
+ self.splitSegments(minimumSize=minimumSize, split=False)
+ elif operationName == SPLIT_ISLANDS_TO_SEGMENTS:
+ self.splitSegments(minimumSize=minimumSize)
+
+ def splitSegments(self, minimumSize=0, maxNumberOfSegments=0, split=True):
+ """
+ minimumSize: if 0 then it means that all islands are kept, regardless of size
+ maxNumberOfSegments: if 0 then it means that all islands are kept, regardless of how many
+ """
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ self.scriptedEffect.saveStateForUndo()
+
+ # Get modifier labelmap
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+
+ castIn = vtk.vtkImageCast()
+ castIn.SetInputData(selectedSegmentLabelmap)
+ castIn.SetOutputScalarTypeToUnsignedInt()
+
+ # Identify the islands in the inverted volume and
+ # find the pixel that corresponds to the background
+ islandMath = vtkITK.vtkITKIslandMath()
+ islandMath.SetInputConnection(castIn.GetOutputPort())
+ islandMath.SetFullyConnected(False)
+ islandMath.SetMinimumSize(minimumSize)
+ islandMath.Update()
+
+ islandImage = slicer.vtkOrientedImageData()
+ islandImage.ShallowCopy(islandMath.GetOutput())
selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
- modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
- # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode
- # method call to import all the resulting segments at once but that would put all the imported segments
- # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase.
- self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode)
+ islandImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
+
+ islandCount = islandMath.GetNumberOfIslands()
+ islandOrigCount = islandMath.GetOriginalNumberOfIslands()
+ ignoredIslands = islandOrigCount - islandCount
+ logging.info("%d islands created (%d ignored)" % (islandCount, ignoredIslands))
+
+ baseSegmentName = "Label"
+ selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ with slicer.util.NodeModify(segmentationNode):
+ segmentation = segmentationNode.GetSegmentation()
+ selectedSegment = segmentation.GetSegment(selectedSegmentID)
+ selectedSegmentName = selectedSegment.GetName()
+ if selectedSegmentName is not None and selectedSegmentName != "":
+ baseSegmentName = selectedSegmentName
+
+ labelValues = vtk.vtkIntArray()
+ slicer.vtkSlicerSegmentationsModuleLogic.GetAllLabelValues(labelValues, islandImage)
+
+ # Erase segment from in original labelmap.
+ # Individuall islands will be added back later.
+ threshold = vtk.vtkImageThreshold()
+ threshold.SetInputData(selectedSegmentLabelmap)
+ threshold.ThresholdBetween(0, 0)
+ threshold.SetInValue(0)
+ threshold.SetOutValue(0)
+ threshold.Update()
+ emptyLabelmap = slicer.vtkOrientedImageData()
+ emptyLabelmap.ShallowCopy(threshold.GetOutput())
+ emptyLabelmap.CopyDirections(selectedSegmentLabelmap)
+ self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, selectedSegmentID, emptyLabelmap,
+ slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+
+ for i in range(labelValues.GetNumberOfTuples()):
+ if (maxNumberOfSegments > 0 and i >= maxNumberOfSegments):
+ # We only care about the segments up to maxNumberOfSegments.
+ # If we do not want to split segments, we only care about the first.
+ break
+
+ labelValue = int(labelValues.GetTuple1(i))
+ segment = selectedSegment
+ segmentID = selectedSegmentID
+ if i != 0 and split:
+ segment = slicer.vtkSegment()
+ name = baseSegmentName + "_" + str(i + 1)
+ segment.SetName(name)
+ segment.AddRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName(),
+ selectedSegment.GetRepresentation(slicer.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()))
+ segmentation.AddSegment(segment)
+ segmentID = segmentation.GetSegmentIdBySegment(segment)
+ segment.SetLabelValue(segmentation.GetUniqueLabelValueForSharedLabelmap(selectedSegmentID))
+
+ threshold = vtk.vtkImageThreshold()
+ threshold.SetInputData(islandMath.GetOutput())
+ if not split and maxNumberOfSegments <= 0:
+ # no need to split segments and no limit on number of segments, so we can lump all islands into one segment
+ threshold.ThresholdByLower(0)
+ threshold.SetInValue(0)
+ threshold.SetOutValue(1)
+ else:
+ # copy only selected islands; or copy islands into different segments
+ threshold.ThresholdBetween(labelValue, labelValue)
+ threshold.SetInValue(1)
+ threshold.SetOutValue(0)
+ threshold.Update()
+
+ modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd
+ if i == 0:
+ modificationMode = slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet
+
+ # Create oriented image data from output
+ modifierImage = slicer.vtkOrientedImageData()
+ modifierImage.DeepCopy(threshold.GetOutput())
+ selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
+ selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
+ modifierImage.SetGeometryFromImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
+ # We could use a single slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode
+ # method call to import all the resulting segments at once but that would put all the imported segments
+ # in a new layer. By using modifySegmentByLabelmap, the number of layers will not increase.
+ self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentID, modifierImage, modificationMode)
+
+ if not split and maxNumberOfSegments <= 0:
+ # all islands lumped into one segment, so we are done
+ break
- if not split and maxNumberOfSegments <= 0:
- # all islands lumped into one segment, so we are done
- break
+ qt.QApplication.restoreOverrideCursor()
- qt.QApplication.restoreOverrideCursor()
+ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
+ import vtkSegmentationCorePython as vtkSegmentationCore
- def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
- import vtkSegmentationCorePython as vtkSegmentationCore
+ abortEvent = False
- abortEvent = False
+ # Only allow in modes where segment selection is needed
+ if not self.currentOperationRequiresSegmentSelection():
+ return False
- # Only allow in modes where segment selection is needed
- if not self.currentOperationRequiresSegmentSelection():
- return False
+ # Only allow for slice views
+ if viewWidget.className() != "qMRMLSliceWidget":
+ return abortEvent
- # Only allow for slice views
- if viewWidget.className() != "qMRMLSliceWidget":
- return abortEvent
+ if eventId != vtk.vtkCommand.LeftButtonPressEvent or callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey():
+ return abortEvent
- if eventId != vtk.vtkCommand.LeftButtonPressEvent or callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey():
- return abortEvent
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return abortEvent
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return abortEvent
+ abortEvent = True
- abortEvent = True
+ # Generate merged labelmap of all visible segments
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ visibleSegmentIds = vtk.vtkStringArray()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
+ if visibleSegmentIds.GetNumberOfValues() == 0:
+ logging.info("Island operation skipped: there are no visible segments")
+ return abortEvent
- # Generate merged labelmap of all visible segments
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- visibleSegmentIds = vtk.vtkStringArray()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
- if visibleSegmentIds.GetNumberOfValues() == 0:
- logging.info("Island operation skipped: there are no visible segments")
- return abortEvent
+ self.scriptedEffect.saveStateForUndo()
- self.scriptedEffect.saveStateForUndo()
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ operationName = self.scriptedEffect.parameter("Operation")
- operationName = self.scriptedEffect.parameter("Operation")
+ if operationName == ADD_SELECTED_ISLAND:
+ inputLabelImage = slicer.vtkOrientedImageData()
+ if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
+ None, visibleSegmentIds):
+ logging.error('Failed to apply island operation: cannot get list of visible segments')
+ qt.QApplication.restoreOverrideCursor()
+ return abortEvent
+ else:
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(selectedSegmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
+ thresh.Update()
+ # Create oriented image data from output
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ inputLabelImage = slicer.vtkOrientedImageData()
+ inputLabelImage.ShallowCopy(thresh.GetOutput())
+ selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
+ selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
+ inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
+
+ xy = callerInteractor.GetEventPosition()
+ ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
+ pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
+
+ try:
+ floodFillingFilter = vtk.vtkImageThresholdConnectivity()
+ floodFillingFilter.SetInputData(inputLabelImage)
+ seedPoints = vtk.vtkPoints()
+ origin = inputLabelImage.GetOrigin()
+ spacing = inputLabelImage.GetSpacing()
+ seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2])
+ floodFillingFilter.SetSeedPoints(seedPoints)
+ floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
+
+ if operationName == ADD_SELECTED_ISLAND:
+ floodFillingFilter.SetInValue(1)
+ floodFillingFilter.SetOutValue(0)
+ floodFillingFilter.Update()
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
+
+ elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
+
+ if operationName == KEEP_SELECTED_ISLAND:
+ floodFillingFilter.SetInValue(1)
+ floodFillingFilter.SetOutValue(0)
+ else: # operationName == REMOVE_SELECTED_ISLAND:
+ floodFillingFilter.SetInValue(1)
+ floodFillingFilter.SetOutValue(0)
+
+ floodFillingFilter.Update()
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
+
+ if operationName == KEEP_SELECTED_ISLAND:
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+ else: # operationName == REMOVE_SELECTED_ISLAND:
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
+
+ except IndexError:
+ logging.error('apply: Failed to threshold master volume!')
+ finally:
+ qt.QApplication.restoreOverrideCursor()
- if operationName == ADD_SELECTED_ISLAND:
- inputLabelImage = slicer.vtkOrientedImageData()
- if not segmentationNode.GenerateMergedLabelmapForAllSegments(inputLabelImage,
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
- None, visibleSegmentIds):
- logging.error('Failed to apply island operation: cannot get list of visible segments')
- qt.QApplication.restoreOverrideCursor()
return abortEvent
- else:
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(selectedSegmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
- thresh.Update()
- # Create oriented image data from output
- import vtkSegmentationCorePython as vtkSegmentationCore
- inputLabelImage = slicer.vtkOrientedImageData()
- inputLabelImage.ShallowCopy(thresh.GetOutput())
- selectedSegmentLabelmapImageToWorldMatrix = vtk.vtkMatrix4x4()
- selectedSegmentLabelmap.GetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
- inputLabelImage.SetImageToWorldMatrix(selectedSegmentLabelmapImageToWorldMatrix)
-
- xy = callerInteractor.GetEventPosition()
- ijk = self.xyToIjk(xy, viewWidget, inputLabelImage, segmentationNode.GetParentTransformNode())
- pixelValue = inputLabelImage.GetScalarComponentAsFloat(ijk[0], ijk[1], ijk[2], 0)
-
- try:
- floodFillingFilter = vtk.vtkImageThresholdConnectivity()
- floodFillingFilter.SetInputData(inputLabelImage)
- seedPoints = vtk.vtkPoints()
- origin = inputLabelImage.GetOrigin()
- spacing = inputLabelImage.GetSpacing()
- seedPoints.InsertNextPoint(origin[0] + ijk[0] * spacing[0], origin[1] + ijk[1] * spacing[1], origin[2] + ijk[2] * spacing[2])
- floodFillingFilter.SetSeedPoints(seedPoints)
- floodFillingFilter.ThresholdBetween(pixelValue, pixelValue)
-
- if operationName == ADD_SELECTED_ISLAND:
- floodFillingFilter.SetInValue(1)
- floodFillingFilter.SetOutValue(0)
- floodFillingFilter.Update()
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
-
- elif pixelValue != 0: # if clicked on empty part then there is nothing to remove or keep
-
- if operationName == KEEP_SELECTED_ISLAND:
- floodFillingFilter.SetInValue(1)
- floodFillingFilter.SetOutValue(0)
- else: # operationName == REMOVE_SELECTED_ISLAND:
- floodFillingFilter.SetInValue(1)
- floodFillingFilter.SetOutValue(0)
-
- floodFillingFilter.Update()
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- modifierLabelmap.DeepCopy(floodFillingFilter.GetOutput())
-
- if operationName == KEEP_SELECTED_ISLAND:
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
- else: # operationName == REMOVE_SELECTED_ISLAND:
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove)
-
- except IndexError:
- logging.error('apply: Failed to threshold master volume!')
- finally:
- qt.QApplication.restoreOverrideCursor()
-
- return abortEvent
-
- def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
- pass # For the sake of example
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
- self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
-
- def updateGUIFromMRML(self):
- for operationRadioButton in self.operationRadioButtons:
- operationRadioButton.blockSignals(True)
- operationName = self.scriptedEffect.parameter("Operation")
- currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
- currentOperationRadioButton.setChecked(True)
- for operationRadioButton in self.operationRadioButtons:
- operationRadioButton.blockSignals(False)
-
- segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
- self.applyButton.setEnabled(not segmentSelectionRequired)
- if segmentSelectionRequired:
- self.applyButton.setToolTip("Click in a slice view to select an island.")
- else:
- self.applyButton.setToolTip("")
-
- # TODO: this call has no effect now
- # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
- # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
-
- showMinimumSizeOption = (operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS])
- self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
- self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
-
- self.minimumSizeSpinBox.blockSignals(True)
- self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
- self.minimumSizeSpinBox.blockSignals(False)
-
- def updateMRMLFromGUI(self):
- # Operation is managed separately
- self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
+
+ def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
+ pass # For the sake of example
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("Operation", KEEP_LARGEST_ISLAND)
+ self.scriptedEffect.setParameterDefault("MinimumSize", 1000)
+
+ def updateGUIFromMRML(self):
+ for operationRadioButton in self.operationRadioButtons:
+ operationRadioButton.blockSignals(True)
+ operationName = self.scriptedEffect.parameter("Operation")
+ currentOperationRadioButton = list(self.widgetToOperationNameMap.keys())[list(self.widgetToOperationNameMap.values()).index(operationName)]
+ currentOperationRadioButton.setChecked(True)
+ for operationRadioButton in self.operationRadioButtons:
+ operationRadioButton.blockSignals(False)
+
+ segmentSelectionRequired = self.currentOperationRequiresSegmentSelection()
+ self.applyButton.setEnabled(not segmentSelectionRequired)
+ if segmentSelectionRequired:
+ self.applyButton.setToolTip("Click in a slice view to select an island.")
+ else:
+ self.applyButton.setToolTip("")
+
+ # TODO: this call has no effect now
+ # qSlicerSegmentEditorAbstractEffect should be improved so that it triggers a cursor update
+ # self.scriptedEffect.showEffectCursorInSliceView = segmentSelectionRequired
+
+ showMinimumSizeOption = (operationName in [KEEP_LARGEST_ISLAND, REMOVE_SMALL_ISLANDS, SPLIT_ISLANDS_TO_SEGMENTS])
+ self.minimumSizeSpinBox.setEnabled(showMinimumSizeOption)
+ self.minimumSizeLabel.setEnabled(showMinimumSizeOption)
+
+ self.minimumSizeSpinBox.blockSignals(True)
+ self.minimumSizeSpinBox.value = self.scriptedEffect.integerParameter("MinimumSize")
+ self.minimumSizeSpinBox.blockSignals(False)
+
+ def updateMRMLFromGUI(self):
+ # Operation is managed separately
+ self.scriptedEffect.setParameter("MinimumSize", self.minimumSizeSpinBox.value)
KEEP_LARGEST_ISLAND = 'KEEP_LARGEST_ISLAND'
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py
index 3be183096e4..8d7ca9161cd 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLevelTracingEffect.py
@@ -11,232 +11,232 @@
class SegmentEditorLevelTracingEffect(AbstractScriptedSegmentEditorLabelEffect):
- """ LevelTracingEffect is a LabelEffect implementing level tracing fill
- using intensity-based isolines
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Level tracing'
- AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
-
- # Effect-specific members
- self.levelTracingPipelines = {}
- self.lastXY = None
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/LevelTracing.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Add uniform intensity region to selected segment
.
+ """ LevelTracingEffect is a LabelEffect implementing level tracing fill
+ using intensity-based isolines
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Level tracing'
+ AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)
+
+ # Effect-specific members
+ self.levelTracingPipelines = {}
+ self.lastXY = None
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedLabelEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/LevelTracing.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Add uniform intensity region to selected segment
.
- Mouse move: current background voxel is used to find a closed path that
follows the same intensity value back to the starting point within the current slice.
- Left-click: add the previewed region to the current segment.
"""
- def setupOptionsFrame(self):
- self.sliceRotatedErrorLabel = qt.QLabel()
- # This widget displays an error message if the slice view is not aligned
- # with the segmentation's axes.
- self.scriptedEffect.addOptionsWidget(self.sliceRotatedErrorLabel)
-
- def activate(self):
- self.sliceRotatedErrorLabel.text = ""
-
- def deactivate(self):
- # Clear draw pipelines
- for sliceWidget, pipeline in self.levelTracingPipelines.items():
- self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
- self.levelTracingPipelines = {}
- self.lastXY = None
-
- def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
- abortEvent = False
-
- # Only allow for slice views
- if viewWidget.className() != "qMRMLSliceWidget":
- return abortEvent
- # Get draw pipeline for current slice
- pipeline = self.pipelineForWidget(viewWidget)
- if pipeline is None:
- return abortEvent
-
- anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
-
- if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ def setupOptionsFrame(self):
+ self.sliceRotatedErrorLabel = qt.QLabel()
+ # This widget displays an error message if the slice view is not aligned
+ # with the segmentation's axes.
+ self.scriptedEffect.addOptionsWidget(self.sliceRotatedErrorLabel)
+
+ def activate(self):
+ self.sliceRotatedErrorLabel.text = ""
+
+ def deactivate(self):
+ # Clear draw pipelines
+ for sliceWidget, pipeline in self.levelTracingPipelines.items():
+ self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
+ self.levelTracingPipelines = {}
+ self.lastXY = None
+
+ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
+ abortEvent = False
+
+ # Only allow for slice views
+ if viewWidget.className() != "qMRMLSliceWidget":
+ return abortEvent
+ # Get draw pipeline for current slice
+ pipeline = self.pipelineForWidget(viewWidget)
+ if pipeline is None:
+ return abortEvent
+
+ anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
+
+ if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return abortEvent
+
+ self.scriptedEffect.saveStateForUndo()
+
+ # Get modifier labelmap
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+
+ # Apply poly data on modifier labelmap
+ pipeline.appendPolyMask(modifierLabelmap)
+ # TODO: it would be nice to reduce extent
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.MouseMoveEvent:
+ if pipeline.actionState == '':
+ xy = callerInteractor.GetEventPosition()
+ if pipeline.preview(xy):
+ self.sliceRotatedErrorLabel.text = ""
+ else:
+ self.sliceRotatedErrorLabel.text = (""
+ + "Slice view is not aligned with segmentation axis.
To use this effect, click the 'Slice views orientation' warning button."
+ + "")
+ abortEvent = True
+ self.lastXY = xy
+ elif eventId == vtk.vtkCommand.EnterEvent:
+ self.sliceRotatedErrorLabel.text = ""
+ pipeline.actor.VisibilityOn()
+ elif eventId == vtk.vtkCommand.LeaveEvent:
+ self.sliceRotatedErrorLabel.text = ""
+ pipeline.actor.VisibilityOff()
+ self.lastXY = None
+
return abortEvent
- self.scriptedEffect.saveStateForUndo()
-
- # Get modifier labelmap
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
-
- # Apply poly data on modifier labelmap
- pipeline.appendPolyMask(modifierLabelmap)
- # TODO: it would be nice to reduce extent
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd)
- abortEvent = True
- elif eventId == vtk.vtkCommand.MouseMoveEvent:
- if pipeline.actionState == '':
- xy = callerInteractor.GetEventPosition()
- if pipeline.preview(xy):
- self.sliceRotatedErrorLabel.text = ""
- else:
- self.sliceRotatedErrorLabel.text = (""
- + "Slice view is not aligned with segmentation axis.
To use this effect, click the 'Slice views orientation' warning button."
- + "")
- abortEvent = True
- self.lastXY = xy
- elif eventId == vtk.vtkCommand.EnterEvent:
- self.sliceRotatedErrorLabel.text = ""
- pipeline.actor.VisibilityOn()
- elif eventId == vtk.vtkCommand.LeaveEvent:
- self.sliceRotatedErrorLabel.text = ""
- pipeline.actor.VisibilityOff()
- self.lastXY = None
-
- return abortEvent
-
- def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
- if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'):
- # Get draw pipeline for current slice
- pipeline = self.pipelineForWidget(viewWidget)
- if pipeline is None:
- logging.error('processViewNodeEvents: Invalid pipeline')
- return
-
- # Update the preview to the new slice
- if pipeline.actionState == '' and self.lastXY:
- pipeline.preview(self.lastXY)
-
- def pipelineForWidget(self, sliceWidget):
- if sliceWidget in self.levelTracingPipelines:
- return self.levelTracingPipelines[sliceWidget]
-
- # Create pipeline if does not yet exist
- pipeline = LevelTracingPipeline(self, sliceWidget)
-
- # Add actor
- renderer = self.scriptedEffect.renderer(sliceWidget)
- if renderer is None:
- logging.error("setupPreviewDisplay: Failed to get renderer!")
- return None
- self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
-
- self.levelTracingPipelines[sliceWidget] = pipeline
- return pipeline
+ def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
+ if callerViewNode and callerViewNode.IsA('vtkMRMLSliceNode'):
+ # Get draw pipeline for current slice
+ pipeline = self.pipelineForWidget(viewWidget)
+ if pipeline is None:
+ logging.error('processViewNodeEvents: Invalid pipeline')
+ return
+
+ # Update the preview to the new slice
+ if pipeline.actionState == '' and self.lastXY:
+ pipeline.preview(self.lastXY)
+
+ def pipelineForWidget(self, sliceWidget):
+ if sliceWidget in self.levelTracingPipelines:
+ return self.levelTracingPipelines[sliceWidget]
+
+ # Create pipeline if does not yet exist
+ pipeline = LevelTracingPipeline(self, sliceWidget)
+
+ # Add actor
+ renderer = self.scriptedEffect.renderer(sliceWidget)
+ if renderer is None:
+ logging.error("setupPreviewDisplay: Failed to get renderer!")
+ return None
+ self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
+
+ self.levelTracingPipelines[sliceWidget] = pipeline
+ return pipeline
#
# LevelTracingPipeline
#
class LevelTracingPipeline:
- """ Visualization objects and pipeline for each slice view for level tracing
- """
-
- def __init__(self, effect, sliceWidget):
- self.effect = effect
- self.sliceWidget = sliceWidget
- self.actionState = ''
-
- self.xyPoints = vtk.vtkPoints()
- self.rasPoints = vtk.vtkPoints()
- self.polyData = vtk.vtkPolyData()
-
- self.tracingFilter = vtkITK.vtkITKLevelTracingImageFilter()
- self.ijkToXY = vtk.vtkGeneralTransform()
-
- self.mapper = vtk.vtkPolyDataMapper2D()
- self.actor = vtk.vtkActor2D()
- actorProperty = self.actor.GetProperty()
- actorProperty.SetColor(107 / 255., 190 / 255., 99 / 255.)
- actorProperty.SetLineWidth(1)
- self.mapper.SetInputData(self.polyData)
- self.actor.SetMapper(self.mapper)
- actorProperty = self.actor.GetProperty()
- actorProperty.SetColor(1, 1, 0)
- actorProperty.SetLineWidth(1)
-
- def preview(self, xy):
- """Calculate the current level trace view if the mouse is inside the volume extent
- Returns False if slice views are rotated.
+ """ Visualization objects and pipeline for each slice view for level tracing
"""
- # Get master volume image data
- masterImageData = self.effect.scriptedEffect.masterVolumeImageData()
-
- segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
- parentTransformNode = None
- if segmentationNode:
- parentTransformNode = segmentationNode.GetParentTransformNode()
-
- self.xyPoints.Reset()
- ijk = self.effect.xyToIjk(xy, self.sliceWidget, masterImageData, parentTransformNode)
- dimensions = masterImageData.GetDimensions()
-
- self.tracingFilter.SetInputData(masterImageData)
- self.tracingFilter.SetSeed(ijk)
-
- # Select the plane corresponding to current slice orientation
- # for the input volume
- sliceNode = self.effect.scriptedEffect.viewNode(self.sliceWidget)
- offset = max(sliceNode.GetDimensions())
-
- i0, j0, k0 = self.effect.xyToIjk((0, 0), self.sliceWidget, masterImageData, parentTransformNode)
- i1, j1, k1 = self.effect.xyToIjk((offset, offset), self.sliceWidget, masterImageData, parentTransformNode)
- if i0 == i1:
- self.tracingFilter.SetPlaneToJK()
- elif j0 == j1:
- self.tracingFilter.SetPlaneToIK()
- elif k0 == k1:
- self.tracingFilter.SetPlaneToIJ()
- else:
- self.polyData.Reset()
- self.sliceWidget.sliceView().scheduleRender()
- return False
-
- self.tracingFilter.Update()
- polyData = self.tracingFilter.GetOutput()
-
- # Get master volume IJK to slice XY transform
- xyToRas = sliceNode.GetXYToRAS()
- rasToIjk = vtk.vtkMatrix4x4()
- masterImageData.GetImageToWorldMatrix(rasToIjk)
- rasToIjk.Invert()
- xyToIjk = vtk.vtkGeneralTransform()
- xyToIjk.PostMultiply()
- xyToIjk.Concatenate(xyToRas)
- if parentTransformNode:
- worldToSegmentation = vtk.vtkMatrix4x4()
- parentTransformNode.GetMatrixTransformFromWorld(worldToSegmentation)
- xyToIjk.Concatenate(worldToSegmentation)
- xyToIjk.Concatenate(rasToIjk)
- ijkToXy = xyToIjk.GetInverse()
- if polyData.GetPoints():
- ijkToXy.TransformPoints(polyData.GetPoints(), self.xyPoints)
- self.polyData.DeepCopy(polyData)
- self.polyData.GetPoints().DeepCopy(self.xyPoints)
- else:
- self.polyData.Reset()
- self.sliceWidget.sliceView().scheduleRender()
- return True
-
- def appendPolyMask(self, modifierLabelmap):
- lines = self.polyData.GetLines()
- if lines.GetNumberOfCells() == 0:
- return
-
- # Apply poly data on modifier labelmap
- segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
- self.effect.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)
+ def __init__(self, effect, sliceWidget):
+ self.effect = effect
+ self.sliceWidget = sliceWidget
+ self.actionState = ''
+
+ self.xyPoints = vtk.vtkPoints()
+ self.rasPoints = vtk.vtkPoints()
+ self.polyData = vtk.vtkPolyData()
+
+ self.tracingFilter = vtkITK.vtkITKLevelTracingImageFilter()
+ self.ijkToXY = vtk.vtkGeneralTransform()
+
+ self.mapper = vtk.vtkPolyDataMapper2D()
+ self.actor = vtk.vtkActor2D()
+ actorProperty = self.actor.GetProperty()
+ actorProperty.SetColor(107 / 255., 190 / 255., 99 / 255.)
+ actorProperty.SetLineWidth(1)
+ self.mapper.SetInputData(self.polyData)
+ self.actor.SetMapper(self.mapper)
+ actorProperty = self.actor.GetProperty()
+ actorProperty.SetColor(1, 1, 0)
+ actorProperty.SetLineWidth(1)
+
+ def preview(self, xy):
+ """Calculate the current level trace view if the mouse is inside the volume extent
+ Returns False if slice views are rotated.
+ """
+
+ # Get master volume image data
+ masterImageData = self.effect.scriptedEffect.masterVolumeImageData()
+
+ segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ parentTransformNode = None
+ if segmentationNode:
+ parentTransformNode = segmentationNode.GetParentTransformNode()
+
+ self.xyPoints.Reset()
+ ijk = self.effect.xyToIjk(xy, self.sliceWidget, masterImageData, parentTransformNode)
+ dimensions = masterImageData.GetDimensions()
+
+ self.tracingFilter.SetInputData(masterImageData)
+ self.tracingFilter.SetSeed(ijk)
+
+ # Select the plane corresponding to current slice orientation
+ # for the input volume
+ sliceNode = self.effect.scriptedEffect.viewNode(self.sliceWidget)
+ offset = max(sliceNode.GetDimensions())
+
+ i0, j0, k0 = self.effect.xyToIjk((0, 0), self.sliceWidget, masterImageData, parentTransformNode)
+ i1, j1, k1 = self.effect.xyToIjk((offset, offset), self.sliceWidget, masterImageData, parentTransformNode)
+ if i0 == i1:
+ self.tracingFilter.SetPlaneToJK()
+ elif j0 == j1:
+ self.tracingFilter.SetPlaneToIK()
+ elif k0 == k1:
+ self.tracingFilter.SetPlaneToIJ()
+ else:
+ self.polyData.Reset()
+ self.sliceWidget.sliceView().scheduleRender()
+ return False
+
+ self.tracingFilter.Update()
+ polyData = self.tracingFilter.GetOutput()
+
+ # Get master volume IJK to slice XY transform
+ xyToRas = sliceNode.GetXYToRAS()
+ rasToIjk = vtk.vtkMatrix4x4()
+ masterImageData.GetImageToWorldMatrix(rasToIjk)
+ rasToIjk.Invert()
+ xyToIjk = vtk.vtkGeneralTransform()
+ xyToIjk.PostMultiply()
+ xyToIjk.Concatenate(xyToRas)
+ if parentTransformNode:
+ worldToSegmentation = vtk.vtkMatrix4x4()
+ parentTransformNode.GetMatrixTransformFromWorld(worldToSegmentation)
+ xyToIjk.Concatenate(worldToSegmentation)
+ xyToIjk.Concatenate(rasToIjk)
+ ijkToXy = xyToIjk.GetInverse()
+ if polyData.GetPoints():
+ ijkToXy.TransformPoints(polyData.GetPoints(), self.xyPoints)
+ self.polyData.DeepCopy(polyData)
+ self.polyData.GetPoints().DeepCopy(self.xyPoints)
+ else:
+ self.polyData.Reset()
+ self.sliceWidget.sliceView().scheduleRender()
+ return True
+
+ def appendPolyMask(self, modifierLabelmap):
+ lines = self.polyData.GetLines()
+ if lines.GetNumberOfCells() == 0:
+ return
+
+ # Apply poly data on modifier labelmap
+ segmentationNode = self.effect.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ self.effect.scriptedEffect.appendPolyMask(modifierLabelmap, self.polyData, self.sliceWidget, segmentationNode)
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py
index a898fe27f12..b0901ddd74f 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorLogicalEffect.py
@@ -10,28 +10,28 @@
class SegmentEditorLogicalEffect(AbstractScriptedSegmentEditorEffect):
- """ LogicalEffect is an MorphologyEffect to erode a layer of pixels from a segment
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Logical operators'
- self.operationsRequireModifierSegment = [LOGICAL_COPY, LOGICAL_UNION, LOGICAL_SUBTRACT, LOGICAL_INTERSECT]
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Logical.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Apply logical operators or combine segments
. Available operations:
+ """ LogicalEffect is an MorphologyEffect to erode a layer of pixels from a segment
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Logical operators'
+ self.operationsRequireModifierSegment = [LOGICAL_COPY, LOGICAL_UNION, LOGICAL_SUBTRACT, LOGICAL_INTERSECT]
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Logical.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Apply logical operators or combine segments
. Available operations:
- Copy: replace the selected segment by the modifier segment.
- Add: add modifier segment to current segment.
@@ -44,231 +44,231 @@ def helpText(self):
Selected segment: segment selected in the segment list - above. Modifier segment: segment chosen in segment list in effect options - below.
"""
- def setupOptionsFrame(self):
-
- self.methodSelectorComboBox = qt.QComboBox()
- self.methodSelectorComboBox.addItem("Copy", LOGICAL_COPY)
- self.methodSelectorComboBox.addItem("Add", LOGICAL_UNION)
- self.methodSelectorComboBox.addItem("Subtract", LOGICAL_SUBTRACT)
- self.methodSelectorComboBox.addItem("Intersect", LOGICAL_INTERSECT)
- self.methodSelectorComboBox.addItem("Invert", LOGICAL_INVERT)
- self.methodSelectorComboBox.addItem("Clear", LOGICAL_CLEAR)
- self.methodSelectorComboBox.addItem("Fill", LOGICAL_FILL)
- self.methodSelectorComboBox.setToolTip('Click Show details link above for description of operations.')
-
- self.bypassMaskingCheckBox = qt.QCheckBox("Bypass masking")
- self.bypassMaskingCheckBox.setToolTip("Ignore all masking options and only modify the selected segment.")
- self.bypassMaskingCheckBox.objectName = self.__class__.__name__ + 'BypassMasking'
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
-
- operationFrame = qt.QHBoxLayout()
- operationFrame.addWidget(self.methodSelectorComboBox)
- operationFrame.addWidget(self.applyButton)
- operationFrame.addWidget(self.bypassMaskingCheckBox)
- self.marginSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationFrame)
-
- self.modifierSegmentSelectorLabel = qt.QLabel("Modifier segment:")
- self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelectorLabel)
-
- self.modifierSegmentSelector = slicer.qMRMLSegmentsTableView()
- self.modifierSegmentSelector.selectionMode = qt.QAbstractItemView.SingleSelection
- self.modifierSegmentSelector.headerVisible = False
- self.modifierSegmentSelector.visibilityColumnVisible = False
- self.modifierSegmentSelector.opacityColumnVisible = False
-
- self.modifierSegmentSelector.setMRMLScene(slicer.mrmlScene)
- self.modifierSegmentSelector.setToolTip('Contents of this segment will be used for modifying the selected segment. This segment itself will not be changed.')
- self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelector)
-
- self.applyButton.connect('clicked()', self.onApply)
- self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
- self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUI)
- self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("Operation", LOGICAL_COPY)
- self.scriptedEffect.setParameterDefault("ModifierSegmentID", "")
- self.scriptedEffect.setParameterDefault("BypassMasking", 1)
-
- def modifierSegmentID(self):
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- if not segmentationNode:
- return ""
- if not self.scriptedEffect.parameterDefined("ModifierSegmentID"):
- # Avoid logging warning
- return ""
- modifierSegmentIDs = self.scriptedEffect.parameter("ModifierSegmentID").split(';')
- if not modifierSegmentIDs:
- return ""
- return modifierSegmentIDs[0]
-
- def updateGUIFromMRML(self):
- operation = self.scriptedEffect.parameter("Operation")
- operationIndex = self.methodSelectorComboBox.findData(operation)
- wasBlocked = self.methodSelectorComboBox.blockSignals(True)
- self.methodSelectorComboBox.setCurrentIndex(operationIndex)
- self.methodSelectorComboBox.blockSignals(wasBlocked)
-
- modifierSegmentID = self.modifierSegmentID()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- wasBlocked = self.modifierSegmentSelector.blockSignals(True)
- self.modifierSegmentSelector.setSegmentationNode(segmentationNode)
- self.modifierSegmentSelector.setSelectedSegmentIDs([modifierSegmentID])
- self.modifierSegmentSelector.blockSignals(wasBlocked)
-
- modifierSegmentRequired = (operation in self.operationsRequireModifierSegment)
- self.modifierSegmentSelectorLabel.setVisible(modifierSegmentRequired)
- self.modifierSegmentSelector.setVisible(modifierSegmentRequired)
-
- if operation == LOGICAL_COPY:
- self.modifierSegmentSelectorLabel.text = "Copy from segment:"
- elif operation == LOGICAL_UNION:
- self.modifierSegmentSelectorLabel.text = "Add segment:"
- elif operation == LOGICAL_SUBTRACT:
- self.modifierSegmentSelectorLabel.text = "Subtract segment:"
- elif operation == LOGICAL_INTERSECT:
- self.modifierSegmentSelectorLabel.text = "Intersect with segment:"
- else:
- self.modifierSegmentSelectorLabel.text = "Modifier segment:"
-
- if modifierSegmentRequired and not modifierSegmentID:
- self.applyButton.setToolTip("Please select a modifier segment in the list below.")
- self.applyButton.enabled = False
- else:
- self.applyButton.setToolTip("")
- self.applyButton.enabled = True
-
- bypassMasking = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("BypassMasking") == 0 else qt.Qt.Checked
- wasBlocked = self.bypassMaskingCheckBox.blockSignals(True)
- self.bypassMaskingCheckBox.setCheckState(bypassMasking)
- self.bypassMaskingCheckBox.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- operationIndex = self.methodSelectorComboBox.currentIndex
- operation = self.methodSelectorComboBox.itemData(operationIndex)
- self.scriptedEffect.setParameter("Operation", operation)
-
- bypassMasking = 1 if self.bypassMaskingCheckBox.isChecked() else 0
- self.scriptedEffect.setParameter("BypassMasking", bypassMasking)
-
- modifierSegmentIDs = ';'.join(self.modifierSegmentSelector.selectedSegmentIDs()) # semicolon-separated list of segment IDs
- self.scriptedEffect.setParameter("ModifierSegmentID", modifierSegmentIDs)
-
- def getInvertedBinaryLabelmap(self, modifierLabelmap):
- fillValue = 1
- eraseValue = 0
- inverter = vtk.vtkImageThreshold()
- inverter.SetInputData(modifierLabelmap)
- inverter.SetInValue(fillValue)
- inverter.SetOutValue(eraseValue)
- inverter.ReplaceInOn()
- inverter.ThresholdByLower(0)
- inverter.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
- inverter.Update()
-
- invertedModifierLabelmap = slicer.vtkOrientedImageData()
- invertedModifierLabelmap.ShallowCopy(inverter.GetOutput())
- imageToWorldMatrix = vtk.vtkMatrix4x4()
- modifierLabelmap.GetImageToWorldMatrix(imageToWorldMatrix)
- invertedModifierLabelmap.SetGeometryFromImageToWorldMatrix(imageToWorldMatrix)
- return invertedModifierLabelmap
-
- def onApply(self):
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
-
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- self.scriptedEffect.saveStateForUndo()
-
- # Get modifier labelmap and parameters
-
- operation = self.scriptedEffect.parameter("Operation")
- bypassMasking = (self.scriptedEffect.integerParameter("BypassMasking") != 0)
-
- selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
-
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentation = segmentationNode.GetSegmentation()
-
- if operation in self.operationsRequireModifierSegment:
-
- # Get modifier segment
- modifierSegmentID = self.modifierSegmentID()
- if not modifierSegmentID:
- logging.error(f"Operation {operation} requires a selected modifier segment")
- return
- modifierSegment = segmentation.GetSegment(modifierSegmentID)
- modifierSegmentLabelmap = slicer.vtkOrientedImageData()
- segmentationNode.GetBinaryLabelmapRepresentation(modifierSegmentID, modifierSegmentLabelmap)
-
- # Get common geometry
- commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS, None)
- if not commonGeometryString:
- logging.info("Logical operation skipped: all segments are empty")
- return
- commonGeometryImage = slicer.vtkOrientedImageData()
- vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, commonGeometryImage, False)
-
- # Make sure modifier segment has correct geometry
- # (if modifier segment has been just copied over from another segment then its geometry may be different)
- if not vtkSegmentationCore.vtkOrientedImageDataResample.DoGeometriesMatch(commonGeometryImage, modifierSegmentLabelmap):
- modifierSegmentLabelmap_CommonGeometry = slicer.vtkOrientedImageData()
- vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- modifierSegmentLabelmap, commonGeometryImage, modifierSegmentLabelmap_CommonGeometry,
- False, # nearest neighbor interpolation,
- True # make sure resampled modifier segment is not cropped
- )
- modifierSegmentLabelmap = modifierSegmentLabelmap_CommonGeometry
-
- if operation == LOGICAL_COPY:
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
- elif operation == LOGICAL_UNION:
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd, bypassMasking)
- elif operation == LOGICAL_SUBTRACT:
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove, bypassMasking)
- elif operation == LOGICAL_INTERSECT:
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- intersectionLabelmap = slicer.vtkOrientedImageData()
- vtkSegmentationCore.vtkOrientedImageDataResample.MergeImage(
- selectedSegmentLabelmap, modifierSegmentLabelmap, intersectionLabelmap,
- vtkSegmentationCore.vtkOrientedImageDataResample.OPERATION_MINIMUM, selectedSegmentLabelmap.GetExtent())
- selectedSegmentLabelmapExtent = selectedSegmentLabelmap.GetExtent()
- modifierSegmentLabelmapExtent = modifierSegmentLabelmap.GetExtent()
- commonExtent = [max(selectedSegmentLabelmapExtent[0], modifierSegmentLabelmapExtent[0]),
- min(selectedSegmentLabelmapExtent[1], modifierSegmentLabelmapExtent[1]),
- max(selectedSegmentLabelmapExtent[2], modifierSegmentLabelmapExtent[2]),
- min(selectedSegmentLabelmapExtent[3], modifierSegmentLabelmapExtent[3]),
- max(selectedSegmentLabelmapExtent[4], modifierSegmentLabelmapExtent[4]),
- min(selectedSegmentLabelmapExtent[5], modifierSegmentLabelmapExtent[5])]
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- intersectionLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, commonExtent, bypassMasking)
-
- elif operation == LOGICAL_INVERT:
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- invertedSelectedSegmentLabelmap = self.getInvertedBinaryLabelmap(selectedSegmentLabelmap)
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- invertedSelectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
-
- elif operation == LOGICAL_CLEAR or operation == LOGICAL_FILL:
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(selectedSegmentLabelmap, 1 if operation == LOGICAL_FILL else 0, selectedSegmentLabelmap.GetExtent())
- self.scriptedEffect.modifySelectedSegmentByLabelmap(
- selectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
-
- else:
- logging.error(f"Unknown operation: {operation}")
+ def setupOptionsFrame(self):
+
+ self.methodSelectorComboBox = qt.QComboBox()
+ self.methodSelectorComboBox.addItem("Copy", LOGICAL_COPY)
+ self.methodSelectorComboBox.addItem("Add", LOGICAL_UNION)
+ self.methodSelectorComboBox.addItem("Subtract", LOGICAL_SUBTRACT)
+ self.methodSelectorComboBox.addItem("Intersect", LOGICAL_INTERSECT)
+ self.methodSelectorComboBox.addItem("Invert", LOGICAL_INVERT)
+ self.methodSelectorComboBox.addItem("Clear", LOGICAL_CLEAR)
+ self.methodSelectorComboBox.addItem("Fill", LOGICAL_FILL)
+ self.methodSelectorComboBox.setToolTip('Click Show details link above for description of operations.')
+
+ self.bypassMaskingCheckBox = qt.QCheckBox("Bypass masking")
+ self.bypassMaskingCheckBox.setToolTip("Ignore all masking options and only modify the selected segment.")
+ self.bypassMaskingCheckBox.objectName = self.__class__.__name__ + 'BypassMasking'
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+
+ operationFrame = qt.QHBoxLayout()
+ operationFrame.addWidget(self.methodSelectorComboBox)
+ operationFrame.addWidget(self.applyButton)
+ operationFrame.addWidget(self.bypassMaskingCheckBox)
+ self.marginSizeMmLabel = self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationFrame)
+
+ self.modifierSegmentSelectorLabel = qt.QLabel("Modifier segment:")
+ self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelectorLabel)
+
+ self.modifierSegmentSelector = slicer.qMRMLSegmentsTableView()
+ self.modifierSegmentSelector.selectionMode = qt.QAbstractItemView.SingleSelection
+ self.modifierSegmentSelector.headerVisible = False
+ self.modifierSegmentSelector.visibilityColumnVisible = False
+ self.modifierSegmentSelector.opacityColumnVisible = False
+
+ self.modifierSegmentSelector.setMRMLScene(slicer.mrmlScene)
+ self.modifierSegmentSelector.setToolTip('Contents of this segment will be used for modifying the selected segment. This segment itself will not be changed.')
+ self.scriptedEffect.addOptionsWidget(self.modifierSegmentSelector)
+
+ self.applyButton.connect('clicked()', self.onApply)
+ self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
+ self.modifierSegmentSelector.connect("selectionChanged(QItemSelection, QItemSelection)", self.updateMRMLFromGUI)
+ self.bypassMaskingCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("Operation", LOGICAL_COPY)
+ self.scriptedEffect.setParameterDefault("ModifierSegmentID", "")
+ self.scriptedEffect.setParameterDefault("BypassMasking", 1)
+
+ def modifierSegmentID(self):
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ if not segmentationNode:
+ return ""
+ if not self.scriptedEffect.parameterDefined("ModifierSegmentID"):
+ # Avoid logging warning
+ return ""
+ modifierSegmentIDs = self.scriptedEffect.parameter("ModifierSegmentID").split(';')
+ if not modifierSegmentIDs:
+ return ""
+ return modifierSegmentIDs[0]
+
+ def updateGUIFromMRML(self):
+ operation = self.scriptedEffect.parameter("Operation")
+ operationIndex = self.methodSelectorComboBox.findData(operation)
+ wasBlocked = self.methodSelectorComboBox.blockSignals(True)
+ self.methodSelectorComboBox.setCurrentIndex(operationIndex)
+ self.methodSelectorComboBox.blockSignals(wasBlocked)
+
+ modifierSegmentID = self.modifierSegmentID()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ wasBlocked = self.modifierSegmentSelector.blockSignals(True)
+ self.modifierSegmentSelector.setSegmentationNode(segmentationNode)
+ self.modifierSegmentSelector.setSelectedSegmentIDs([modifierSegmentID])
+ self.modifierSegmentSelector.blockSignals(wasBlocked)
+
+ modifierSegmentRequired = (operation in self.operationsRequireModifierSegment)
+ self.modifierSegmentSelectorLabel.setVisible(modifierSegmentRequired)
+ self.modifierSegmentSelector.setVisible(modifierSegmentRequired)
+
+ if operation == LOGICAL_COPY:
+ self.modifierSegmentSelectorLabel.text = "Copy from segment:"
+ elif operation == LOGICAL_UNION:
+ self.modifierSegmentSelectorLabel.text = "Add segment:"
+ elif operation == LOGICAL_SUBTRACT:
+ self.modifierSegmentSelectorLabel.text = "Subtract segment:"
+ elif operation == LOGICAL_INTERSECT:
+ self.modifierSegmentSelectorLabel.text = "Intersect with segment:"
+ else:
+ self.modifierSegmentSelectorLabel.text = "Modifier segment:"
+
+ if modifierSegmentRequired and not modifierSegmentID:
+ self.applyButton.setToolTip("Please select a modifier segment in the list below.")
+ self.applyButton.enabled = False
+ else:
+ self.applyButton.setToolTip("")
+ self.applyButton.enabled = True
+
+ bypassMasking = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("BypassMasking") == 0 else qt.Qt.Checked
+ wasBlocked = self.bypassMaskingCheckBox.blockSignals(True)
+ self.bypassMaskingCheckBox.setCheckState(bypassMasking)
+ self.bypassMaskingCheckBox.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ operationIndex = self.methodSelectorComboBox.currentIndex
+ operation = self.methodSelectorComboBox.itemData(operationIndex)
+ self.scriptedEffect.setParameter("Operation", operation)
+
+ bypassMasking = 1 if self.bypassMaskingCheckBox.isChecked() else 0
+ self.scriptedEffect.setParameter("BypassMasking", bypassMasking)
+
+ modifierSegmentIDs = ';'.join(self.modifierSegmentSelector.selectedSegmentIDs()) # semicolon-separated list of segment IDs
+ self.scriptedEffect.setParameter("ModifierSegmentID", modifierSegmentIDs)
+
+ def getInvertedBinaryLabelmap(self, modifierLabelmap):
+ fillValue = 1
+ eraseValue = 0
+ inverter = vtk.vtkImageThreshold()
+ inverter.SetInputData(modifierLabelmap)
+ inverter.SetInValue(fillValue)
+ inverter.SetOutValue(eraseValue)
+ inverter.ReplaceInOn()
+ inverter.ThresholdByLower(0)
+ inverter.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+ inverter.Update()
+
+ invertedModifierLabelmap = slicer.vtkOrientedImageData()
+ invertedModifierLabelmap.ShallowCopy(inverter.GetOutput())
+ imageToWorldMatrix = vtk.vtkMatrix4x4()
+ modifierLabelmap.GetImageToWorldMatrix(imageToWorldMatrix)
+ invertedModifierLabelmap.SetGeometryFromImageToWorldMatrix(imageToWorldMatrix)
+ return invertedModifierLabelmap
+
+ def onApply(self):
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ self.scriptedEffect.saveStateForUndo()
+
+ # Get modifier labelmap and parameters
+
+ operation = self.scriptedEffect.parameter("Operation")
+ bypassMasking = (self.scriptedEffect.integerParameter("BypassMasking") != 0)
+
+ selectedSegmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentation = segmentationNode.GetSegmentation()
+
+ if operation in self.operationsRequireModifierSegment:
+
+ # Get modifier segment
+ modifierSegmentID = self.modifierSegmentID()
+ if not modifierSegmentID:
+ logging.error(f"Operation {operation} requires a selected modifier segment")
+ return
+ modifierSegment = segmentation.GetSegment(modifierSegmentID)
+ modifierSegmentLabelmap = slicer.vtkOrientedImageData()
+ segmentationNode.GetBinaryLabelmapRepresentation(modifierSegmentID, modifierSegmentLabelmap)
+
+ # Get common geometry
+ commonGeometryString = segmentationNode.GetSegmentation().DetermineCommonLabelmapGeometry(
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS, None)
+ if not commonGeometryString:
+ logging.info("Logical operation skipped: all segments are empty")
+ return
+ commonGeometryImage = slicer.vtkOrientedImageData()
+ vtkSegmentationCore.vtkSegmentationConverter.DeserializeImageGeometry(commonGeometryString, commonGeometryImage, False)
+
+ # Make sure modifier segment has correct geometry
+ # (if modifier segment has been just copied over from another segment then its geometry may be different)
+ if not vtkSegmentationCore.vtkOrientedImageDataResample.DoGeometriesMatch(commonGeometryImage, modifierSegmentLabelmap):
+ modifierSegmentLabelmap_CommonGeometry = slicer.vtkOrientedImageData()
+ vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ modifierSegmentLabelmap, commonGeometryImage, modifierSegmentLabelmap_CommonGeometry,
+ False, # nearest neighbor interpolation,
+ True # make sure resampled modifier segment is not cropped
+ )
+ modifierSegmentLabelmap = modifierSegmentLabelmap_CommonGeometry
+
+ if operation == LOGICAL_COPY:
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
+ elif operation == LOGICAL_UNION:
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeAdd, bypassMasking)
+ elif operation == LOGICAL_SUBTRACT:
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ modifierSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeRemove, bypassMasking)
+ elif operation == LOGICAL_INTERSECT:
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ intersectionLabelmap = slicer.vtkOrientedImageData()
+ vtkSegmentationCore.vtkOrientedImageDataResample.MergeImage(
+ selectedSegmentLabelmap, modifierSegmentLabelmap, intersectionLabelmap,
+ vtkSegmentationCore.vtkOrientedImageDataResample.OPERATION_MINIMUM, selectedSegmentLabelmap.GetExtent())
+ selectedSegmentLabelmapExtent = selectedSegmentLabelmap.GetExtent()
+ modifierSegmentLabelmapExtent = modifierSegmentLabelmap.GetExtent()
+ commonExtent = [max(selectedSegmentLabelmapExtent[0], modifierSegmentLabelmapExtent[0]),
+ min(selectedSegmentLabelmapExtent[1], modifierSegmentLabelmapExtent[1]),
+ max(selectedSegmentLabelmapExtent[2], modifierSegmentLabelmapExtent[2]),
+ min(selectedSegmentLabelmapExtent[3], modifierSegmentLabelmapExtent[3]),
+ max(selectedSegmentLabelmapExtent[4], modifierSegmentLabelmapExtent[4]),
+ min(selectedSegmentLabelmapExtent[5], modifierSegmentLabelmapExtent[5])]
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ intersectionLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, commonExtent, bypassMasking)
+
+ elif operation == LOGICAL_INVERT:
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ invertedSelectedSegmentLabelmap = self.getInvertedBinaryLabelmap(selectedSegmentLabelmap)
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ invertedSelectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
+
+ elif operation == LOGICAL_CLEAR or operation == LOGICAL_FILL:
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(selectedSegmentLabelmap, 1 if operation == LOGICAL_FILL else 0, selectedSegmentLabelmap.GetExtent())
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(
+ selectedSegmentLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, bypassMasking)
+
+ else:
+ logging.error(f"Unknown operation: {operation}")
LOGICAL_COPY = 'COPY'
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py
index c430cb8eb7b..f9ec140c25f 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMarginEffect.py
@@ -11,238 +11,238 @@
class SegmentEditorMarginEffect(AbstractScriptedSegmentEditorEffect):
- """ MaringEffect grows or shrinks the segment by a specified margin
- """
+ """ MaringEffect grows or shrinks the segment by a specified margin
+ """
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Margin'
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Margin'
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Margin.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Margin.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
- def helpText(self):
- return "Grow or shrink selected segment by specified margin size."
+ def helpText(self):
+ return "Grow or shrink selected segment by specified margin size."
- def setupOptionsFrame(self):
+ def setupOptionsFrame(self):
- operationLayout = qt.QVBoxLayout()
+ operationLayout = qt.QVBoxLayout()
- self.shrinkOptionRadioButton = qt.QRadioButton("Shrink")
- self.growOptionRadioButton = qt.QRadioButton("Grow")
- operationLayout.addWidget(self.shrinkOptionRadioButton)
- operationLayout.addWidget(self.growOptionRadioButton)
- self.growOptionRadioButton.setChecked(True)
+ self.shrinkOptionRadioButton = qt.QRadioButton("Shrink")
+ self.growOptionRadioButton = qt.QRadioButton("Grow")
+ operationLayout.addWidget(self.shrinkOptionRadioButton)
+ operationLayout.addWidget(self.growOptionRadioButton)
+ self.growOptionRadioButton.setChecked(True)
- self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
+ self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
- self.marginSizeMMSpinBox = slicer.qMRMLSpinBox()
- self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
- self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.")
- self.marginSizeMMSpinBox.quantity = "length"
- self.marginSizeMMSpinBox.value = 3.0
- self.marginSizeMMSpinBox.singleStep = 1.0
+ self.marginSizeMMSpinBox = slicer.qMRMLSpinBox()
+ self.marginSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
+ self.marginSizeMMSpinBox.setToolTip("Segment boundaries will be shifted by this distance. Positive value means the segments will grow, negative value means segment will shrink.")
+ self.marginSizeMMSpinBox.quantity = "length"
+ self.marginSizeMMSpinBox.value = 3.0
+ self.marginSizeMMSpinBox.singleStep = 1.0
- self.marginSizeLabel = qt.QLabel()
- self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.")
+ self.marginSizeLabel = qt.QLabel()
+ self.marginSizeLabel.setToolTip("Size change in pixel. Computed from the segment's spacing and the specified margin size.")
- marginSizeFrame = qt.QHBoxLayout()
- marginSizeFrame.addWidget(self.marginSizeMMSpinBox)
- self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame)
- self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel)
+ marginSizeFrame = qt.QHBoxLayout()
+ marginSizeFrame.addWidget(self.marginSizeMMSpinBox)
+ self.marginSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Margin size:", marginSizeFrame)
+ self.scriptedEffect.addLabeledOptionsWidget("", self.marginSizeLabel)
- self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
- self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \
+ self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
+ self.applyToAllVisibleSegmentsCheckBox.setToolTip("Grow or shrink all visible segments in this segmentation node. \
This operation may take a while.")
- self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
- self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
-
- self.applyButton.connect('clicked()', self.onApply)
- self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.growOptionRadioButton.connect("toggled(bool)", self.growOperationToggled)
- self.shrinkOptionRadioButton.connect("toggled(bool)", self.shrinkOperationToggled)
- self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
- self.scriptedEffect.setParameterDefault("MarginSizeMm", 3)
-
- def getMarginSizePixel(self):
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
-
- marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm"))
- marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing]
- return marginSizePixel
-
- def updateGUIFromMRML(self):
- marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
- wasBlocked = self.marginSizeMMSpinBox.blockSignals(True)
- self.marginSizeMMSpinBox.value = abs(marginSizeMM)
- self.marginSizeMMSpinBox.blockSignals(wasBlocked)
-
- wasBlocked = self.growOptionRadioButton.blockSignals(True)
- self.growOptionRadioButton.setChecked(marginSizeMM > 0)
- self.growOptionRadioButton.blockSignals(wasBlocked)
-
- wasBlocked = self.shrinkOptionRadioButton.blockSignals(True)
- self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0)
- self.shrinkOptionRadioButton.blockSignals(wasBlocked)
-
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
- marginSizePixel = self.getMarginSizePixel()
- if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1:
- self.marginSizeLabel.text = "Not feasible at current resolution."
- self.applyButton.setEnabled(False)
- else:
- marginSizeMM = self.getMarginSizeMM()
- self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel)
- self.applyButton.setEnabled(True)
- else:
- self.marginSizeLabel.text = "Empty segment"
-
- applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
- wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
- self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
- self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
-
- self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
-
- def growOperationToggled(self, toggled):
- if toggled:
- self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value)
-
- def shrinkOperationToggled(self, toggled):
- if toggled:
- self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value)
-
- def updateMRMLFromGUI(self):
- marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value)
- self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM)
- applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
- self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
-
- def getMarginSizeMM(self):
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
-
- marginSizePixel = self.getMarginSizePixel()
- marginSizeMM = [abs((marginSizePixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
- for i in range(3):
- if marginSizeMM[i] > 0:
- marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))), 1))
- return marginSizeMM
-
- def showStatusMessage(self, msg, timeoutMsec=500):
+ self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
+ self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Grows or shrinks selected segment /default) or all segments (checkbox) by the specified margin.")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+
+ self.applyButton.connect('clicked()', self.onApply)
+ self.marginSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.growOptionRadioButton.connect("toggled(bool)", self.growOperationToggled)
+ self.shrinkOptionRadioButton.connect("toggled(bool)", self.shrinkOperationToggled)
+ self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
+ self.scriptedEffect.setParameterDefault("MarginSizeMm", 3)
+
+ def getMarginSizePixel(self):
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+
+ marginSizeMM = abs(self.scriptedEffect.doubleParameter("MarginSizeMm"))
+ marginSizePixel = [int(math.floor(marginSizeMM / spacing)) for spacing in selectedSegmentLabelmapSpacing]
+ return marginSizePixel
+
+ def updateGUIFromMRML(self):
+ marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
+ wasBlocked = self.marginSizeMMSpinBox.blockSignals(True)
+ self.marginSizeMMSpinBox.value = abs(marginSizeMM)
+ self.marginSizeMMSpinBox.blockSignals(wasBlocked)
+
+ wasBlocked = self.growOptionRadioButton.blockSignals(True)
+ self.growOptionRadioButton.setChecked(marginSizeMM > 0)
+ self.growOptionRadioButton.blockSignals(wasBlocked)
+
+ wasBlocked = self.shrinkOptionRadioButton.blockSignals(True)
+ self.shrinkOptionRadioButton.setChecked(marginSizeMM < 0)
+ self.shrinkOptionRadioButton.blockSignals(wasBlocked)
+
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+ marginSizePixel = self.getMarginSizePixel()
+ if marginSizePixel[0] < 1 or marginSizePixel[1] < 1 or marginSizePixel[2] < 1:
+ self.marginSizeLabel.text = "Not feasible at current resolution."
+ self.applyButton.setEnabled(False)
+ else:
+ marginSizeMM = self.getMarginSizeMM()
+ self.marginSizeLabel.text = "Actual: {} x {} x {} mm ({}x{}x{} pixel)".format(*marginSizeMM, *marginSizePixel)
+ self.applyButton.setEnabled(True)
+ else:
+ self.marginSizeLabel.text = "Empty segment"
+
+ applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
+ wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
+ self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
+ self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
+
+ self.setWidgetMinMaxStepFromImageSpacing(self.marginSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
+
+ def growOperationToggled(self, toggled):
+ if toggled:
+ self.scriptedEffect.setParameter("MarginSizeMm", self.marginSizeMMSpinBox.value)
+
+ def shrinkOperationToggled(self, toggled):
+ if toggled:
+ self.scriptedEffect.setParameter("MarginSizeMm", -self.marginSizeMMSpinBox.value)
+
+ def updateMRMLFromGUI(self):
+ marginSizeMM = (self.marginSizeMMSpinBox.value) if self.growOptionRadioButton.checked else (-self.marginSizeMMSpinBox.value)
+ self.scriptedEffect.setParameter("MarginSizeMm", marginSizeMM)
+ applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
+ self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
+
+ def getMarginSizeMM(self):
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
+
+ marginSizePixel = self.getMarginSizePixel()
+ marginSizeMM = [abs((marginSizePixel[i]) * selectedSegmentLabelmapSpacing[i]) for i in range(3)]
+ for i in range(3):
+ if marginSizeMM[i] > 0:
+ marginSizeMM[i] = round(marginSizeMM[i], max(int(-math.floor(math.log10(marginSizeMM[i]))), 1))
+ return marginSizeMM
+
+ def showStatusMessage(self, msg, timeoutMsec=500):
slicer.util.showStatusMessage(msg, timeoutMsec)
slicer.app.processEvents()
- def processMargin(self):
- # Get modifier labelmap and parameters
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
-
- marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
-
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(selectedSegmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
- if (marginSizeMM < 0):
- # The distance filter used in the margin filter starts at zero at the border voxels,
- # so if we need to shrink the margin, it is more accurate to invert the labelmap and
- # use positive distance when calculating the margin
- thresh.SetInValue(labelValue)
- thresh.SetOutValue(backgroundValue)
-
- import vtkITK
- margin = vtkITK.vtkITKImageMargin()
- margin.SetInputConnection(thresh.GetOutputPort())
- margin.CalculateMarginInMMOn()
- margin.SetOuterMarginMM(abs(marginSizeMM))
- margin.Update()
-
- if marginSizeMM >= 0:
- modifierLabelmap.ShallowCopy(margin.GetOutput())
- else:
- # If we are shrinking then the result needs to be inverted.
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(margin.GetOutput())
- thresh.ThresholdByLower(0)
- thresh.SetInValue(labelValue)
- thresh.SetOutValue(backgroundValue)
- thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
- thresh.Update()
- modifierLabelmap.ShallowCopy(thresh.GetOutput())
-
- # Apply changes
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
-
- def onApply(self):
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
-
- try:
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- self.scriptedEffect.saveStateForUndo()
-
- applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
- if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
-
- if applyToAllVisibleSegments:
- # Smooth all visible segments
- inputSegmentIDs = vtk.vtkStringArray()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
- segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
- segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
- # store which segment was selected before operation
- selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
- if inputSegmentIDs.GetNumberOfValues() == 0:
- logging.info("Margin operation skipped: there are no visible segments.")
- return
- # select input segments one by one, process
- for index in range(inputSegmentIDs.GetNumberOfValues()):
- segmentID = inputSegmentIDs.GetValue(index)
- self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
- segmentEditorNode.SetSelectedSegmentID(segmentID)
- self.processMargin()
- # restore segment selection
- segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
- else:
- self.processMargin()
-
- finally:
- qt.QApplication.restoreOverrideCursor()
+ def processMargin(self):
+ # Get modifier labelmap and parameters
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+
+ marginSizeMM = self.scriptedEffect.doubleParameter("MarginSizeMm")
+
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(selectedSegmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
+ if (marginSizeMM < 0):
+ # The distance filter used in the margin filter starts at zero at the border voxels,
+ # so if we need to shrink the margin, it is more accurate to invert the labelmap and
+ # use positive distance when calculating the margin
+ thresh.SetInValue(labelValue)
+ thresh.SetOutValue(backgroundValue)
+
+ import vtkITK
+ margin = vtkITK.vtkITKImageMargin()
+ margin.SetInputConnection(thresh.GetOutputPort())
+ margin.CalculateMarginInMMOn()
+ margin.SetOuterMarginMM(abs(marginSizeMM))
+ margin.Update()
+
+ if marginSizeMM >= 0:
+ modifierLabelmap.ShallowCopy(margin.GetOutput())
+ else:
+ # If we are shrinking then the result needs to be inverted.
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(margin.GetOutput())
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(labelValue)
+ thresh.SetOutValue(backgroundValue)
+ thresh.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
+ thresh.Update()
+ modifierLabelmap.ShallowCopy(thresh.GetOutput())
+
+ # Apply changes
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+
+ def onApply(self):
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+
+ try:
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ self.scriptedEffect.saveStateForUndo()
+
+ applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
+ if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
+
+ if applyToAllVisibleSegments:
+ # Smooth all visible segments
+ inputSegmentIDs = vtk.vtkStringArray()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
+ segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
+ segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
+ # store which segment was selected before operation
+ selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
+ if inputSegmentIDs.GetNumberOfValues() == 0:
+ logging.info("Margin operation skipped: there are no visible segments.")
+ return
+ # select input segments one by one, process
+ for index in range(inputSegmentIDs.GetNumberOfValues()):
+ segmentID = inputSegmentIDs.GetValue(index)
+ self.showStatusMessage(f'Processing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
+ segmentEditorNode.SetSelectedSegmentID(segmentID)
+ self.processMargin()
+ # restore segment selection
+ segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
+ else:
+ self.processMargin()
+
+ finally:
+ qt.QApplication.restoreOverrideCursor()
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py
index 9d35c932213..c10610aaa84 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorMaskVolumeEffect.py
@@ -5,368 +5,368 @@
class SegmentEditorMaskVolumeEffect(AbstractScriptedSegmentEditorEffect):
- """This effect fills a selected volume node inside and/or outside a segment with a chosen value.
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Mask volume'
- scriptedEffect.perSegment = True # this effect operates on a single selected segment
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- # Effect-specific members
- self.buttonToOperationNameMap = {}
-
- def clone(self):
- # It should not be necessary to modify this method
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- # It should not be necessary to modify this method
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MaskVolume.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Use the currently selected segment as a mask to blank out regions in a volume.
The mask is applied to the master volume by default.
+ """This effect fills a selected volume node inside and/or outside a segment with a chosen value.
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Mask volume'
+ scriptedEffect.perSegment = True # this effect operates on a single selected segment
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ # Effect-specific members
+ self.buttonToOperationNameMap = {}
+
+ def clone(self):
+ # It should not be necessary to modify this method
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ # It should not be necessary to modify this method
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MaskVolume.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Use the currently selected segment as a mask to blank out regions in a volume.
The mask is applied to the master volume by default.
Fill inside and outside operation creates a binary labelmap volume as output, with the inside and outside fill values modifiable.
"""
- def setupOptionsFrame(self):
- self.operationRadioButtons = []
- self.updatingGUIFromMRML = False
- self.visibleIcon = qt.QIcon(":/Icons/Small/SlicerVisible.png")
- self.invisibleIcon = qt.QIcon(":/Icons/Small/SlicerInvisible.png")
-
- # Fill operation buttons
- self.fillInsideButton = qt.QRadioButton("Fill inside")
- self.operationRadioButtons.append(self.fillInsideButton)
- self.buttonToOperationNameMap[self.fillInsideButton] = 'FILL_INSIDE'
-
- self.fillOutsideButton = qt.QRadioButton("Fill outside")
- self.operationRadioButtons.append(self.fillOutsideButton)
- self.buttonToOperationNameMap[self.fillOutsideButton] = 'FILL_OUTSIDE'
-
- self.binaryMaskFillButton = qt.QRadioButton("Fill inside and outside")
- self.binaryMaskFillButton.setToolTip("Create a labelmap volume with specified inside and outside fill values.")
- self.operationRadioButtons.append(self.binaryMaskFillButton)
- self.buttonToOperationNameMap[self.binaryMaskFillButton] = 'FILL_INSIDE_AND_OUTSIDE'
-
- # Operation buttons layout
- operationLayout = qt.QGridLayout()
- operationLayout.addWidget(self.fillInsideButton, 0, 0)
- operationLayout.addWidget(self.fillOutsideButton, 1, 0)
- operationLayout.addWidget(self.binaryMaskFillButton, 0, 1)
- self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
-
- # fill value
- self.fillValueEdit = ctk.ctkDoubleSpinBox()
- self.fillValueEdit.setToolTip("Choose the voxel intensity that will be used to fill the masked region.")
- self.fillValueLabel = qt.QLabel("Fill value: ")
-
- # Binary mask fill outside value
- self.binaryMaskFillOutsideEdit = ctk.ctkDoubleSpinBox()
- self.binaryMaskFillOutsideEdit.setToolTip("Choose the voxel intensity that will be used to fill outside the mask.")
- self.fillOutsideLabel = qt.QLabel("Outside fill value: ")
-
- # Binary mask fill outside value
- self.binaryMaskFillInsideEdit = ctk.ctkDoubleSpinBox()
- self.binaryMaskFillInsideEdit.setToolTip("Choose the voxel intensity that will be used to fill inside the mask.")
- self.fillInsideLabel = qt.QLabel(" Inside fill value: ")
-
- for fillValueEdit in [self.fillValueEdit, self.binaryMaskFillOutsideEdit, self.binaryMaskFillInsideEdit]:
- fillValueEdit.decimalsOption = ctk.ctkDoubleSpinBox.DecimalsByValue + ctk.ctkDoubleSpinBox.DecimalsByKey + ctk.ctkDoubleSpinBox.InsertDecimals
- fillValueEdit.minimum = vtk.vtkDoubleArray().GetDataTypeMin(vtk.VTK_DOUBLE)
- fillValueEdit.maximum = vtk.vtkDoubleArray().GetDataTypeMax(vtk.VTK_DOUBLE)
- fillValueEdit.connect("valueChanged(double)", self.fillValueChanged)
-
- # Fill value layouts
- fillValueLayout = qt.QFormLayout()
- fillValueLayout.addRow(self.fillValueLabel, self.fillValueEdit)
-
- fillOutsideLayout = qt.QFormLayout()
- fillOutsideLayout.addRow(self.fillOutsideLabel, self.binaryMaskFillOutsideEdit)
-
- fillInsideLayout = qt.QFormLayout()
- fillInsideLayout.addRow(self.fillInsideLabel, self.binaryMaskFillInsideEdit)
-
- binaryMaskFillLayout = qt.QHBoxLayout()
- binaryMaskFillLayout.addLayout(fillOutsideLayout)
- binaryMaskFillLayout.addLayout(fillInsideLayout)
- fillValuesSpinBoxLayout = qt.QFormLayout()
- fillValuesSpinBoxLayout.addRow(binaryMaskFillLayout)
- fillValuesSpinBoxLayout.addRow(fillValueLayout)
- self.scriptedEffect.addOptionsWidget(fillValuesSpinBoxLayout)
-
- # input volume selector
- self.inputVolumeSelector = slicer.qMRMLNodeComboBox()
- self.inputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
- self.inputVolumeSelector.selectNodeUponCreation = True
- self.inputVolumeSelector.addEnabled = True
- self.inputVolumeSelector.removeEnabled = True
- self.inputVolumeSelector.noneEnabled = True
- self.inputVolumeSelector.noneDisplay = "(Master volume)"
- self.inputVolumeSelector.showHidden = False
- self.inputVolumeSelector.setMRMLScene(slicer.mrmlScene)
- self.inputVolumeSelector.setToolTip("Volume to mask. Default is current master volume node.")
- self.inputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputVolumeChanged)
-
- self.inputVisibilityButton = qt.QToolButton()
- self.inputVisibilityButton.setIcon(self.invisibleIcon)
- self.inputVisibilityButton.connect('clicked()', self.onInputVisibilityButtonClicked)
- inputLayout = qt.QHBoxLayout()
- inputLayout.addWidget(self.inputVisibilityButton)
- inputLayout.addWidget(self.inputVolumeSelector)
- self.scriptedEffect.addLabeledOptionsWidget("Input Volume: ", inputLayout)
-
- # output volume selector
- self.outputVolumeSelector = slicer.qMRMLNodeComboBox()
- self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"]
- self.outputVolumeSelector.selectNodeUponCreation = True
- self.outputVolumeSelector.addEnabled = True
- self.outputVolumeSelector.removeEnabled = True
- self.outputVolumeSelector.renameEnabled = True
- self.outputVolumeSelector.noneEnabled = True
- self.outputVolumeSelector.noneDisplay = "(Create new Volume)"
- self.outputVolumeSelector.showHidden = False
- self.outputVolumeSelector.setMRMLScene(slicer.mrmlScene)
- self.outputVolumeSelector.setToolTip("Masked output volume. It may be the same as the input volume for cumulative masking.")
- self.outputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onOutputVolumeChanged)
-
- self.outputVisibilityButton = qt.QToolButton()
- self.outputVisibilityButton.setIcon(self.invisibleIcon)
- self.outputVisibilityButton.connect('clicked()', self.onOutputVisibilityButtonClicked)
- outputLayout = qt.QHBoxLayout()
- outputLayout.addWidget(self.outputVisibilityButton)
- outputLayout.addWidget(self.outputVolumeSelector)
- self.scriptedEffect.addLabeledOptionsWidget("Output Volume: ", outputLayout)
-
- # Apply button
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Apply segment as volume mask. No undo operation available once applied.")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
- self.applyButton.connect('clicked()', self.onApply)
-
- for button in self.operationRadioButtons:
- button.connect('toggled(bool)',
- lambda toggle, widget=self.buttonToOperationNameMap[button]: self.onOperationSelectionChanged(widget, toggle))
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("FillValue", "0")
- self.scriptedEffect.setParameterDefault("BinaryMaskFillValueInside", "1")
- self.scriptedEffect.setParameterDefault("BinaryMaskFillValueOutside", "0")
- self.scriptedEffect.setParameterDefault("Operation", "FILL_OUTSIDE")
-
- def isVolumeVisible(self, volumeNode):
- if not volumeNode:
- return False
- volumeNodeID = volumeNode.GetID()
- lm = slicer.app.layoutManager()
- sliceViewNames = lm.sliceViewNames()
- for sliceViewName in sliceViewNames:
- sliceWidget = lm.sliceWidget(sliceViewName)
- if volumeNodeID == sliceWidget.mrmlSliceCompositeNode().GetBackgroundVolumeID():
- return True
- return False
-
- def updateGUIFromMRML(self):
- self.updatingGUIFromMRML = True
-
- self.fillValueEdit.setValue(float(self.scriptedEffect.parameter("FillValue")) if self.scriptedEffect.parameter("FillValue") else 0)
- self.binaryMaskFillOutsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueOutside"))
- if self.scriptedEffect.parameter("BinaryMaskFillValueOutside") else 0)
- self.binaryMaskFillInsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueInside"))
- if self.scriptedEffect.parameter("BinaryMaskFillValueInside") else 1)
- operationName = self.scriptedEffect.parameter("Operation")
- if operationName:
- operationButton = list(self.buttonToOperationNameMap.keys())[list(self.buttonToOperationNameMap.values()).index(operationName)]
- operationButton.setChecked(True)
-
- inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume")
- self.inputVolumeSelector.setCurrentNode(inputVolume)
- outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume")
- self.outputVolumeSelector.setCurrentNode(outputVolume)
-
- masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
- if inputVolume is None:
- inputVolume = masterVolume
-
- self.fillValueEdit.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"])
- self.fillValueLabel.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"])
- self.binaryMaskFillInsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
- self.fillInsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
- self.binaryMaskFillOutsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
- self.fillOutsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
- if operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]:
- if self.outputVolumeSelector.noneDisplay != "(Create new Volume)":
- self.outputVolumeSelector.noneDisplay = "(Create new Volume)"
+ def setupOptionsFrame(self):
+ self.operationRadioButtons = []
+ self.updatingGUIFromMRML = False
+ self.visibleIcon = qt.QIcon(":/Icons/Small/SlicerVisible.png")
+ self.invisibleIcon = qt.QIcon(":/Icons/Small/SlicerInvisible.png")
+
+ # Fill operation buttons
+ self.fillInsideButton = qt.QRadioButton("Fill inside")
+ self.operationRadioButtons.append(self.fillInsideButton)
+ self.buttonToOperationNameMap[self.fillInsideButton] = 'FILL_INSIDE'
+
+ self.fillOutsideButton = qt.QRadioButton("Fill outside")
+ self.operationRadioButtons.append(self.fillOutsideButton)
+ self.buttonToOperationNameMap[self.fillOutsideButton] = 'FILL_OUTSIDE'
+
+ self.binaryMaskFillButton = qt.QRadioButton("Fill inside and outside")
+ self.binaryMaskFillButton.setToolTip("Create a labelmap volume with specified inside and outside fill values.")
+ self.operationRadioButtons.append(self.binaryMaskFillButton)
+ self.buttonToOperationNameMap[self.binaryMaskFillButton] = 'FILL_INSIDE_AND_OUTSIDE'
+
+ # Operation buttons layout
+ operationLayout = qt.QGridLayout()
+ operationLayout.addWidget(self.fillInsideButton, 0, 0)
+ operationLayout.addWidget(self.fillOutsideButton, 1, 0)
+ operationLayout.addWidget(self.binaryMaskFillButton, 0, 1)
+ self.scriptedEffect.addLabeledOptionsWidget("Operation:", operationLayout)
+
+ # fill value
+ self.fillValueEdit = ctk.ctkDoubleSpinBox()
+ self.fillValueEdit.setToolTip("Choose the voxel intensity that will be used to fill the masked region.")
+ self.fillValueLabel = qt.QLabel("Fill value: ")
+
+ # Binary mask fill outside value
+ self.binaryMaskFillOutsideEdit = ctk.ctkDoubleSpinBox()
+ self.binaryMaskFillOutsideEdit.setToolTip("Choose the voxel intensity that will be used to fill outside the mask.")
+ self.fillOutsideLabel = qt.QLabel("Outside fill value: ")
+
+ # Binary mask fill outside value
+ self.binaryMaskFillInsideEdit = ctk.ctkDoubleSpinBox()
+ self.binaryMaskFillInsideEdit.setToolTip("Choose the voxel intensity that will be used to fill inside the mask.")
+ self.fillInsideLabel = qt.QLabel(" Inside fill value: ")
+
+ for fillValueEdit in [self.fillValueEdit, self.binaryMaskFillOutsideEdit, self.binaryMaskFillInsideEdit]:
+ fillValueEdit.decimalsOption = ctk.ctkDoubleSpinBox.DecimalsByValue + ctk.ctkDoubleSpinBox.DecimalsByKey + ctk.ctkDoubleSpinBox.InsertDecimals
+ fillValueEdit.minimum = vtk.vtkDoubleArray().GetDataTypeMin(vtk.VTK_DOUBLE)
+ fillValueEdit.maximum = vtk.vtkDoubleArray().GetDataTypeMax(vtk.VTK_DOUBLE)
+ fillValueEdit.connect("valueChanged(double)", self.fillValueChanged)
+
+ # Fill value layouts
+ fillValueLayout = qt.QFormLayout()
+ fillValueLayout.addRow(self.fillValueLabel, self.fillValueEdit)
+
+ fillOutsideLayout = qt.QFormLayout()
+ fillOutsideLayout.addRow(self.fillOutsideLabel, self.binaryMaskFillOutsideEdit)
+
+ fillInsideLayout = qt.QFormLayout()
+ fillInsideLayout.addRow(self.fillInsideLabel, self.binaryMaskFillInsideEdit)
+
+ binaryMaskFillLayout = qt.QHBoxLayout()
+ binaryMaskFillLayout.addLayout(fillOutsideLayout)
+ binaryMaskFillLayout.addLayout(fillInsideLayout)
+ fillValuesSpinBoxLayout = qt.QFormLayout()
+ fillValuesSpinBoxLayout.addRow(binaryMaskFillLayout)
+ fillValuesSpinBoxLayout.addRow(fillValueLayout)
+ self.scriptedEffect.addOptionsWidget(fillValuesSpinBoxLayout)
+
+ # input volume selector
+ self.inputVolumeSelector = slicer.qMRMLNodeComboBox()
+ self.inputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
+ self.inputVolumeSelector.selectNodeUponCreation = True
+ self.inputVolumeSelector.addEnabled = True
+ self.inputVolumeSelector.removeEnabled = True
+ self.inputVolumeSelector.noneEnabled = True
+ self.inputVolumeSelector.noneDisplay = "(Master volume)"
+ self.inputVolumeSelector.showHidden = False
+ self.inputVolumeSelector.setMRMLScene(slicer.mrmlScene)
+ self.inputVolumeSelector.setToolTip("Volume to mask. Default is current master volume node.")
+ self.inputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onInputVolumeChanged)
+
+ self.inputVisibilityButton = qt.QToolButton()
+ self.inputVisibilityButton.setIcon(self.invisibleIcon)
+ self.inputVisibilityButton.connect('clicked()', self.onInputVisibilityButtonClicked)
+ inputLayout = qt.QHBoxLayout()
+ inputLayout.addWidget(self.inputVisibilityButton)
+ inputLayout.addWidget(self.inputVolumeSelector)
+ self.scriptedEffect.addLabeledOptionsWidget("Input Volume: ", inputLayout)
+
+ # output volume selector
+ self.outputVolumeSelector = slicer.qMRMLNodeComboBox()
self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"]
- else:
- if self.outputVolumeSelector.noneDisplay != "(Create new Labelmap Volume)":
- self.outputVolumeSelector.noneDisplay = "(Create new Labelmap Volume)"
- self.outputVolumeSelector.nodeTypes = ["vtkMRMLLabelMapVolumeNode", "vtkMRMLScalarVolumeNode"]
-
- self.inputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(inputVolume) else self.invisibleIcon)
- self.outputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(outputVolume) else self.invisibleIcon)
-
- self.updatingGUIFromMRML = False
-
- def updateMRMLFromGUI(self):
- if self.updatingGUIFromMRML:
- return
- self.scriptedEffect.setParameter("FillValue", self.fillValueEdit.value)
- self.scriptedEffect.setParameter("BinaryMaskFillValueInside", self.binaryMaskFillInsideEdit.value)
- self.scriptedEffect.setParameter("BinaryMaskFillValueOutside", self.binaryMaskFillOutsideEdit.value)
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID)
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID)
-
- def activate(self):
- self.scriptedEffect.setParameter("InputVisibility", "True")
-
- def deactivate(self):
- if self.outputVolumeSelector.currentNode() is not self.scriptedEffect.parameterSetNode().GetMasterVolumeNode():
- self.scriptedEffect.setParameter("OutputVisibility", "False")
- slicer.util.setSliceViewerLayers(background=self.scriptedEffect.parameterSetNode().GetMasterVolumeNode())
-
- def onOperationSelectionChanged(self, operationName, toggle):
- if not toggle:
- return
- self.scriptedEffect.setParameter("Operation", operationName)
-
- def getInputVolume(self):
- inputVolume = self.inputVolumeSelector.currentNode()
- if inputVolume is None:
- inputVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
- return inputVolume
-
- def onInputVisibilityButtonClicked(self):
- inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume")
- masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
- if inputVolume is None:
- inputVolume = masterVolume
- if inputVolume:
- slicer.util.setSliceViewerLayers(background=inputVolume)
- self.updateGUIFromMRML()
-
- def onOutputVisibilityButtonClicked(self):
- outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume")
- if outputVolume:
- slicer.util.setSliceViewerLayers(background=outputVolume)
- self.updateGUIFromMRML()
-
- def onInputVolumeChanged(self):
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID)
- self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually
-
- def onOutputVolumeChanged(self):
- self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID)
- self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually
-
- def fillValueChanged(self):
- self.updateMRMLFromGUI()
-
- def onApply(self):
- inputVolume = self.getInputVolume()
- outputVolume = self.outputVolumeSelector.currentNode()
- operationMode = self.scriptedEffect.parameter("Operation")
- if not outputVolume:
- # Create new node for output
- volumesLogic = slicer.modules.volumes.logic()
- scene = inputVolume.GetScene()
- if operationMode == "FILL_INSIDE_AND_OUTSIDE":
- outputVolumeName = inputVolume.GetName() + " label"
- outputVolume = volumesLogic.CreateAndAddLabelVolume(inputVolume, outputVolumeName)
- else:
- outputVolumeName = inputVolume.GetName() + " masked"
- outputVolume = volumesLogic.CloneVolumeGeneric(scene, inputVolume, outputVolumeName, False)
- self.outputVolumeSelector.setCurrentNode(outputVolume)
-
- if operationMode in ["FILL_INSIDE", "FILL_OUTSIDE"]:
- fillValues = [self.fillValueEdit.value]
- else:
- fillValues = [self.binaryMaskFillInsideEdit.value, self.binaryMaskFillOutsideEdit.value]
-
- segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
-
- slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
- SegmentEditorMaskVolumeEffect.maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolume, outputVolume)
-
- slicer.util.setSliceViewerLayers(background=outputVolume)
- qt.QApplication.restoreOverrideCursor()
-
- self.updateGUIFromMRML()
-
- @staticmethod
- def maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolumeNode, outputVolumeNode, maskExtent=None):
- """
- Fill voxels of the input volume inside/outside the masking model with the provided fill value
- maskExtent: optional output to return computed mask extent (expected input is a 6-element list)
- fillValues: list containing one or two fill values. If fill mode is inside or outside then only one value is specified in the list.
- If fill mode is inside&outside then the list must contain two values: first is the inside fill, second is the outside fill value.
- """
-
- segmentIDs = vtk.vtkStringArray()
- segmentIDs.InsertNextValue(segmentID)
- maskVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(inputVolumeNode, "TemporaryVolumeMask")
- if not maskVolumeNode:
- logging.error("maskVolumeWithSegment failed: invalid maskVolumeNode")
- return False
-
- if not slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, segmentIDs, maskVolumeNode, inputVolumeNode):
- logging.error("maskVolumeWithSegment failed: ExportSegmentsToLabelmapNode error")
- slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode())
- slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode())
- slicer.mrmlScene.RemoveNode(maskVolumeNode)
- return False
-
- if maskExtent:
- img = slicer.modules.segmentations.logic().CreateOrientedImageDataFromVolumeNode(maskVolumeNode)
- img.UnRegister(None)
- import vtkSegmentationCorePython as vtkSegmentationCore
- vtkSegmentationCore.vtkOrientedImageDataResample.CalculateEffectiveExtent(img, maskExtent, 0)
-
- maskToStencil = vtk.vtkImageToImageStencil()
- maskToStencil.ThresholdByLower(0)
- maskToStencil.SetInputData(maskVolumeNode.GetImageData())
-
- stencil = vtk.vtkImageStencil()
-
- if operationMode == "FILL_INSIDE_AND_OUTSIDE":
- # Set input to constant value
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(inputVolumeNode.GetImageData())
- thresh.ThresholdByLower(0)
- thresh.SetInValue(fillValues[1])
- thresh.SetOutValue(fillValues[1])
- thresh.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType())
- thresh.Update()
- stencil.SetInputData(thresh.GetOutput())
- else:
- stencil.SetInputData(inputVolumeNode.GetImageData())
-
- stencil.SetStencilConnection(maskToStencil.GetOutputPort())
- stencil.SetReverseStencil(operationMode == "FILL_OUTSIDE")
- stencil.SetBackgroundValue(fillValues[0])
- stencil.Update()
-
- outputVolumeNode.SetAndObserveImageData(stencil.GetOutput())
-
- # Set the same geometry and parent transform as the input volume
- ijkToRas = vtk.vtkMatrix4x4()
- inputVolumeNode.GetIJKToRASMatrix(ijkToRas)
- outputVolumeNode.SetIJKToRASMatrix(ijkToRas)
- inputVolumeNode.SetAndObserveTransformNodeID(inputVolumeNode.GetTransformNodeID())
-
- slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode())
- slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode())
- slicer.mrmlScene.RemoveNode(maskVolumeNode)
- return True
+ self.outputVolumeSelector.selectNodeUponCreation = True
+ self.outputVolumeSelector.addEnabled = True
+ self.outputVolumeSelector.removeEnabled = True
+ self.outputVolumeSelector.renameEnabled = True
+ self.outputVolumeSelector.noneEnabled = True
+ self.outputVolumeSelector.noneDisplay = "(Create new Volume)"
+ self.outputVolumeSelector.showHidden = False
+ self.outputVolumeSelector.setMRMLScene(slicer.mrmlScene)
+ self.outputVolumeSelector.setToolTip("Masked output volume. It may be the same as the input volume for cumulative masking.")
+ self.outputVolumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onOutputVolumeChanged)
+
+ self.outputVisibilityButton = qt.QToolButton()
+ self.outputVisibilityButton.setIcon(self.invisibleIcon)
+ self.outputVisibilityButton.connect('clicked()', self.onOutputVisibilityButtonClicked)
+ outputLayout = qt.QHBoxLayout()
+ outputLayout.addWidget(self.outputVisibilityButton)
+ outputLayout.addWidget(self.outputVolumeSelector)
+ self.scriptedEffect.addLabeledOptionsWidget("Output Volume: ", outputLayout)
+
+ # Apply button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Apply segment as volume mask. No undo operation available once applied.")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+ self.applyButton.connect('clicked()', self.onApply)
+
+ for button in self.operationRadioButtons:
+ button.connect('toggled(bool)',
+ lambda toggle, widget=self.buttonToOperationNameMap[button]: self.onOperationSelectionChanged(widget, toggle))
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("FillValue", "0")
+ self.scriptedEffect.setParameterDefault("BinaryMaskFillValueInside", "1")
+ self.scriptedEffect.setParameterDefault("BinaryMaskFillValueOutside", "0")
+ self.scriptedEffect.setParameterDefault("Operation", "FILL_OUTSIDE")
+
+ def isVolumeVisible(self, volumeNode):
+ if not volumeNode:
+ return False
+ volumeNodeID = volumeNode.GetID()
+ lm = slicer.app.layoutManager()
+ sliceViewNames = lm.sliceViewNames()
+ for sliceViewName in sliceViewNames:
+ sliceWidget = lm.sliceWidget(sliceViewName)
+ if volumeNodeID == sliceWidget.mrmlSliceCompositeNode().GetBackgroundVolumeID():
+ return True
+ return False
+
+ def updateGUIFromMRML(self):
+ self.updatingGUIFromMRML = True
+
+ self.fillValueEdit.setValue(float(self.scriptedEffect.parameter("FillValue")) if self.scriptedEffect.parameter("FillValue") else 0)
+ self.binaryMaskFillOutsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueOutside"))
+ if self.scriptedEffect.parameter("BinaryMaskFillValueOutside") else 0)
+ self.binaryMaskFillInsideEdit.setValue(float(self.scriptedEffect.parameter("BinaryMaskFillValueInside"))
+ if self.scriptedEffect.parameter("BinaryMaskFillValueInside") else 1)
+ operationName = self.scriptedEffect.parameter("Operation")
+ if operationName:
+ operationButton = list(self.buttonToOperationNameMap.keys())[list(self.buttonToOperationNameMap.values()).index(operationName)]
+ operationButton.setChecked(True)
+
+ inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume")
+ self.inputVolumeSelector.setCurrentNode(inputVolume)
+ outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume")
+ self.outputVolumeSelector.setCurrentNode(outputVolume)
+
+ masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
+ if inputVolume is None:
+ inputVolume = masterVolume
+
+ self.fillValueEdit.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"])
+ self.fillValueLabel.setVisible(operationName in ["FILL_INSIDE", "FILL_OUTSIDE"])
+ self.binaryMaskFillInsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
+ self.fillInsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
+ self.binaryMaskFillOutsideEdit.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
+ self.fillOutsideLabel.setVisible(operationName == "FILL_INSIDE_AND_OUTSIDE")
+ if operationName in ["FILL_INSIDE", "FILL_OUTSIDE"]:
+ if self.outputVolumeSelector.noneDisplay != "(Create new Volume)":
+ self.outputVolumeSelector.noneDisplay = "(Create new Volume)"
+ self.outputVolumeSelector.nodeTypes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"]
+ else:
+ if self.outputVolumeSelector.noneDisplay != "(Create new Labelmap Volume)":
+ self.outputVolumeSelector.noneDisplay = "(Create new Labelmap Volume)"
+ self.outputVolumeSelector.nodeTypes = ["vtkMRMLLabelMapVolumeNode", "vtkMRMLScalarVolumeNode"]
+
+ self.inputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(inputVolume) else self.invisibleIcon)
+ self.outputVisibilityButton.setIcon(self.visibleIcon if self.isVolumeVisible(outputVolume) else self.invisibleIcon)
+
+ self.updatingGUIFromMRML = False
+
+ def updateMRMLFromGUI(self):
+ if self.updatingGUIFromMRML:
+ return
+ self.scriptedEffect.setParameter("FillValue", self.fillValueEdit.value)
+ self.scriptedEffect.setParameter("BinaryMaskFillValueInside", self.binaryMaskFillInsideEdit.value)
+ self.scriptedEffect.setParameter("BinaryMaskFillValueOutside", self.binaryMaskFillOutsideEdit.value)
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID)
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID)
+
+ def activate(self):
+ self.scriptedEffect.setParameter("InputVisibility", "True")
+
+ def deactivate(self):
+ if self.outputVolumeSelector.currentNode() is not self.scriptedEffect.parameterSetNode().GetMasterVolumeNode():
+ self.scriptedEffect.setParameter("OutputVisibility", "False")
+ slicer.util.setSliceViewerLayers(background=self.scriptedEffect.parameterSetNode().GetMasterVolumeNode())
+
+ def onOperationSelectionChanged(self, operationName, toggle):
+ if not toggle:
+ return
+ self.scriptedEffect.setParameter("Operation", operationName)
+
+ def getInputVolume(self):
+ inputVolume = self.inputVolumeSelector.currentNode()
+ if inputVolume is None:
+ inputVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
+ return inputVolume
+
+ def onInputVisibilityButtonClicked(self):
+ inputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.InputVolume")
+ masterVolume = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
+ if inputVolume is None:
+ inputVolume = masterVolume
+ if inputVolume:
+ slicer.util.setSliceViewerLayers(background=inputVolume)
+ self.updateGUIFromMRML()
+
+ def onOutputVisibilityButtonClicked(self):
+ outputVolume = self.scriptedEffect.parameterSetNode().GetNodeReference("Mask volume.OutputVolume")
+ if outputVolume:
+ slicer.util.setSliceViewerLayers(background=outputVolume)
+ self.updateGUIFromMRML()
+
+ def onInputVolumeChanged(self):
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.InputVolume", self.inputVolumeSelector.currentNodeID)
+ self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually
+
+ def onOutputVolumeChanged(self):
+ self.scriptedEffect.parameterSetNode().SetNodeReferenceID("Mask volume.OutputVolume", self.outputVolumeSelector.currentNodeID)
+ self.updateGUIFromMRML() # node reference changes are not observed, update GUI manually
+
+ def fillValueChanged(self):
+ self.updateMRMLFromGUI()
+
+ def onApply(self):
+ inputVolume = self.getInputVolume()
+ outputVolume = self.outputVolumeSelector.currentNode()
+ operationMode = self.scriptedEffect.parameter("Operation")
+ if not outputVolume:
+ # Create new node for output
+ volumesLogic = slicer.modules.volumes.logic()
+ scene = inputVolume.GetScene()
+ if operationMode == "FILL_INSIDE_AND_OUTSIDE":
+ outputVolumeName = inputVolume.GetName() + " label"
+ outputVolume = volumesLogic.CreateAndAddLabelVolume(inputVolume, outputVolumeName)
+ else:
+ outputVolumeName = inputVolume.GetName() + " masked"
+ outputVolume = volumesLogic.CloneVolumeGeneric(scene, inputVolume, outputVolumeName, False)
+ self.outputVolumeSelector.setCurrentNode(outputVolume)
+
+ if operationMode in ["FILL_INSIDE", "FILL_OUTSIDE"]:
+ fillValues = [self.fillValueEdit.value]
+ else:
+ fillValues = [self.binaryMaskFillInsideEdit.value, self.binaryMaskFillOutsideEdit.value]
+
+ segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+
+ slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
+ SegmentEditorMaskVolumeEffect.maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolume, outputVolume)
+
+ slicer.util.setSliceViewerLayers(background=outputVolume)
+ qt.QApplication.restoreOverrideCursor()
+
+ self.updateGUIFromMRML()
+
+ @staticmethod
+ def maskVolumeWithSegment(segmentationNode, segmentID, operationMode, fillValues, inputVolumeNode, outputVolumeNode, maskExtent=None):
+ """
+ Fill voxels of the input volume inside/outside the masking model with the provided fill value
+ maskExtent: optional output to return computed mask extent (expected input is a 6-element list)
+ fillValues: list containing one or two fill values. If fill mode is inside or outside then only one value is specified in the list.
+ If fill mode is inside&outside then the list must contain two values: first is the inside fill, second is the outside fill value.
+ """
+
+ segmentIDs = vtk.vtkStringArray()
+ segmentIDs.InsertNextValue(segmentID)
+ maskVolumeNode = slicer.modules.volumes.logic().CreateAndAddLabelVolume(inputVolumeNode, "TemporaryVolumeMask")
+ if not maskVolumeNode:
+ logging.error("maskVolumeWithSegment failed: invalid maskVolumeNode")
+ return False
+
+ if not slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, segmentIDs, maskVolumeNode, inputVolumeNode):
+ logging.error("maskVolumeWithSegment failed: ExportSegmentsToLabelmapNode error")
+ slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode())
+ slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode())
+ slicer.mrmlScene.RemoveNode(maskVolumeNode)
+ return False
+
+ if maskExtent:
+ img = slicer.modules.segmentations.logic().CreateOrientedImageDataFromVolumeNode(maskVolumeNode)
+ img.UnRegister(None)
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ vtkSegmentationCore.vtkOrientedImageDataResample.CalculateEffectiveExtent(img, maskExtent, 0)
+
+ maskToStencil = vtk.vtkImageToImageStencil()
+ maskToStencil.ThresholdByLower(0)
+ maskToStencil.SetInputData(maskVolumeNode.GetImageData())
+
+ stencil = vtk.vtkImageStencil()
+
+ if operationMode == "FILL_INSIDE_AND_OUTSIDE":
+ # Set input to constant value
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(inputVolumeNode.GetImageData())
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(fillValues[1])
+ thresh.SetOutValue(fillValues[1])
+ thresh.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType())
+ thresh.Update()
+ stencil.SetInputData(thresh.GetOutput())
+ else:
+ stencil.SetInputData(inputVolumeNode.GetImageData())
+
+ stencil.SetStencilConnection(maskToStencil.GetOutputPort())
+ stencil.SetReverseStencil(operationMode == "FILL_OUTSIDE")
+ stencil.SetBackgroundValue(fillValues[0])
+ stencil.Update()
+
+ outputVolumeNode.SetAndObserveImageData(stencil.GetOutput())
+
+ # Set the same geometry and parent transform as the input volume
+ ijkToRas = vtk.vtkMatrix4x4()
+ inputVolumeNode.GetIJKToRASMatrix(ijkToRas)
+ outputVolumeNode.SetIJKToRASMatrix(ijkToRas)
+ inputVolumeNode.SetAndObserveTransformNodeID(inputVolumeNode.GetTransformNodeID())
+
+ slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode().GetColorNode())
+ slicer.mrmlScene.RemoveNode(maskVolumeNode.GetDisplayNode())
+ slicer.mrmlScene.RemoveNode(maskVolumeNode)
+ return True
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py
index a5a3052b532..4b5f1f2ddfb 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorSmoothingEffect.py
@@ -11,27 +11,27 @@
class SegmentEditorSmoothingEffect(AbstractScriptedSegmentEditorPaintEffect):
- """ SmoothingEffect is an Effect that smoothes a selected segment
- """
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'Smoothing'
- AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Smoothing.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Make segment boundaries smoother
by removing extrusions and filling small holes. The effect can be either applied locally
+ """ SmoothingEffect is an Effect that smoothes a selected segment
+ """
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'Smoothing'
+ AbstractScriptedSegmentEditorPaintEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedPaintEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Smoothing.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Make segment boundaries smoother
by removing extrusions and filling small holes. The effect can be either applied locally
(by painting in viewers) or to the whole segment (by clicking Apply button). Available methods:
- Median: removes small details while keeps smooth contours mostly unchanged. Applied to selected segment only.
@@ -42,450 +42,450 @@ def helpText(self):
If segments overlap, segment higher in the segments table will have priority. Applied to all visible segments.
"""
- def setupOptionsFrame(self):
-
- self.methodSelectorComboBox = qt.QComboBox()
- self.methodSelectorComboBox.addItem("Median", MEDIAN)
- self.methodSelectorComboBox.addItem("Opening (remove extrusions)", MORPHOLOGICAL_OPENING)
- self.methodSelectorComboBox.addItem("Closing (fill holes)", MORPHOLOGICAL_CLOSING)
- self.methodSelectorComboBox.addItem("Gaussian", GAUSSIAN)
- self.methodSelectorComboBox.addItem("Joint smoothing", JOINT_TAUBIN)
- self.scriptedEffect.addLabeledOptionsWidget("Smoothing method:", self.methodSelectorComboBox)
-
- self.kernelSizeMMSpinBox = slicer.qMRMLSpinBox()
- self.kernelSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
- self.kernelSizeMMSpinBox.setToolTip("Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).")
- self.kernelSizeMMSpinBox.quantity = "length"
- self.kernelSizeMMSpinBox.minimum = 0.0
- self.kernelSizeMMSpinBox.value = 3.0
- self.kernelSizeMMSpinBox.singleStep = 1.0
-
- self.kernelSizePixel = qt.QLabel()
- self.kernelSizePixel.setToolTip("Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.")
-
- kernelSizeFrame = qt.QHBoxLayout()
- kernelSizeFrame.addWidget(self.kernelSizeMMSpinBox)
- kernelSizeFrame.addWidget(self.kernelSizePixel)
- self.kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Kernel size:", kernelSizeFrame)
-
- self.gaussianStandardDeviationMMSpinBox = slicer.qMRMLSpinBox()
- self.gaussianStandardDeviationMMSpinBox.setMRMLScene(slicer.mrmlScene)
- self.gaussianStandardDeviationMMSpinBox.setToolTip("Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).")
- self.gaussianStandardDeviationMMSpinBox.quantity = "length"
- self.gaussianStandardDeviationMMSpinBox.value = 3.0
- self.gaussianStandardDeviationMMSpinBox.singleStep = 1.0
- self.gaussianStandardDeviationMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Standard deviation:", self.gaussianStandardDeviationMMSpinBox)
-
- self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget()
- self.jointTaubinSmoothingFactorSlider.setToolTip("Higher value means stronger smoothing.")
- self.jointTaubinSmoothingFactorSlider.minimum = 0.01
- self.jointTaubinSmoothingFactorSlider.maximum = 1.0
- self.jointTaubinSmoothingFactorSlider.value = 0.5
- self.jointTaubinSmoothingFactorSlider.singleStep = 0.01
- self.jointTaubinSmoothingFactorSlider.pageStep = 0.1
- self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget("Smoothing factor:", self.jointTaubinSmoothingFactorSlider)
-
- self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
- self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply smoothing effect to all visible segments in this segmentation node. \
+ def setupOptionsFrame(self):
+
+ self.methodSelectorComboBox = qt.QComboBox()
+ self.methodSelectorComboBox.addItem("Median", MEDIAN)
+ self.methodSelectorComboBox.addItem("Opening (remove extrusions)", MORPHOLOGICAL_OPENING)
+ self.methodSelectorComboBox.addItem("Closing (fill holes)", MORPHOLOGICAL_CLOSING)
+ self.methodSelectorComboBox.addItem("Gaussian", GAUSSIAN)
+ self.methodSelectorComboBox.addItem("Joint smoothing", JOINT_TAUBIN)
+ self.scriptedEffect.addLabeledOptionsWidget("Smoothing method:", self.methodSelectorComboBox)
+
+ self.kernelSizeMMSpinBox = slicer.qMRMLSpinBox()
+ self.kernelSizeMMSpinBox.setMRMLScene(slicer.mrmlScene)
+ self.kernelSizeMMSpinBox.setToolTip("Diameter of the neighborhood that will be considered around each voxel. Higher value makes smoothing stronger (more details are suppressed).")
+ self.kernelSizeMMSpinBox.quantity = "length"
+ self.kernelSizeMMSpinBox.minimum = 0.0
+ self.kernelSizeMMSpinBox.value = 3.0
+ self.kernelSizeMMSpinBox.singleStep = 1.0
+
+ self.kernelSizePixel = qt.QLabel()
+ self.kernelSizePixel.setToolTip("Diameter of the neighborhood in pixel. Computed from the segment's spacing and the specified kernel size.")
+
+ kernelSizeFrame = qt.QHBoxLayout()
+ kernelSizeFrame.addWidget(self.kernelSizeMMSpinBox)
+ kernelSizeFrame.addWidget(self.kernelSizePixel)
+ self.kernelSizeMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Kernel size:", kernelSizeFrame)
+
+ self.gaussianStandardDeviationMMSpinBox = slicer.qMRMLSpinBox()
+ self.gaussianStandardDeviationMMSpinBox.setMRMLScene(slicer.mrmlScene)
+ self.gaussianStandardDeviationMMSpinBox.setToolTip("Standard deviation of the Gaussian smoothing filter coefficients. Higher value makes smoothing stronger (more details are suppressed).")
+ self.gaussianStandardDeviationMMSpinBox.quantity = "length"
+ self.gaussianStandardDeviationMMSpinBox.value = 3.0
+ self.gaussianStandardDeviationMMSpinBox.singleStep = 1.0
+ self.gaussianStandardDeviationMMLabel = self.scriptedEffect.addLabeledOptionsWidget("Standard deviation:", self.gaussianStandardDeviationMMSpinBox)
+
+ self.jointTaubinSmoothingFactorSlider = ctk.ctkSliderWidget()
+ self.jointTaubinSmoothingFactorSlider.setToolTip("Higher value means stronger smoothing.")
+ self.jointTaubinSmoothingFactorSlider.minimum = 0.01
+ self.jointTaubinSmoothingFactorSlider.maximum = 1.0
+ self.jointTaubinSmoothingFactorSlider.value = 0.5
+ self.jointTaubinSmoothingFactorSlider.singleStep = 0.01
+ self.jointTaubinSmoothingFactorSlider.pageStep = 0.1
+ self.jointTaubinSmoothingFactorLabel = self.scriptedEffect.addLabeledOptionsWidget("Smoothing factor:", self.jointTaubinSmoothingFactorSlider)
+
+ self.applyToAllVisibleSegmentsCheckBox = qt.QCheckBox()
+ self.applyToAllVisibleSegmentsCheckBox.setToolTip("Apply smoothing effect to all visible segments in this segmentation node. \
This operation may take a while.")
- self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
- self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Apply smoothing to selected segment")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
-
- self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
- self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
- self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
- self.applyButton.connect('clicked()', self.onApply)
-
- # Customize smoothing brush
- self.scriptedEffect.setColorSmudgeCheckboxVisible(False)
- self.paintOptionsGroupBox = ctk.ctkCollapsibleGroupBox()
- self.paintOptionsGroupBox.setTitle("Smoothing brush options")
- self.paintOptionsGroupBox.setLayout(qt.QVBoxLayout())
- self.paintOptionsGroupBox.layout().addWidget(self.scriptedEffect.paintOptionsFrame())
- self.paintOptionsGroupBox.collapsed = True
- self.scriptedEffect.addOptionsWidget(self.paintOptionsGroupBox)
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
- self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3)
- self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5)
- self.scriptedEffect.setParameterDefault("KernelSizeMm", 3)
- self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN)
-
- def updateParameterWidgetsVisibility(self):
- methodIndex = self.methodSelectorComboBox.currentIndex
- smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
- morphologicalMethod = (smoothingMethod == MEDIAN or smoothingMethod == MORPHOLOGICAL_OPENING or smoothingMethod == MORPHOLOGICAL_CLOSING)
- self.kernelSizeMMLabel.setVisible(morphologicalMethod)
- self.kernelSizeMMSpinBox.setVisible(morphologicalMethod)
- self.kernelSizePixel.setVisible(morphologicalMethod)
- self.gaussianStandardDeviationMMLabel.setVisible(smoothingMethod == GAUSSIAN)
- self.gaussianStandardDeviationMMSpinBox.setVisible(smoothingMethod == GAUSSIAN)
- self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod == JOINT_TAUBIN)
- self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod == JOINT_TAUBIN)
- self.applyToAllVisibleSegmentsLabel.setVisible(smoothingMethod != JOINT_TAUBIN)
- self.applyToAllVisibleSegmentsCheckBox.setVisible(smoothingMethod != JOINT_TAUBIN)
-
- def getKernelSizePixel(self):
- selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
- if selectedSegmentLabelmap:
- selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
-
- # size rounded to nearest odd number. If kernel size is even then image gets shifted.
- kernelSizeMM = self.scriptedEffect.doubleParameter("KernelSizeMm")
- kernelSizePixel = [int(round((kernelSizeMM / selectedSegmentLabelmapSpacing[componentIndex] + 1) / 2) * 2 - 1) for componentIndex in range(3)]
- return kernelSizePixel
-
- def updateGUIFromMRML(self):
- methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod"))
- wasBlocked = self.methodSelectorComboBox.blockSignals(True)
- self.methodSelectorComboBox.setCurrentIndex(methodIndex)
- self.methodSelectorComboBox.blockSignals(wasBlocked)
-
- wasBlocked = self.kernelSizeMMSpinBox.blockSignals(True)
- self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
- self.kernelSizeMMSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm")
- self.kernelSizeMMSpinBox.blockSignals(wasBlocked)
- kernelSizePixel = self.getKernelSizePixel()
- self.kernelSizePixel.text = f"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel"
-
- wasBlocked = self.gaussianStandardDeviationMMSpinBox.blockSignals(True)
- self.setWidgetMinMaxStepFromImageSpacing(self.gaussianStandardDeviationMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
- self.gaussianStandardDeviationMMSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
- self.gaussianStandardDeviationMMSpinBox.blockSignals(wasBlocked)
-
- wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True)
- self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
- self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked)
-
- applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
- wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
- self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
- self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
-
- self.updateParameterWidgetsVisibility()
-
- def updateMRMLFromGUI(self):
- methodIndex = self.methodSelectorComboBox.currentIndex
- smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
- self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod)
- self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMMSpinBox.value)
- self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMMSpinBox.value)
- self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value)
- applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
- self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
-
- self.updateParameterWidgetsVisibility()
-
- #
- # Effect specific methods (the above ones are the API methods to override)
- #
-
- def showStatusMessage(self, msg, timeoutMsec=500):
- slicer.util.showStatusMessage(msg, timeoutMsec)
- slicer.app.processEvents()
+ self.applyToAllVisibleSegmentsCheckBox.objectName = self.__class__.__name__ + 'ApplyToAllVisibleSegments'
+ self.applyToAllVisibleSegmentsLabel = self.scriptedEffect.addLabeledOptionsWidget("Apply to all segments:", self.applyToAllVisibleSegmentsCheckBox)
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Apply smoothing to selected segment")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+
+ self.methodSelectorComboBox.connect("currentIndexChanged(int)", self.updateMRMLFromGUI)
+ self.kernelSizeMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.gaussianStandardDeviationMMSpinBox.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.jointTaubinSmoothingFactorSlider.connect("valueChanged(double)", self.updateMRMLFromGUI)
+ self.applyToAllVisibleSegmentsCheckBox.connect("stateChanged(int)", self.updateMRMLFromGUI)
+ self.applyButton.connect('clicked()', self.onApply)
+
+ # Customize smoothing brush
+ self.scriptedEffect.setColorSmudgeCheckboxVisible(False)
+ self.paintOptionsGroupBox = ctk.ctkCollapsibleGroupBox()
+ self.paintOptionsGroupBox.setTitle("Smoothing brush options")
+ self.paintOptionsGroupBox.setLayout(qt.QVBoxLayout())
+ self.paintOptionsGroupBox.layout().addWidget(self.scriptedEffect.paintOptionsFrame())
+ self.paintOptionsGroupBox.collapsed = True
+ self.scriptedEffect.addOptionsWidget(self.paintOptionsGroupBox)
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("ApplyToAllVisibleSegments", 0)
+ self.scriptedEffect.setParameterDefault("GaussianStandardDeviationMm", 3)
+ self.scriptedEffect.setParameterDefault("JointTaubinSmoothingFactor", 0.5)
+ self.scriptedEffect.setParameterDefault("KernelSizeMm", 3)
+ self.scriptedEffect.setParameterDefault("SmoothingMethod", MEDIAN)
+
+ def updateParameterWidgetsVisibility(self):
+ methodIndex = self.methodSelectorComboBox.currentIndex
+ smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
+ morphologicalMethod = (smoothingMethod == MEDIAN or smoothingMethod == MORPHOLOGICAL_OPENING or smoothingMethod == MORPHOLOGICAL_CLOSING)
+ self.kernelSizeMMLabel.setVisible(morphologicalMethod)
+ self.kernelSizeMMSpinBox.setVisible(morphologicalMethod)
+ self.kernelSizePixel.setVisible(morphologicalMethod)
+ self.gaussianStandardDeviationMMLabel.setVisible(smoothingMethod == GAUSSIAN)
+ self.gaussianStandardDeviationMMSpinBox.setVisible(smoothingMethod == GAUSSIAN)
+ self.jointTaubinSmoothingFactorLabel.setVisible(smoothingMethod == JOINT_TAUBIN)
+ self.jointTaubinSmoothingFactorSlider.setVisible(smoothingMethod == JOINT_TAUBIN)
+ self.applyToAllVisibleSegmentsLabel.setVisible(smoothingMethod != JOINT_TAUBIN)
+ self.applyToAllVisibleSegmentsCheckBox.setVisible(smoothingMethod != JOINT_TAUBIN)
+
+ def getKernelSizePixel(self):
+ selectedSegmentLabelmapSpacing = [1.0, 1.0, 1.0]
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+ if selectedSegmentLabelmap:
+ selectedSegmentLabelmapSpacing = selectedSegmentLabelmap.GetSpacing()
- def onApply(self, maskImage=None, maskExtent=None):
- """maskImage: contains nonzero where smoothing will be applied
- """
- smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
- applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
- if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
-
- if smoothingMethod != JOINT_TAUBIN:
- # Make sure the user wants to do the operation, even if the segment is not visible
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
-
- try:
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- self.scriptedEffect.saveStateForUndo()
-
- if smoothingMethod == JOINT_TAUBIN:
- self.smoothMultipleSegments(maskImage, maskExtent)
- elif applyToAllVisibleSegments:
- # Smooth all visible segments
- inputSegmentIDs = vtk.vtkStringArray()
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
- segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
- segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
- # store which segment was selected before operation
- selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
- if inputSegmentIDs.GetNumberOfValues() == 0:
- logging.info("Smoothing operation skipped: there are no visible segments.")
- return
- for index in range(inputSegmentIDs.GetNumberOfValues()):
- segmentID = inputSegmentIDs.GetValue(index)
- self.showStatusMessage(f'Smoothing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
- segmentEditorNode.SetSelectedSegmentID(segmentID)
- self.smoothSelectedSegment(maskImage, maskExtent)
- # restore segment selection
- segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
- else:
- self.smoothSelectedSegment(maskImage, maskExtent)
- finally:
- qt.QApplication.restoreOverrideCursor()
-
- def clipImage(self, inputImage, maskExtent, margin):
- clipper = vtk.vtkImageClip()
- clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0],
- maskExtent[2] - margin[1], maskExtent[3] + margin[1],
- maskExtent[4] - margin[2], maskExtent[5] + margin[2])
- clipper.SetInputData(inputImage)
- clipper.SetClipData(True)
- clipper.Update()
- clippedImage = slicer.vtkOrientedImageData()
- clippedImage.ShallowCopy(clipper.GetOutput())
- clippedImage.CopyDirections(inputImage)
- return clippedImage
-
- def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent):
- if maskImage:
- smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
- smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
- smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
-
- # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM
- fillValue = 1.0
- slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False)
- # set original segment labelmap outside painted region, solid 1 inside painted region
- slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
- slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
- slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
- slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
-
- updateExtent = [0, -1, 0, -1, 0, -1]
- modifierExtent = modifierLabelmap.GetExtent()
- for i in range(3):
- updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i])
- updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1])
-
- self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage,
- slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
- updateExtent)
- else:
- modifierLabelmap.DeepCopy(smoothedImage)
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
-
- def smoothSelectedSegment(self, maskImage=None, maskExtent=None):
- try:
- # Get modifier labelmap
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
-
- smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
-
- if smoothingMethod == GAUSSIAN:
- maxValue = 255
- radiusFactor = 4.0
- standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
- spacing = modifierLabelmap.GetSpacing()
- standardDeviationPixel = [1.0, 1.0, 1.0]
- radiusPixel = [3, 3, 3]
- for idx in range(3):
- standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
- radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
- if maskExtent:
- clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
- else:
- clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
-
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(clippedSelectedSegmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(0)
- thresh.SetOutValue(maxValue)
- thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
-
- gaussianFilter = vtk.vtkImageGaussianSmooth()
- gaussianFilter.SetInputConnection(thresh.GetOutputPort())
- gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
- gaussianFilter.SetRadiusFactor(radiusFactor)
-
- thresh2 = vtk.vtkImageThreshold()
- thresh2.SetInputConnection(gaussianFilter.GetOutputPort())
- thresh2.ThresholdByUpper(int(maxValue / 2))
- thresh2.SetInValue(1)
- thresh2.SetOutValue(0)
- thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
- thresh2.Update()
-
- self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
-
- else:
# size rounded to nearest odd number. If kernel size is even then image gets shifted.
+ kernelSizeMM = self.scriptedEffect.doubleParameter("KernelSizeMm")
+ kernelSizePixel = [int(round((kernelSizeMM / selectedSegmentLabelmapSpacing[componentIndex] + 1) / 2) * 2 - 1) for componentIndex in range(3)]
+ return kernelSizePixel
+
+ def updateGUIFromMRML(self):
+ methodIndex = self.methodSelectorComboBox.findData(self.scriptedEffect.parameter("SmoothingMethod"))
+ wasBlocked = self.methodSelectorComboBox.blockSignals(True)
+ self.methodSelectorComboBox.setCurrentIndex(methodIndex)
+ self.methodSelectorComboBox.blockSignals(wasBlocked)
+
+ wasBlocked = self.kernelSizeMMSpinBox.blockSignals(True)
+ self.setWidgetMinMaxStepFromImageSpacing(self.kernelSizeMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
+ self.kernelSizeMMSpinBox.value = self.scriptedEffect.doubleParameter("KernelSizeMm")
+ self.kernelSizeMMSpinBox.blockSignals(wasBlocked)
kernelSizePixel = self.getKernelSizePixel()
+ self.kernelSizePixel.text = f"{kernelSizePixel[0]}x{kernelSizePixel[1]}x{kernelSizePixel[2]} pixel"
- if maskExtent:
- clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
- else:
- clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
+ wasBlocked = self.gaussianStandardDeviationMMSpinBox.blockSignals(True)
+ self.setWidgetMinMaxStepFromImageSpacing(self.gaussianStandardDeviationMMSpinBox, self.scriptedEffect.selectedSegmentLabelmap())
+ self.gaussianStandardDeviationMMSpinBox.value = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
+ self.gaussianStandardDeviationMMSpinBox.blockSignals(wasBlocked)
+
+ wasBlocked = self.jointTaubinSmoothingFactorSlider.blockSignals(True)
+ self.jointTaubinSmoothingFactorSlider.value = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
+ self.jointTaubinSmoothingFactorSlider.blockSignals(wasBlocked)
+
+ applyToAllVisibleSegments = qt.Qt.Unchecked if self.scriptedEffect.integerParameter("ApplyToAllVisibleSegments") == 0 else qt.Qt.Checked
+ wasBlocked = self.applyToAllVisibleSegmentsCheckBox.blockSignals(True)
+ self.applyToAllVisibleSegmentsCheckBox.setCheckState(applyToAllVisibleSegments)
+ self.applyToAllVisibleSegmentsCheckBox.blockSignals(wasBlocked)
+
+ self.updateParameterWidgetsVisibility()
+
+ def updateMRMLFromGUI(self):
+ methodIndex = self.methodSelectorComboBox.currentIndex
+ smoothingMethod = self.methodSelectorComboBox.itemData(methodIndex)
+ self.scriptedEffect.setParameter("SmoothingMethod", smoothingMethod)
+ self.scriptedEffect.setParameter("KernelSizeMm", self.kernelSizeMMSpinBox.value)
+ self.scriptedEffect.setParameter("GaussianStandardDeviationMm", self.gaussianStandardDeviationMMSpinBox.value)
+ self.scriptedEffect.setParameter("JointTaubinSmoothingFactor", self.jointTaubinSmoothingFactorSlider.value)
+ applyToAllVisibleSegments = 1 if self.applyToAllVisibleSegmentsCheckBox.isChecked() else 0
+ self.scriptedEffect.setParameter("ApplyToAllVisibleSegments", applyToAllVisibleSegments)
- if smoothingMethod == MEDIAN:
- # Median filter does not require a particular label value
- smoothingFilter = vtk.vtkImageMedian3D()
- smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
+ self.updateParameterWidgetsVisibility()
+ #
+ # Effect specific methods (the above ones are the API methods to override)
+ #
+
+ def showStatusMessage(self, msg, timeoutMsec=500):
+ slicer.util.showStatusMessage(msg, timeoutMsec)
+ slicer.app.processEvents()
+
+ def onApply(self, maskImage=None, maskExtent=None):
+ """maskImage: contains nonzero where smoothing will be applied
+ """
+ smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
+ applyToAllVisibleSegments = int(self.scriptedEffect.parameter("ApplyToAllVisibleSegments")) != 0 \
+ if self.scriptedEffect.parameter("ApplyToAllVisibleSegments") else False
+
+ if smoothingMethod != JOINT_TAUBIN:
+ # Make sure the user wants to do the operation, even if the segment is not visible
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+
+ try:
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ self.scriptedEffect.saveStateForUndo()
+
+ if smoothingMethod == JOINT_TAUBIN:
+ self.smoothMultipleSegments(maskImage, maskExtent)
+ elif applyToAllVisibleSegments:
+ # Smooth all visible segments
+ inputSegmentIDs = vtk.vtkStringArray()
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs)
+ segmentEditorWidget = slicer.modules.segmenteditor.widgetRepresentation().self().editor
+ segmentEditorNode = segmentEditorWidget.mrmlSegmentEditorNode()
+ # store which segment was selected before operation
+ selectedStartSegmentID = segmentEditorNode.GetSelectedSegmentID()
+ if inputSegmentIDs.GetNumberOfValues() == 0:
+ logging.info("Smoothing operation skipped: there are no visible segments.")
+ return
+ for index in range(inputSegmentIDs.GetNumberOfValues()):
+ segmentID = inputSegmentIDs.GetValue(index)
+ self.showStatusMessage(f'Smoothing {segmentationNode.GetSegmentation().GetSegment(segmentID).GetName()}...')
+ segmentEditorNode.SetSelectedSegmentID(segmentID)
+ self.smoothSelectedSegment(maskImage, maskExtent)
+ # restore segment selection
+ segmentEditorNode.SetSelectedSegmentID(selectedStartSegmentID)
+ else:
+ self.smoothSelectedSegment(maskImage, maskExtent)
+ finally:
+ qt.QApplication.restoreOverrideCursor()
+
+ def clipImage(self, inputImage, maskExtent, margin):
+ clipper = vtk.vtkImageClip()
+ clipper.SetOutputWholeExtent(maskExtent[0] - margin[0], maskExtent[1] + margin[0],
+ maskExtent[2] - margin[1], maskExtent[3] + margin[1],
+ maskExtent[4] - margin[2], maskExtent[5] + margin[2])
+ clipper.SetInputData(inputImage)
+ clipper.SetClipData(True)
+ clipper.Update()
+ clippedImage = slicer.vtkOrientedImageData()
+ clippedImage.ShallowCopy(clipper.GetOutput())
+ clippedImage.CopyDirections(inputImage)
+ return clippedImage
+
+ def modifySelectedSegmentByLabelmap(self, smoothedImage, selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent):
+ if maskImage:
+ smoothedClippedSelectedSegmentLabelmap = slicer.vtkOrientedImageData()
+ smoothedClippedSelectedSegmentLabelmap.ShallowCopy(smoothedImage)
+ smoothedClippedSelectedSegmentLabelmap.CopyDirections(modifierLabelmap)
+
+ # fill smoothed selected segment outside the painted region to 1 so that in the end the image is not modified by OPERATION_MINIMUM
+ fillValue = 1.0
+ slicer.vtkOrientedImageDataResample.ApplyImageMask(smoothedClippedSelectedSegmentLabelmap, maskImage, fillValue, False)
+ # set original segment labelmap outside painted region, solid 1 inside painted region
+ slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, selectedSegmentLabelmap,
+ slicer.vtkOrientedImageDataResample.OPERATION_MAXIMUM)
+ slicer.vtkOrientedImageDataResample.ModifyImage(maskImage, smoothedClippedSelectedSegmentLabelmap,
+ slicer.vtkOrientedImageDataResample.OPERATION_MINIMUM)
+
+ updateExtent = [0, -1, 0, -1, 0, -1]
+ modifierExtent = modifierLabelmap.GetExtent()
+ for i in range(3):
+ updateExtent[2 * i] = min(maskExtent[2 * i], modifierExtent[2 * i])
+ updateExtent[2 * i + 1] = max(maskExtent[2 * i + 1], modifierExtent[2 * i + 1])
+
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(maskImage,
+ slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet,
+ updateExtent)
else:
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(clippedSelectedSegmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType())
-
- smoothingFilter = vtk.vtkImageOpenClose3D()
- smoothingFilter.SetInputConnection(thresh.GetOutputPort())
- if smoothingMethod == MORPHOLOGICAL_OPENING:
- smoothingFilter.SetOpenValue(labelValue)
- smoothingFilter.SetCloseValue(backgroundValue)
- else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING:
- smoothingFilter.SetOpenValue(backgroundValue)
- smoothingFilter.SetCloseValue(labelValue)
-
- smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
- smoothingFilter.Update()
-
- self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
-
- except IndexError:
- logging.error('apply: Failed to apply smoothing')
-
- def smoothMultipleSegments(self, maskImage=None, maskExtent=None):
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- self.showStatusMessage(f'Joint smoothing ...')
- # Generate merged labelmap of all visible segments
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- visibleSegmentIds = vtk.vtkStringArray()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
- if visibleSegmentIds.GetNumberOfValues() == 0:
- logging.info("Smoothing operation skipped: there are no visible segments")
- return
-
- mergedImage = slicer.vtkOrientedImageData()
- if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
- vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
- None, visibleSegmentIds):
- logging.error('Failed to apply smoothing: cannot get list of visible segments')
- return
-
- segmentLabelValues = [] # list of [segmentId, labelValue]
- for i in range(visibleSegmentIds.GetNumberOfValues()):
- segmentId = visibleSegmentIds.GetValue(i)
- segmentLabelValues.append([segmentId, i + 1])
-
- # Perform smoothing in voxel space
- ici = vtk.vtkImageChangeInformation()
- ici.SetInputData(mergedImage)
- ici.SetOutputSpacing(1, 1, 1)
- ici.SetOutputOrigin(0, 0, 0)
-
- # Convert labelmap to combined polydata
- # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter,
- # each labeled region is completely disconnected from neighboring regions, and
- # for joint smoothing it is essential for the points to move together.
- convertToPolyData = vtk.vtkDiscreteMarchingCubes()
- convertToPolyData.SetInputConnection(ici.GetOutputPort())
- convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
-
- contourIndex = 0
- for segmentId, labelValue in segmentLabelValues:
- convertToPolyData.SetValue(contourIndex, labelValue)
- contourIndex += 1
-
- # Low-pass filtering using Taubin's method
- smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
- smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking
- passBand = pow(10.0, -4.0 * smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1
- smoother = vtk.vtkWindowedSincPolyDataFilter()
- smoother.SetInputConnection(convertToPolyData.GetOutputPort())
- smoother.SetNumberOfIterations(smoothingIterations)
- smoother.BoundarySmoothingOff()
- smoother.FeatureEdgeSmoothingOff()
- smoother.SetFeatureAngle(90.0)
- smoother.SetPassBand(passBand)
- smoother.NonManifoldSmoothingOn()
- smoother.NormalizeCoordinatesOn()
-
- # Extract a label
- threshold = vtk.vtkThreshold()
- threshold.SetInputConnection(smoother.GetOutputPort())
-
- # Convert to polydata
- geometryFilter = vtk.vtkGeometryFilter()
- geometryFilter.SetInputConnection(threshold.GetOutputPort())
-
- # Convert polydata to stencil
- polyDataToImageStencil = vtk.vtkPolyDataToImageStencil()
- polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort())
- polyDataToImageStencil.SetOutputSpacing(1, 1, 1)
- polyDataToImageStencil.SetOutputOrigin(0, 0, 0)
- polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent())
-
- # Convert stencil to image
- stencil = vtk.vtkImageStencil()
- emptyBinaryLabelMap = vtk.vtkImageData()
- emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent())
- emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0)
- stencil.SetInputData(emptyBinaryLabelMap)
- stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort())
- stencil.ReverseStencilOn()
- stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil)
-
- imageToWorldMatrix = vtk.vtkMatrix4x4()
- mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
-
- # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional
- # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be separated/merged automatically.
- # This effect could leverage those options once they have been implemented.
- oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode()
- self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments)
- for segmentId, labelValue in segmentLabelValues:
- threshold.ThresholdBetween(labelValue, labelValue)
- stencil.Update()
- smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
- smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
- smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
- self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
- slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
- self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
-
- def paintApply(self, viewWidget):
-
- # Current limitation: smoothing brush is not implemented for joint smoothing
- smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
- if smoothingMethod == JOINT_TAUBIN:
- self.scriptedEffect.clearBrushes()
- self.scriptedEffect.forceRender(viewWidget)
- slicer.util.messageBox("Smoothing brush is not available for 'joint smoothing' method.")
- return
-
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- maskImage = slicer.vtkOrientedImageData()
- maskImage.DeepCopy(modifierLabelmap)
- maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget)
- self.scriptedEffect.clearBrushes()
- self.scriptedEffect.forceRender(viewWidget)
- if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]:
- return
-
- self.scriptedEffect.saveStateForUndo()
- self.onApply(maskImage, maskExtent)
+ modifierLabelmap.DeepCopy(smoothedImage)
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+
+ def smoothSelectedSegment(self, maskImage=None, maskExtent=None):
+ try:
+ # Get modifier labelmap
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ selectedSegmentLabelmap = self.scriptedEffect.selectedSegmentLabelmap()
+
+ smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
+
+ if smoothingMethod == GAUSSIAN:
+ maxValue = 255
+ radiusFactor = 4.0
+ standardDeviationMM = self.scriptedEffect.doubleParameter("GaussianStandardDeviationMm")
+ spacing = modifierLabelmap.GetSpacing()
+ standardDeviationPixel = [1.0, 1.0, 1.0]
+ radiusPixel = [3, 3, 3]
+ for idx in range(3):
+ standardDeviationPixel[idx] = standardDeviationMM / spacing[idx]
+ radiusPixel[idx] = int(standardDeviationPixel[idx] * radiusFactor) + 1
+ if maskExtent:
+ clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, radiusPixel)
+ else:
+ clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
+
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(clippedSelectedSegmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(0)
+ thresh.SetOutValue(maxValue)
+ thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+
+ gaussianFilter = vtk.vtkImageGaussianSmooth()
+ gaussianFilter.SetInputConnection(thresh.GetOutputPort())
+ gaussianFilter.SetStandardDeviation(*standardDeviationPixel)
+ gaussianFilter.SetRadiusFactor(radiusFactor)
+
+ thresh2 = vtk.vtkImageThreshold()
+ thresh2.SetInputConnection(gaussianFilter.GetOutputPort())
+ thresh2.ThresholdByUpper(int(maxValue / 2))
+ thresh2.SetInValue(1)
+ thresh2.SetOutValue(0)
+ thresh2.SetOutputScalarType(selectedSegmentLabelmap.GetScalarType())
+ thresh2.Update()
+
+ self.modifySelectedSegmentByLabelmap(thresh2.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
+
+ else:
+ # size rounded to nearest odd number. If kernel size is even then image gets shifted.
+ kernelSizePixel = self.getKernelSizePixel()
+
+ if maskExtent:
+ clippedSelectedSegmentLabelmap = self.clipImage(selectedSegmentLabelmap, maskExtent, kernelSizePixel)
+ else:
+ clippedSelectedSegmentLabelmap = selectedSegmentLabelmap
+
+ if smoothingMethod == MEDIAN:
+ # Median filter does not require a particular label value
+ smoothingFilter = vtk.vtkImageMedian3D()
+ smoothingFilter.SetInputData(clippedSelectedSegmentLabelmap)
+
+ else:
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(clippedSelectedSegmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(clippedSelectedSegmentLabelmap.GetScalarType())
+
+ smoothingFilter = vtk.vtkImageOpenClose3D()
+ smoothingFilter.SetInputConnection(thresh.GetOutputPort())
+ if smoothingMethod == MORPHOLOGICAL_OPENING:
+ smoothingFilter.SetOpenValue(labelValue)
+ smoothingFilter.SetCloseValue(backgroundValue)
+ else: # must be smoothingMethod == MORPHOLOGICAL_CLOSING:
+ smoothingFilter.SetOpenValue(backgroundValue)
+ smoothingFilter.SetCloseValue(labelValue)
+
+ smoothingFilter.SetKernelSize(kernelSizePixel[0], kernelSizePixel[1], kernelSizePixel[2])
+ smoothingFilter.Update()
+
+ self.modifySelectedSegmentByLabelmap(smoothingFilter.GetOutput(), selectedSegmentLabelmap, modifierLabelmap, maskImage, maskExtent)
+
+ except IndexError:
+ logging.error('apply: Failed to apply smoothing')
+
+ def smoothMultipleSegments(self, maskImage=None, maskExtent=None):
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ self.showStatusMessage(f'Joint smoothing ...')
+ # Generate merged labelmap of all visible segments
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ visibleSegmentIds = vtk.vtkStringArray()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
+ if visibleSegmentIds.GetNumberOfValues() == 0:
+ logging.info("Smoothing operation skipped: there are no visible segments")
+ return
+
+ mergedImage = slicer.vtkOrientedImageData()
+ if not segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
+ vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_SEGMENTS_PADDED,
+ None, visibleSegmentIds):
+ logging.error('Failed to apply smoothing: cannot get list of visible segments')
+ return
+
+ segmentLabelValues = [] # list of [segmentId, labelValue]
+ for i in range(visibleSegmentIds.GetNumberOfValues()):
+ segmentId = visibleSegmentIds.GetValue(i)
+ segmentLabelValues.append([segmentId, i + 1])
+
+ # Perform smoothing in voxel space
+ ici = vtk.vtkImageChangeInformation()
+ ici.SetInputData(mergedImage)
+ ici.SetOutputSpacing(1, 1, 1)
+ ici.SetOutputOrigin(0, 0, 0)
+
+ # Convert labelmap to combined polydata
+ # vtkDiscreteFlyingEdges3D cannot be used here, as in the output of that filter,
+ # each labeled region is completely disconnected from neighboring regions, and
+ # for joint smoothing it is essential for the points to move together.
+ convertToPolyData = vtk.vtkDiscreteMarchingCubes()
+ convertToPolyData.SetInputConnection(ici.GetOutputPort())
+ convertToPolyData.SetNumberOfContours(len(segmentLabelValues))
+
+ contourIndex = 0
+ for segmentId, labelValue in segmentLabelValues:
+ convertToPolyData.SetValue(contourIndex, labelValue)
+ contourIndex += 1
+
+ # Low-pass filtering using Taubin's method
+ smoothingFactor = self.scriptedEffect.doubleParameter("JointTaubinSmoothingFactor")
+ smoothingIterations = 100 # according to VTK documentation 10-20 iterations could be enough but we use a higher value to reduce chance of shrinking
+ passBand = pow(10.0, -4.0 * smoothingFactor) # gives a nice range of 1-0.0001 from a user input of 0-1
+ smoother = vtk.vtkWindowedSincPolyDataFilter()
+ smoother.SetInputConnection(convertToPolyData.GetOutputPort())
+ smoother.SetNumberOfIterations(smoothingIterations)
+ smoother.BoundarySmoothingOff()
+ smoother.FeatureEdgeSmoothingOff()
+ smoother.SetFeatureAngle(90.0)
+ smoother.SetPassBand(passBand)
+ smoother.NonManifoldSmoothingOn()
+ smoother.NormalizeCoordinatesOn()
+
+ # Extract a label
+ threshold = vtk.vtkThreshold()
+ threshold.SetInputConnection(smoother.GetOutputPort())
+
+ # Convert to polydata
+ geometryFilter = vtk.vtkGeometryFilter()
+ geometryFilter.SetInputConnection(threshold.GetOutputPort())
+
+ # Convert polydata to stencil
+ polyDataToImageStencil = vtk.vtkPolyDataToImageStencil()
+ polyDataToImageStencil.SetInputConnection(geometryFilter.GetOutputPort())
+ polyDataToImageStencil.SetOutputSpacing(1, 1, 1)
+ polyDataToImageStencil.SetOutputOrigin(0, 0, 0)
+ polyDataToImageStencil.SetOutputWholeExtent(mergedImage.GetExtent())
+
+ # Convert stencil to image
+ stencil = vtk.vtkImageStencil()
+ emptyBinaryLabelMap = vtk.vtkImageData()
+ emptyBinaryLabelMap.SetExtent(mergedImage.GetExtent())
+ emptyBinaryLabelMap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ vtkSegmentationCore.vtkOrientedImageDataResample.FillImage(emptyBinaryLabelMap, 0)
+ stencil.SetInputData(emptyBinaryLabelMap)
+ stencil.SetStencilConnection(polyDataToImageStencil.GetOutputPort())
+ stencil.ReverseStencilOn()
+ stencil.SetBackgroundValue(1) # General foreground value is 1 (background value because of reverse stencil)
+
+ imageToWorldMatrix = vtk.vtkMatrix4x4()
+ mergedImage.GetImageToWorldMatrix(imageToWorldMatrix)
+
+ # TODO: Temporarily setting the overwrite mode to OverwriteVisibleSegments is an approach that should be change once additional
+ # layer control options have been implemented. Users may wish to keep segments on separate layers, and not allow them to be separated/merged automatically.
+ # This effect could leverage those options once they have been implemented.
+ oldOverwriteMode = self.scriptedEffect.parameterSetNode().GetOverwriteMode()
+ self.scriptedEffect.parameterSetNode().SetOverwriteMode(slicer.vtkMRMLSegmentEditorNode.OverwriteVisibleSegments)
+ for segmentId, labelValue in segmentLabelValues:
+ threshold.ThresholdBetween(labelValue, labelValue)
+ stencil.Update()
+ smoothedBinaryLabelMap = slicer.vtkOrientedImageData()
+ smoothedBinaryLabelMap.ShallowCopy(stencil.GetOutput())
+ smoothedBinaryLabelMap.SetImageToWorldMatrix(imageToWorldMatrix)
+ self.scriptedEffect.modifySegmentByLabelmap(segmentationNode, segmentId, smoothedBinaryLabelMap,
+ slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet, False)
+ self.scriptedEffect.parameterSetNode().SetOverwriteMode(oldOverwriteMode)
+
+ def paintApply(self, viewWidget):
+
+ # Current limitation: smoothing brush is not implemented for joint smoothing
+ smoothingMethod = self.scriptedEffect.parameter("SmoothingMethod")
+ if smoothingMethod == JOINT_TAUBIN:
+ self.scriptedEffect.clearBrushes()
+ self.scriptedEffect.forceRender(viewWidget)
+ slicer.util.messageBox("Smoothing brush is not available for 'joint smoothing' method.")
+ return
+
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ maskImage = slicer.vtkOrientedImageData()
+ maskImage.DeepCopy(modifierLabelmap)
+ maskExtent = self.scriptedEffect.paintBrushesIntoLabelmap(maskImage, viewWidget)
+ self.scriptedEffect.clearBrushes()
+ self.scriptedEffect.forceRender(viewWidget)
+ if maskExtent[0] > maskExtent[1] or maskExtent[2] > maskExtent[3] or maskExtent[4] > maskExtent[5]:
+ return
+
+ self.scriptedEffect.saveStateForUndo()
+ self.onApply(maskImage, maskExtent)
MEDIAN = 'MEDIAN'
diff --git a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py
index 1c27c1b965a..926fdc12450 100644
--- a/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py
+++ b/Modules/Loadable/Segmentations/EditorEffects/Python/SegmentEditorThresholdEffect.py
@@ -10,962 +10,962 @@
class SegmentEditorThresholdEffect(AbstractScriptedSegmentEditorEffect):
- """ ThresholdEffect is an Effect implementing the global threshold
- operation in the segment editor
-
- This is also an example for scripted effects, and some methods have no
- function. The methods that are not needed (i.e. the default implementation in
- qSlicerSegmentEditorAbstractEffect is satisfactory) can simply be omitted.
- """
-
- def __init__(self, scriptedEffect):
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
- scriptedEffect.name = 'Threshold'
-
- self.segment2DFillOpacity = None
- self.segment2DOutlineOpacity = None
- self.previewedSegmentID = None
-
- # Effect-specific members
- import vtkITK
- self.autoThresholdCalculator = vtkITK.vtkITKImageThresholdCalculator()
-
- self.timer = qt.QTimer()
- self.previewState = 0
- self.previewStep = 1
- self.previewSteps = 5
- self.timer.connect('timeout()', self.preview)
-
- self.previewPipelines = {}
- self.histogramPipeline = None
- self.setupPreviewDisplay()
-
- # Histogram stencil setup
- self.stencil = vtk.vtkPolyDataToImageStencil()
-
- # Histogram reslice setup
- self.reslice = vtk.vtkImageReslice()
- self.reslice.AutoCropOutputOff()
- self.reslice.SetOptimization(1)
- self.reslice.SetOutputOrigin(0, 0, 0)
- self.reslice.SetOutputSpacing(1, 1, 1)
- self.reslice.SetOutputDimensionality(3)
- self.reslice.GenerateStencilOutputOn()
-
- self.imageAccumulate = vtk.vtkImageAccumulate()
- self.imageAccumulate.SetInputConnection(0, self.reslice.GetOutputPort())
- self.imageAccumulate.SetInputConnection(1, self.stencil.GetOutputPort())
-
- self.selectionStartPosition = None
- self.selectionEndPosition = None
-
- def clone(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Threshold.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Fill segment based on master volume intensity range
. Options:
+ """ ThresholdEffect is an Effect implementing the global threshold
+ operation in the segment editor
+
+ This is also an example for scripted effects, and some methods have no
+ function. The methods that are not needed (i.e. the default implementation in
+ qSlicerSegmentEditorAbstractEffect is satisfactory) can simply be omitted.
+ """
+
+ def __init__(self, scriptedEffect):
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+ scriptedEffect.name = 'Threshold'
+
+ self.segment2DFillOpacity = None
+ self.segment2DOutlineOpacity = None
+ self.previewedSegmentID = None
+
+ # Effect-specific members
+ import vtkITK
+ self.autoThresholdCalculator = vtkITK.vtkITKImageThresholdCalculator()
+
+ self.timer = qt.QTimer()
+ self.previewState = 0
+ self.previewStep = 1
+ self.previewSteps = 5
+ self.timer.connect('timeout()', self.preview)
+
+ self.previewPipelines = {}
+ self.histogramPipeline = None
+ self.setupPreviewDisplay()
+
+ # Histogram stencil setup
+ self.stencil = vtk.vtkPolyDataToImageStencil()
+
+ # Histogram reslice setup
+ self.reslice = vtk.vtkImageReslice()
+ self.reslice.AutoCropOutputOff()
+ self.reslice.SetOptimization(1)
+ self.reslice.SetOutputOrigin(0, 0, 0)
+ self.reslice.SetOutputSpacing(1, 1, 1)
+ self.reslice.SetOutputDimensionality(3)
+ self.reslice.GenerateStencilOutputOn()
+
+ self.imageAccumulate = vtk.vtkImageAccumulate()
+ self.imageAccumulate.SetInputConnection(0, self.reslice.GetOutputPort())
+ self.imageAccumulate.SetInputConnection(1, self.stencil.GetOutputPort())
+
+ self.selectionStartPosition = None
+ self.selectionEndPosition = None
+
+ def clone(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/Threshold.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Fill segment based on master volume intensity range
. Options:
- Use for masking: set the selected intensity range as Editable intensity range and switch to Paint effect.
- Apply: set the previewed segmentation in the selected segment. Previous contents of the segment is overwritten.
"""
- def activate(self):
- self.setCurrentSegmentTransparent()
-
- # Update intensity range
- self.masterVolumeNodeChanged()
-
- # Setup and start preview pulse
- self.setupPreviewDisplay()
- self.timer.start(200)
-
- def deactivate(self):
- self.restorePreviewedSegmentTransparency()
-
- # Clear preview pipeline and stop timer
- self.clearPreviewDisplay()
- self.clearHistogramDisplay()
- self.timer.stop()
+ def activate(self):
+ self.setCurrentSegmentTransparent()
+
+ # Update intensity range
+ self.masterVolumeNodeChanged()
+
+ # Setup and start preview pulse
+ self.setupPreviewDisplay()
+ self.timer.start(200)
+
+ def deactivate(self):
+ self.restorePreviewedSegmentTransparency()
+
+ # Clear preview pipeline and stop timer
+ self.clearPreviewDisplay()
+ self.clearHistogramDisplay()
+ self.timer.stop()
+
+ def setCurrentSegmentTransparent(self):
+ """Save current segment opacity and set it to zero
+ to temporarily hide the segment so that threshold preview
+ can be seen better.
+ It also restores opacity of previously previewed segment.
+ Call restorePreviewedSegmentTransparency() to restore original
+ opacity.
+ """
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ if not segmentationNode:
+ return
+ displayNode = segmentationNode.GetDisplayNode()
+ if not displayNode:
+ return
+ segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+
+ if segmentID == self.previewedSegmentID:
+ # already previewing the current segment
+ return
+
+ # If an other segment was previewed before, restore that.
+ if self.previewedSegmentID:
+ self.restorePreviewedSegmentTransparency()
+
+ # Make current segment fully transparent
+ if segmentID:
+ self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID)
+ self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID)
+ self.previewedSegmentID = segmentID
+ displayNode.SetSegmentOpacity2DFill(segmentID, 0)
+ displayNode.SetSegmentOpacity2DOutline(segmentID, 0)
+
+ def restorePreviewedSegmentTransparency(self):
+ """Restore previewed segment's opacity that was temporarily
+ made transparen by calling setCurrentSegmentTransparent()."""
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ if not segmentationNode:
+ return
+ displayNode = segmentationNode.GetDisplayNode()
+ if not displayNode:
+ return
+ if not self.previewedSegmentID:
+ # already previewing the current segment
+ return
+ displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity)
+ displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity)
+ self.previewedSegmentID = None
+
+ def setupOptionsFrame(self):
+ self.thresholdSliderLabel = qt.QLabel("Threshold Range:")
+ self.thresholdSliderLabel.setToolTip("Set the range of the background values that should be labeled.")
+ self.scriptedEffect.addOptionsWidget(self.thresholdSliderLabel)
+
+ self.thresholdSlider = ctk.ctkRangeWidget()
+ self.thresholdSlider.spinBoxAlignment = qt.Qt.AlignTop
+ self.thresholdSlider.singleStep = 0.01
+ self.scriptedEffect.addOptionsWidget(self.thresholdSlider)
+
+ self.autoThresholdModeSelectorComboBox = qt.QComboBox()
+ self.autoThresholdModeSelectorComboBox.addItem("threshold above", MODE_SET_LOWER_MAX)
+ self.autoThresholdModeSelectorComboBox.addItem("threshold below", MODE_SET_MIN_UPPER)
+ self.autoThresholdModeSelectorComboBox.addItem("set as lower value", MODE_SET_LOWER)
+ self.autoThresholdModeSelectorComboBox.addItem("set as upper value", MODE_SET_UPPER)
+ self.autoThresholdModeSelectorComboBox.setToolTip("How to set lower and upper values of the threshold range."
+ " Threshold above/below: sets the range from the computed value to maximum/minimum."
+ " Set as lower/upper value: only modifies one side of the threshold range.")
+
+ self.autoThresholdMethodSelectorComboBox = qt.QComboBox()
+ self.autoThresholdMethodSelectorComboBox.addItem("Otsu", METHOD_OTSU)
+ self.autoThresholdMethodSelectorComboBox.addItem("Huang", METHOD_HUANG)
+ self.autoThresholdMethodSelectorComboBox.addItem("IsoData", METHOD_ISO_DATA)
+ # Kittler-Illingworth sometimes fails with an exception, but it does not cause any major issue,
+ # it just logs an error message and does not compute a new threshold value
+ self.autoThresholdMethodSelectorComboBox.addItem("Kittler-Illingworth", METHOD_KITTLER_ILLINGWORTH)
+ # Li sometimes crashes (index out of range error in
+ # ITK/Modules/Filtering/Thresholding/include/itkLiThresholdCalculator.hxx#L94)
+ # We can add this method back when issue is fixed in ITK.
+ # self.autoThresholdMethodSelectorComboBox.addItem("Li", METHOD_LI)
+ self.autoThresholdMethodSelectorComboBox.addItem("Maximum entropy", METHOD_MAXIMUM_ENTROPY)
+ self.autoThresholdMethodSelectorComboBox.addItem("Moments", METHOD_MOMENTS)
+ self.autoThresholdMethodSelectorComboBox.addItem("Renyi entropy", METHOD_RENYI_ENTROPY)
+ self.autoThresholdMethodSelectorComboBox.addItem("Shanbhag", METHOD_SHANBHAG)
+ self.autoThresholdMethodSelectorComboBox.addItem("Triangle", METHOD_TRIANGLE)
+ self.autoThresholdMethodSelectorComboBox.addItem("Yen", METHOD_YEN)
+ self.autoThresholdMethodSelectorComboBox.setToolTip("Select method to compute threshold value automatically.")
+
+ self.selectPreviousAutoThresholdButton = qt.QToolButton()
+ self.selectPreviousAutoThresholdButton.text = "<"
+ self.selectPreviousAutoThresholdButton.setToolTip("Select previous thresholding method and set thresholds."
+ + " Useful for iterating through all available methods.")
+
+ self.selectNextAutoThresholdButton = qt.QToolButton()
+ self.selectNextAutoThresholdButton.text = ">"
+ self.selectNextAutoThresholdButton.setToolTip("Select next thresholding method and set thresholds."
+ + " Useful for iterating through all available methods.")
+
+ self.setAutoThresholdButton = qt.QPushButton("Set")
+ self.setAutoThresholdButton.setToolTip("Set threshold using selected method.")
+ # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ # fails on some systems, therefore set the policies using separate method calls
+ qSize = qt.QSizePolicy()
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
+ self.setAutoThresholdButton.setSizePolicy(qSize)
+
+ autoThresholdFrame = qt.QGridLayout()
+ autoThresholdFrame.addWidget(self.autoThresholdMethodSelectorComboBox, 0, 0, 1, 1)
+ autoThresholdFrame.addWidget(self.selectPreviousAutoThresholdButton, 0, 1, 1, 1)
+ autoThresholdFrame.addWidget(self.selectNextAutoThresholdButton, 0, 2, 1, 1)
+ autoThresholdFrame.addWidget(self.autoThresholdModeSelectorComboBox, 1, 0, 1, 3)
+ autoThresholdFrame.addWidget(self.setAutoThresholdButton, 2, 0, 1, 3)
+
+ autoThresholdGroupBox = ctk.ctkCollapsibleGroupBox()
+ autoThresholdGroupBox.setTitle("Automatic threshold")
+ autoThresholdGroupBox.setLayout(autoThresholdFrame)
+ autoThresholdGroupBox.collapsed = True
+ self.scriptedEffect.addOptionsWidget(autoThresholdGroupBox)
+
+ histogramFrame = qt.QVBoxLayout()
+
+ histogramBrushFrame = qt.QHBoxLayout()
+ histogramFrame.addLayout(histogramBrushFrame)
+
+ self.regionLabel = qt.QLabel("Region shape:")
+ histogramBrushFrame.addWidget(self.regionLabel)
+
+ self.histogramBrushButtonGroup = qt.QButtonGroup()
+ self.histogramBrushButtonGroup.setExclusive(True)
+
+ self.boxROIButton = qt.QToolButton()
+ self.boxROIButton.setText("Box")
+ self.boxROIButton.setCheckable(True)
+ self.boxROIButton.clicked.connect(self.updateMRMLFromGUI)
+ histogramBrushFrame.addWidget(self.boxROIButton)
+ self.histogramBrushButtonGroup.addButton(self.boxROIButton)
+
+ self.circleROIButton = qt.QToolButton()
+ self.circleROIButton.setText("Circle")
+ self.circleROIButton.setCheckable(True)
+ self.circleROIButton.clicked.connect(self.updateMRMLFromGUI)
+ histogramBrushFrame.addWidget(self.circleROIButton)
+ self.histogramBrushButtonGroup.addButton(self.circleROIButton)
+
+ self.drawROIButton = qt.QToolButton()
+ self.drawROIButton.setText("Draw")
+ self.drawROIButton.setCheckable(True)
+ self.drawROIButton.clicked.connect(self.updateMRMLFromGUI)
+ histogramBrushFrame.addWidget(self.drawROIButton)
+ self.histogramBrushButtonGroup.addButton(self.drawROIButton)
+
+ self.lineROIButton = qt.QToolButton()
+ self.lineROIButton.setText("Line")
+ self.lineROIButton.setCheckable(True)
+ self.lineROIButton.clicked.connect(self.updateMRMLFromGUI)
+ histogramBrushFrame.addWidget(self.lineROIButton)
+ self.histogramBrushButtonGroup.addButton(self.lineROIButton)
+
+ histogramBrushFrame.addStretch()
+
+ self.histogramView = ctk.ctkTransferFunctionView()
+ self.histogramView = self.histogramView
+ histogramFrame.addWidget(self.histogramView)
+ scene = self.histogramView.scene()
+
+ self.histogramFunction = vtk.vtkPiecewiseFunction()
+ self.histogramFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
+ self.histogramFunctionContainer.setPiecewiseFunction(self.histogramFunction)
+ self.histogramFunctionItem = ctk.ctkTransferFunctionBarsItem(self.histogramFunctionContainer)
+ self.histogramFunctionItem.barWidth = 1.0
+ self.histogramFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
+ self.histogramFunctionItem.setZValue(1)
+ scene.addItem(self.histogramFunctionItem)
+
+ self.histogramEventFilter = HistogramEventFilter()
+ self.histogramEventFilter.setThresholdEffect(self)
+ self.histogramFunctionItem.installEventFilter(self.histogramEventFilter)
+
+ self.minMaxFunction = vtk.vtkPiecewiseFunction()
+ self.minMaxFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
+ self.minMaxFunctionContainer.setPiecewiseFunction(self.minMaxFunction)
+ self.minMaxFunctionItem = ctk.ctkTransferFunctionBarsItem(self.minMaxFunctionContainer)
+ self.minMaxFunctionItem.barWidth = 0.03
+ self.minMaxFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
+ self.minMaxFunctionItem.barColor = qt.QColor(200, 0, 0)
+ self.minMaxFunctionItem.setZValue(0)
+ scene.addItem(self.minMaxFunctionItem)
+
+ self.averageFunction = vtk.vtkPiecewiseFunction()
+ self.averageFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
+ self.averageFunctionContainer.setPiecewiseFunction(self.averageFunction)
+ self.averageFunctionItem = ctk.ctkTransferFunctionBarsItem(self.averageFunctionContainer)
+ self.averageFunctionItem.barWidth = 0.03
+ self.averageFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
+ self.averageFunctionItem.barColor = qt.QColor(225, 150, 0)
+ self.averageFunctionItem.setZValue(-1)
+ scene.addItem(self.averageFunctionItem)
+
+ # Window level gradient
+ self.backgroundColor = [1.0, 1.0, 0.7]
+ self.backgroundFunction = vtk.vtkColorTransferFunction()
+ self.backgroundFunctionContainer = ctk.ctkVTKColorTransferFunction(self.scriptedEffect)
+ self.backgroundFunctionContainer.setColorTransferFunction(self.backgroundFunction)
+ self.backgroundFunctionItem = ctk.ctkTransferFunctionGradientItem(self.backgroundFunctionContainer)
+ self.backgroundFunctionItem.setZValue(-2)
+ scene.addItem(self.backgroundFunctionItem)
+
+ histogramItemFrame = qt.QHBoxLayout()
+ histogramFrame.addLayout(histogramItemFrame)
+
+ ###
+ # Lower histogram threshold buttons
+
+ lowerGroupBox = qt.QGroupBox("Lower")
+ lowerHistogramLayout = qt.QHBoxLayout()
+ lowerHistogramLayout.setContentsMargins(0, 3, 0, 3)
+ lowerGroupBox.setLayout(lowerHistogramLayout)
+ histogramItemFrame.addWidget(lowerGroupBox)
+ self.histogramLowerMethodButtonGroup = qt.QButtonGroup()
+ self.histogramLowerMethodButtonGroup.setExclusive(True)
+
+ self.histogramLowerThresholdMinimumButton = qt.QToolButton()
+ self.histogramLowerThresholdMinimumButton.setText("Min")
+ self.histogramLowerThresholdMinimumButton.setToolTip("Minimum")
+ self.histogramLowerThresholdMinimumButton.setCheckable(True)
+ self.histogramLowerThresholdMinimumButton.clicked.connect(self.updateMRMLFromGUI)
+ lowerHistogramLayout.addWidget(self.histogramLowerThresholdMinimumButton)
+ self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdMinimumButton)
+
+ self.histogramLowerThresholdLowerButton = qt.QToolButton()
+ self.histogramLowerThresholdLowerButton.setText("Lower")
+ self.histogramLowerThresholdLowerButton.setCheckable(True)
+ self.histogramLowerThresholdLowerButton.clicked.connect(self.updateMRMLFromGUI)
+ lowerHistogramLayout.addWidget(self.histogramLowerThresholdLowerButton)
+ self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdLowerButton)
+
+ self.histogramLowerThresholdAverageButton = qt.QToolButton()
+ self.histogramLowerThresholdAverageButton.setText("Mean")
+ self.histogramLowerThresholdAverageButton.setCheckable(True)
+ self.histogramLowerThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI)
+ lowerHistogramLayout.addWidget(self.histogramLowerThresholdAverageButton)
+ self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdAverageButton)
+
+ ###
+ # Upper histogram threshold buttons
+
+ upperGroupBox = qt.QGroupBox("Upper")
+ upperHistogramLayout = qt.QHBoxLayout()
+ upperHistogramLayout.setContentsMargins(0, 3, 0, 3)
+ upperGroupBox.setLayout(upperHistogramLayout)
+ histogramItemFrame.addWidget(upperGroupBox)
+ self.histogramUpperMethodButtonGroup = qt.QButtonGroup()
+ self.histogramUpperMethodButtonGroup.setExclusive(True)
+
+ self.histogramUpperThresholdAverageButton = qt.QToolButton()
+ self.histogramUpperThresholdAverageButton.setText("Mean")
+ self.histogramUpperThresholdAverageButton.setCheckable(True)
+ self.histogramUpperThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI)
+ upperHistogramLayout.addWidget(self.histogramUpperThresholdAverageButton)
+ self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdAverageButton)
+
+ self.histogramUpperThresholdUpperButton = qt.QToolButton()
+ self.histogramUpperThresholdUpperButton.setText("Upper")
+ self.histogramUpperThresholdUpperButton.setCheckable(True)
+ self.histogramUpperThresholdUpperButton.clicked.connect(self.updateMRMLFromGUI)
+ upperHistogramLayout.addWidget(self.histogramUpperThresholdUpperButton)
+ self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdUpperButton)
+
+ self.histogramUpperThresholdMaximumButton = qt.QToolButton()
+ self.histogramUpperThresholdMaximumButton.setText("Max")
+ self.histogramUpperThresholdMaximumButton.setToolTip("Maximum")
+ self.histogramUpperThresholdMaximumButton.setCheckable(True)
+ self.histogramUpperThresholdMaximumButton.clicked.connect(self.updateMRMLFromGUI)
+ upperHistogramLayout.addWidget(self.histogramUpperThresholdMaximumButton)
+ self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdMaximumButton)
+
+ histogramGroupBox = ctk.ctkCollapsibleGroupBox()
+ histogramGroupBox.setTitle("Local histogram")
+ histogramGroupBox.setLayout(histogramFrame)
+ histogramGroupBox.collapsed = True
+ self.scriptedEffect.addOptionsWidget(histogramGroupBox)
+
+ self.useForPaintButton = qt.QPushButton("Use for masking")
+ self.useForPaintButton.setToolTip("Use specified intensity range for masking and switch to Paint effect.")
+ self.scriptedEffect.addOptionsWidget(self.useForPaintButton)
+
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Fill selected segment in regions that are in the specified intensity range.")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+
+ self.useForPaintButton.connect('clicked()', self.onUseForPaint)
+ self.thresholdSlider.connect('valuesChanged(double,double)', self.onThresholdValuesChanged)
+ self.autoThresholdMethodSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod)
+ self.autoThresholdModeSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod)
+ self.selectPreviousAutoThresholdButton.connect('clicked()', self.onSelectPreviousAutoThresholdMethod)
+ self.selectNextAutoThresholdButton.connect('clicked()', self.onSelectNextAutoThresholdMethod)
+ self.setAutoThresholdButton.connect('clicked()', self.onAutoThreshold)
+ self.applyButton.connect('clicked()', self.onApply)
+
+ def masterVolumeNodeChanged(self):
+ # Set scalar range of master volume image data to threshold slider
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ if masterImageData:
+ lo, hi = masterImageData.GetScalarRange()
+ self.thresholdSlider.setRange(lo, hi)
+ self.thresholdSlider.singleStep = (hi - lo) / 1000.
+ if (self.scriptedEffect.doubleParameter("MinimumThreshold") == self.scriptedEffect.doubleParameter("MaximumThreshold")):
+ # has not been initialized yet
+ self.scriptedEffect.setParameter("MinimumThreshold", lo + (hi - lo) * 0.25)
+ self.scriptedEffect.setParameter("MaximumThreshold", hi)
+
+ def layoutChanged(self):
+ self.setupPreviewDisplay()
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.)
+ self.scriptedEffect.setParameterDefault("MaximumThreshold", 0)
+ self.scriptedEffect.setParameterDefault("AutoThresholdMethod", METHOD_OTSU)
+ self.scriptedEffect.setParameterDefault("AutoThresholdMode", MODE_SET_LOWER_MAX)
+ self.scriptedEffect.setParameterDefault(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, HISTOGRAM_BRUSH_TYPE_CIRCLE)
+ self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_LOWER_PARAMETER_NAME, HISTOGRAM_SET_LOWER)
+ self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_UPPER_PARAMETER_NAME, HISTOGRAM_SET_UPPER)
+
+ def updateGUIFromMRML(self):
+ self.thresholdSlider.blockSignals(True)
+ self.thresholdSlider.setMinimumValue(self.scriptedEffect.doubleParameter("MinimumThreshold"))
+ self.thresholdSlider.setMaximumValue(self.scriptedEffect.doubleParameter("MaximumThreshold"))
+ self.thresholdSlider.blockSignals(False)
+
+ autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMethod"))
+ wasBlocked = self.autoThresholdMethodSelectorComboBox.blockSignals(True)
+ self.autoThresholdMethodSelectorComboBox.setCurrentIndex(autoThresholdMethod)
+ self.autoThresholdMethodSelectorComboBox.blockSignals(wasBlocked)
+
+ autoThresholdMode = self.autoThresholdModeSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMode"))
+ wasBlocked = self.autoThresholdModeSelectorComboBox.blockSignals(True)
+ self.autoThresholdModeSelectorComboBox.setCurrentIndex(autoThresholdMode)
+ self.autoThresholdModeSelectorComboBox.blockSignals(wasBlocked)
+
+ histogramBrushType = self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME)
+ self.boxROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_BOX)
+ self.circleROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_CIRCLE)
+ self.drawROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_DRAW)
+ self.lineROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_LINE)
+
+ histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
+ self.histogramLowerThresholdMinimumButton.checked = (histogramSetModeLower == HISTOGRAM_SET_MINIMUM)
+ self.histogramLowerThresholdLowerButton.checked = (histogramSetModeLower == HISTOGRAM_SET_LOWER)
+ self.histogramLowerThresholdAverageButton.checked = (histogramSetModeLower == HISTOGRAM_SET_AVERAGE)
+
+ histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
+ self.histogramUpperThresholdAverageButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_AVERAGE)
+ self.histogramUpperThresholdUpperButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_UPPER)
+ self.histogramUpperThresholdMaximumButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM)
+
+ self.updateHistogramBackground()
+
+ def updateMRMLFromGUI(self):
+ with slicer.util.NodeModify(self.scriptedEffect.parameterSetNode()):
+ self.scriptedEffect.setParameter("MinimumThreshold", self.thresholdSlider.minimumValue)
+ self.scriptedEffect.setParameter("MaximumThreshold", self.thresholdSlider.maximumValue)
+
+ methodIndex = self.autoThresholdMethodSelectorComboBox.currentIndex
+ autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.itemData(methodIndex)
+ self.scriptedEffect.setParameter("AutoThresholdMethod", autoThresholdMethod)
+
+ modeIndex = self.autoThresholdModeSelectorComboBox.currentIndex
+ autoThresholdMode = self.autoThresholdModeSelectorComboBox.itemData(modeIndex)
+ self.scriptedEffect.setParameter("AutoThresholdMode", autoThresholdMode)
+
+ histogramParameterChanged = False
+
+ histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
+ if self.boxROIButton.checked:
+ histogramBrushType = HISTOGRAM_BRUSH_TYPE_BOX
+ elif self.circleROIButton.checked:
+ histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
+ elif self.drawROIButton.checked:
+ histogramBrushType = HISTOGRAM_BRUSH_TYPE_DRAW
+ elif self.lineROIButton.checked:
+ histogramBrushType = HISTOGRAM_BRUSH_TYPE_LINE
+
+ if histogramBrushType != self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME):
+ self.scriptedEffect.setParameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, histogramBrushType)
+ histogramParameterChanged = True
+
+ histogramSetModeLower = HISTOGRAM_SET_LOWER
+ if self.histogramLowerThresholdMinimumButton.checked:
+ histogramSetModeLower = HISTOGRAM_SET_MINIMUM
+ elif self.histogramLowerThresholdLowerButton.checked:
+ histogramSetModeLower = HISTOGRAM_SET_LOWER
+ elif self.histogramLowerThresholdAverageButton.checked:
+ histogramSetModeLower = HISTOGRAM_SET_AVERAGE
+ if histogramSetModeLower != self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME):
+ self.scriptedEffect.setParameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME, histogramSetModeLower)
+ histogramParameterChanged = True
+
+ histogramSetModeUpper = HISTOGRAM_SET_UPPER
+ if self.histogramUpperThresholdAverageButton.checked:
+ histogramSetModeUpper = HISTOGRAM_SET_AVERAGE
+ elif self.histogramUpperThresholdUpperButton.checked:
+ histogramSetModeUpper = HISTOGRAM_SET_UPPER
+ elif self.histogramUpperThresholdMaximumButton.checked:
+ histogramSetModeUpper = HISTOGRAM_SET_MAXIMUM
+ if histogramSetModeUpper != self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME):
+ self.scriptedEffect.setParameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME, histogramSetModeUpper)
+ histogramParameterChanged = True
+
+ if histogramParameterChanged:
+ self.updateHistogram()
+
+ #
+ # Effect specific methods (the above ones are the API methods to override)
+ #
+ def onThresholdValuesChanged(self, min, max):
+ self.scriptedEffect.updateMRMLFromGUI()
+
+ def onUseForPaint(self):
+ parameterSetNode = self.scriptedEffect.parameterSetNode()
+ parameterSetNode.MasterVolumeIntensityMaskOn()
+ parameterSetNode.SetMasterVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue)
+ # Switch to paint effect
+ self.scriptedEffect.selectEffect("Paint")
+
+ def onSelectPreviousAutoThresholdMethod(self):
+ self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex - 1) \
+ % self.autoThresholdMethodSelectorComboBox.count
+ self.onSelectedAutoThresholdMethod()
+
+ def onSelectNextAutoThresholdMethod(self):
+ self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex + 1) \
+ % self.autoThresholdMethodSelectorComboBox.count
+ self.onSelectedAutoThresholdMethod()
+
+ def onSelectedAutoThresholdMethod(self):
+ self.updateMRMLFromGUI()
+ self.onAutoThreshold()
+ self.updateGUIFromMRML()
+
+ def onAutoThreshold(self):
+ autoThresholdMethod = self.scriptedEffect.parameter("AutoThresholdMethod")
+ autoThresholdMode = self.scriptedEffect.parameter("AutoThresholdMode")
+ self.autoThreshold(autoThresholdMethod, autoThresholdMode)
+
+ def autoThreshold(self, autoThresholdMethod, autoThresholdMode):
+ if autoThresholdMethod == METHOD_HUANG:
+ self.autoThresholdCalculator.SetMethodToHuang()
+ elif autoThresholdMethod == METHOD_INTERMODES:
+ self.autoThresholdCalculator.SetMethodToIntermodes()
+ elif autoThresholdMethod == METHOD_ISO_DATA:
+ self.autoThresholdCalculator.SetMethodToIsoData()
+ elif autoThresholdMethod == METHOD_KITTLER_ILLINGWORTH:
+ self.autoThresholdCalculator.SetMethodToKittlerIllingworth()
+ elif autoThresholdMethod == METHOD_LI:
+ self.autoThresholdCalculator.SetMethodToLi()
+ elif autoThresholdMethod == METHOD_MAXIMUM_ENTROPY:
+ self.autoThresholdCalculator.SetMethodToMaximumEntropy()
+ elif autoThresholdMethod == METHOD_MOMENTS:
+ self.autoThresholdCalculator.SetMethodToMoments()
+ elif autoThresholdMethod == METHOD_OTSU:
+ self.autoThresholdCalculator.SetMethodToOtsu()
+ elif autoThresholdMethod == METHOD_RENYI_ENTROPY:
+ self.autoThresholdCalculator.SetMethodToRenyiEntropy()
+ elif autoThresholdMethod == METHOD_SHANBHAG:
+ self.autoThresholdCalculator.SetMethodToShanbhag()
+ elif autoThresholdMethod == METHOD_TRIANGLE:
+ self.autoThresholdCalculator.SetMethodToTriangle()
+ elif autoThresholdMethod == METHOD_YEN:
+ self.autoThresholdCalculator.SetMethodToYen()
+ else:
+ logging.error(f"Unknown AutoThresholdMethod {autoThresholdMethod}")
+
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ self.autoThresholdCalculator.SetInputData(masterImageData)
+
+ self.autoThresholdCalculator.Update()
+ computedThreshold = self.autoThresholdCalculator.GetThreshold()
+
+ masterVolumeMin, masterVolumeMax = masterImageData.GetScalarRange()
+
+ if autoThresholdMode == MODE_SET_UPPER:
+ self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
+ elif autoThresholdMode == MODE_SET_LOWER:
+ self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
+ elif autoThresholdMode == MODE_SET_MIN_UPPER:
+ self.scriptedEffect.setParameter("MinimumThreshold", masterVolumeMin)
+ self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
+ elif autoThresholdMode == MODE_SET_LOWER_MAX:
+ self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
+ self.scriptedEffect.setParameter("MaximumThreshold", masterVolumeMax)
+ else:
+ logging.error(f"Unknown AutoThresholdMode {autoThresholdMode}")
+
+ def onApply(self):
+ if not self.scriptedEffect.confirmCurrentSegmentVisible():
+ return
+
+ try:
+ # Get master volume image data
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ # Get modifier labelmap
+ modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
+ originalImageToWorldMatrix = vtk.vtkMatrix4x4()
+ modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix)
+ # Get parameters
+ min = self.scriptedEffect.doubleParameter("MinimumThreshold")
+ max = self.scriptedEffect.doubleParameter("MaximumThreshold")
+
+ self.scriptedEffect.saveStateForUndo()
+
+ # Perform thresholding
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(masterImageData)
+ thresh.ThresholdBetween(min, max)
+ thresh.SetInValue(1)
+ thresh.SetOutValue(0)
+ thresh.SetOutputScalarType(modifierLabelmap.GetScalarType())
+ thresh.Update()
+ modifierLabelmap.DeepCopy(thresh.GetOutput())
+ except IndexError:
+ logging.error('apply: Failed to threshold master volume!')
+ pass
+
+ # Apply changes
+ self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
+
+ # De-select effect
+ self.scriptedEffect.selectEffect("")
+
+ def clearPreviewDisplay(self):
+ for sliceWidget, pipeline in self.previewPipelines.items():
+ self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
+ self.previewPipelines = {}
+
+ def clearHistogramDisplay(self):
+ if self.histogramPipeline is None:
+ return
+ self.histogramPipeline.removeActors()
+ self.histogramPipeline = None
+
+ def setupPreviewDisplay(self):
+ # Clear previous pipelines before setting up the new ones
+ self.clearPreviewDisplay()
+
+ layoutManager = slicer.app.layoutManager()
+ if layoutManager is None:
+ return
+
+ # Add a pipeline for each 2D slice view
+ for sliceViewName in layoutManager.sliceViewNames():
+ sliceWidget = layoutManager.sliceWidget(sliceViewName)
+ if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()):
+ continue
+ renderer = self.scriptedEffect.renderer(sliceWidget)
+ if renderer is None:
+ logging.error("setupPreviewDisplay: Failed to get renderer!")
+ continue
+
+ # Create pipeline
+ pipeline = PreviewPipeline()
+ self.previewPipelines[sliceWidget] = pipeline
+
+ # Add actor
+ self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
+
+ def preview(self):
+
+ opacity = 0.5 + self.previewState / (2. * self.previewSteps)
+ min = self.scriptedEffect.doubleParameter("MinimumThreshold")
+ max = self.scriptedEffect.doubleParameter("MaximumThreshold")
+
+ # Get color of edited segment
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ if not segmentationNode:
+ # scene was closed while preview was active
+ return
+ displayNode = segmentationNode.GetDisplayNode()
+ if displayNode is None:
+ logging.error("preview: Invalid segmentation display node!")
+ color = [0.5, 0.5, 0.5]
+ segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
+
+ # Make sure we keep the currently selected segment hidden (the user may have changed selection)
+ if segmentID != self.previewedSegmentID:
+ self.setCurrentSegmentTransparent()
+
+ r, g, b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor()
+
+ # Set values to pipelines
+ for sliceWidget in self.previewPipelines:
+ pipeline = self.previewPipelines[sliceWidget]
+ pipeline.lookupTable.SetTableValue(1, r, g, b, opacity)
+ layerLogic = self.getMasterVolumeLayerLogic(sliceWidget)
+ pipeline.thresholdFilter.SetInputConnection(layerLogic.GetReslice().GetOutputPort())
+ pipeline.thresholdFilter.ThresholdBetween(min, max)
+ pipeline.actor.VisibilityOn()
+ sliceWidget.sliceView().scheduleRender()
+
+ self.previewState += self.previewStep
+ if self.previewState >= self.previewSteps:
+ self.previewStep = -1
+ if self.previewState <= 0:
+ self.previewStep = 1
+
+ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
+ abortEvent = False
+
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ if masterImageData is None:
+ return abortEvent
+
+ # Only allow for slice views
+ if viewWidget.className() != "qMRMLSliceWidget":
+ return abortEvent
+
+ anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
+
+ # Clicking in a view should remove all previous pipelines
+ if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
+ self.clearHistogramDisplay()
+
+ if self.histogramPipeline is None:
+ self.createHistogramPipeline(viewWidget)
+
+ xy = callerInteractor.GetEventPosition()
+ ras = self.xyToRas(xy, viewWidget)
+
+ if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
+ self.histogramPipeline.state = HISTOGRAM_STATE_MOVING
+ self.histogramPipeline.addPoint(ras)
+ self.updateHistogram()
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
+ if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
+ self.histogramPipeline.state = HISTOGRAM_STATE_PLACED
+ abortEvent = True
+ elif eventId == vtk.vtkCommand.MouseMoveEvent:
+ if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
+ self.histogramPipeline.addPoint(ras)
+ self.updateHistogram()
+ return abortEvent
+
+ def createHistogramPipeline(self, sliceWidget):
+ brushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
+ if self.boxROIButton.checked:
+ brushType = HISTOGRAM_BRUSH_TYPE_BOX
+ elif self.drawROIButton.checked:
+ brushType = HISTOGRAM_BRUSH_TYPE_DRAW
+ elif self.lineROIButton.checked:
+ brushType = HISTOGRAM_BRUSH_TYPE_LINE
+ pipeline = HistogramPipeline(self, self.scriptedEffect, sliceWidget, brushType)
+ self.histogramPipeline = pipeline
+
+ def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
+ if self.histogramPipeline is not None:
+ self.histogramPipeline.updateBrushModel()
+
+ def onHistogramMouseClick(self, pos, button):
+ self.selectionStartPosition = pos
+ self.selectionEndPosition = pos
+ if (button == qt.Qt.RightButton):
+ self.selectionStartPosition = None
+ self.selectionEndPosition = None
+ self.minMaxFunction.RemoveAllPoints()
+ self.averageFunction.RemoveAllPoints()
+ self.updateHistogram()
- def setCurrentSegmentTransparent(self):
- """Save current segment opacity and set it to zero
- to temporarily hide the segment so that threshold preview
- can be seen better.
- It also restores opacity of previously previewed segment.
- Call restorePreviewedSegmentTransparency() to restore original
- opacity.
- """
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- if not segmentationNode:
- return
- displayNode = segmentationNode.GetDisplayNode()
- if not displayNode:
- return
- segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
-
- if segmentID == self.previewedSegmentID:
- # already previewing the current segment
- return
-
- # If an other segment was previewed before, restore that.
- if self.previewedSegmentID:
- self.restorePreviewedSegmentTransparency()
-
- # Make current segment fully transparent
- if segmentID:
- self.segment2DFillOpacity = displayNode.GetSegmentOpacity2DFill(segmentID)
- self.segment2DOutlineOpacity = displayNode.GetSegmentOpacity2DOutline(segmentID)
- self.previewedSegmentID = segmentID
- displayNode.SetSegmentOpacity2DFill(segmentID, 0)
- displayNode.SetSegmentOpacity2DOutline(segmentID, 0)
-
- def restorePreviewedSegmentTransparency(self):
- """Restore previewed segment's opacity that was temporarily
- made transparen by calling setCurrentSegmentTransparent()."""
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- if not segmentationNode:
- return
- displayNode = segmentationNode.GetDisplayNode()
- if not displayNode:
- return
- if not self.previewedSegmentID:
- # already previewing the current segment
- return
- displayNode.SetSegmentOpacity2DFill(self.previewedSegmentID, self.segment2DFillOpacity)
- displayNode.SetSegmentOpacity2DOutline(self.previewedSegmentID, self.segment2DOutlineOpacity)
- self.previewedSegmentID = None
-
- def setupOptionsFrame(self):
- self.thresholdSliderLabel = qt.QLabel("Threshold Range:")
- self.thresholdSliderLabel.setToolTip("Set the range of the background values that should be labeled.")
- self.scriptedEffect.addOptionsWidget(self.thresholdSliderLabel)
-
- self.thresholdSlider = ctk.ctkRangeWidget()
- self.thresholdSlider.spinBoxAlignment = qt.Qt.AlignTop
- self.thresholdSlider.singleStep = 0.01
- self.scriptedEffect.addOptionsWidget(self.thresholdSlider)
-
- self.autoThresholdModeSelectorComboBox = qt.QComboBox()
- self.autoThresholdModeSelectorComboBox.addItem("threshold above", MODE_SET_LOWER_MAX)
- self.autoThresholdModeSelectorComboBox.addItem("threshold below", MODE_SET_MIN_UPPER)
- self.autoThresholdModeSelectorComboBox.addItem("set as lower value", MODE_SET_LOWER)
- self.autoThresholdModeSelectorComboBox.addItem("set as upper value", MODE_SET_UPPER)
- self.autoThresholdModeSelectorComboBox.setToolTip("How to set lower and upper values of the threshold range."
- " Threshold above/below: sets the range from the computed value to maximum/minimum."
- " Set as lower/upper value: only modifies one side of the threshold range.")
-
- self.autoThresholdMethodSelectorComboBox = qt.QComboBox()
- self.autoThresholdMethodSelectorComboBox.addItem("Otsu", METHOD_OTSU)
- self.autoThresholdMethodSelectorComboBox.addItem("Huang", METHOD_HUANG)
- self.autoThresholdMethodSelectorComboBox.addItem("IsoData", METHOD_ISO_DATA)
- # Kittler-Illingworth sometimes fails with an exception, but it does not cause any major issue,
- # it just logs an error message and does not compute a new threshold value
- self.autoThresholdMethodSelectorComboBox.addItem("Kittler-Illingworth", METHOD_KITTLER_ILLINGWORTH)
- # Li sometimes crashes (index out of range error in
- # ITK/Modules/Filtering/Thresholding/include/itkLiThresholdCalculator.hxx#L94)
- # We can add this method back when issue is fixed in ITK.
- # self.autoThresholdMethodSelectorComboBox.addItem("Li", METHOD_LI)
- self.autoThresholdMethodSelectorComboBox.addItem("Maximum entropy", METHOD_MAXIMUM_ENTROPY)
- self.autoThresholdMethodSelectorComboBox.addItem("Moments", METHOD_MOMENTS)
- self.autoThresholdMethodSelectorComboBox.addItem("Renyi entropy", METHOD_RENYI_ENTROPY)
- self.autoThresholdMethodSelectorComboBox.addItem("Shanbhag", METHOD_SHANBHAG)
- self.autoThresholdMethodSelectorComboBox.addItem("Triangle", METHOD_TRIANGLE)
- self.autoThresholdMethodSelectorComboBox.addItem("Yen", METHOD_YEN)
- self.autoThresholdMethodSelectorComboBox.setToolTip("Select method to compute threshold value automatically.")
-
- self.selectPreviousAutoThresholdButton = qt.QToolButton()
- self.selectPreviousAutoThresholdButton.text = "<"
- self.selectPreviousAutoThresholdButton.setToolTip("Select previous thresholding method and set thresholds."
- + " Useful for iterating through all available methods.")
-
- self.selectNextAutoThresholdButton = qt.QToolButton()
- self.selectNextAutoThresholdButton.text = ">"
- self.selectNextAutoThresholdButton.setToolTip("Select next thresholding method and set thresholds."
- + " Useful for iterating through all available methods.")
-
- self.setAutoThresholdButton = qt.QPushButton("Set")
- self.setAutoThresholdButton.setToolTip("Set threshold using selected method.")
- # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
- # fails on some systems, therefore set the policies using separate method calls
- qSize = qt.QSizePolicy()
- qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
- self.setAutoThresholdButton.setSizePolicy(qSize)
-
- autoThresholdFrame = qt.QGridLayout()
- autoThresholdFrame.addWidget(self.autoThresholdMethodSelectorComboBox, 0, 0, 1, 1)
- autoThresholdFrame.addWidget(self.selectPreviousAutoThresholdButton, 0, 1, 1, 1)
- autoThresholdFrame.addWidget(self.selectNextAutoThresholdButton, 0, 2, 1, 1)
- autoThresholdFrame.addWidget(self.autoThresholdModeSelectorComboBox, 1, 0, 1, 3)
- autoThresholdFrame.addWidget(self.setAutoThresholdButton, 2, 0, 1, 3)
-
- autoThresholdGroupBox = ctk.ctkCollapsibleGroupBox()
- autoThresholdGroupBox.setTitle("Automatic threshold")
- autoThresholdGroupBox.setLayout(autoThresholdFrame)
- autoThresholdGroupBox.collapsed = True
- self.scriptedEffect.addOptionsWidget(autoThresholdGroupBox)
-
- histogramFrame = qt.QVBoxLayout()
-
- histogramBrushFrame = qt.QHBoxLayout()
- histogramFrame.addLayout(histogramBrushFrame)
-
- self.regionLabel = qt.QLabel("Region shape:")
- histogramBrushFrame.addWidget(self.regionLabel)
-
- self.histogramBrushButtonGroup = qt.QButtonGroup()
- self.histogramBrushButtonGroup.setExclusive(True)
-
- self.boxROIButton = qt.QToolButton()
- self.boxROIButton.setText("Box")
- self.boxROIButton.setCheckable(True)
- self.boxROIButton.clicked.connect(self.updateMRMLFromGUI)
- histogramBrushFrame.addWidget(self.boxROIButton)
- self.histogramBrushButtonGroup.addButton(self.boxROIButton)
-
- self.circleROIButton = qt.QToolButton()
- self.circleROIButton.setText("Circle")
- self.circleROIButton.setCheckable(True)
- self.circleROIButton.clicked.connect(self.updateMRMLFromGUI)
- histogramBrushFrame.addWidget(self.circleROIButton)
- self.histogramBrushButtonGroup.addButton(self.circleROIButton)
-
- self.drawROIButton = qt.QToolButton()
- self.drawROIButton.setText("Draw")
- self.drawROIButton.setCheckable(True)
- self.drawROIButton.clicked.connect(self.updateMRMLFromGUI)
- histogramBrushFrame.addWidget(self.drawROIButton)
- self.histogramBrushButtonGroup.addButton(self.drawROIButton)
-
- self.lineROIButton = qt.QToolButton()
- self.lineROIButton.setText("Line")
- self.lineROIButton.setCheckable(True)
- self.lineROIButton.clicked.connect(self.updateMRMLFromGUI)
- histogramBrushFrame.addWidget(self.lineROIButton)
- self.histogramBrushButtonGroup.addButton(self.lineROIButton)
-
- histogramBrushFrame.addStretch()
-
- self.histogramView = ctk.ctkTransferFunctionView()
- self.histogramView = self.histogramView
- histogramFrame.addWidget(self.histogramView)
- scene = self.histogramView.scene()
-
- self.histogramFunction = vtk.vtkPiecewiseFunction()
- self.histogramFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
- self.histogramFunctionContainer.setPiecewiseFunction(self.histogramFunction)
- self.histogramFunctionItem = ctk.ctkTransferFunctionBarsItem(self.histogramFunctionContainer)
- self.histogramFunctionItem.barWidth = 1.0
- self.histogramFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
- self.histogramFunctionItem.setZValue(1)
- scene.addItem(self.histogramFunctionItem)
-
- self.histogramEventFilter = HistogramEventFilter()
- self.histogramEventFilter.setThresholdEffect(self)
- self.histogramFunctionItem.installEventFilter(self.histogramEventFilter)
-
- self.minMaxFunction = vtk.vtkPiecewiseFunction()
- self.minMaxFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
- self.minMaxFunctionContainer.setPiecewiseFunction(self.minMaxFunction)
- self.minMaxFunctionItem = ctk.ctkTransferFunctionBarsItem(self.minMaxFunctionContainer)
- self.minMaxFunctionItem.barWidth = 0.03
- self.minMaxFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
- self.minMaxFunctionItem.barColor = qt.QColor(200, 0, 0)
- self.minMaxFunctionItem.setZValue(0)
- scene.addItem(self.minMaxFunctionItem)
-
- self.averageFunction = vtk.vtkPiecewiseFunction()
- self.averageFunctionContainer = ctk.ctkVTKPiecewiseFunction(self.scriptedEffect)
- self.averageFunctionContainer.setPiecewiseFunction(self.averageFunction)
- self.averageFunctionItem = ctk.ctkTransferFunctionBarsItem(self.averageFunctionContainer)
- self.averageFunctionItem.barWidth = 0.03
- self.averageFunctionItem.logMode = ctk.ctkTransferFunctionBarsItem.NoLog
- self.averageFunctionItem.barColor = qt.QColor(225, 150, 0)
- self.averageFunctionItem.setZValue(-1)
- scene.addItem(self.averageFunctionItem)
-
- # Window level gradient
- self.backgroundColor = [1.0, 1.0, 0.7]
- self.backgroundFunction = vtk.vtkColorTransferFunction()
- self.backgroundFunctionContainer = ctk.ctkVTKColorTransferFunction(self.scriptedEffect)
- self.backgroundFunctionContainer.setColorTransferFunction(self.backgroundFunction)
- self.backgroundFunctionItem = ctk.ctkTransferFunctionGradientItem(self.backgroundFunctionContainer)
- self.backgroundFunctionItem.setZValue(-2)
- scene.addItem(self.backgroundFunctionItem)
-
- histogramItemFrame = qt.QHBoxLayout()
- histogramFrame.addLayout(histogramItemFrame)
-
- ###
- # Lower histogram threshold buttons
-
- lowerGroupBox = qt.QGroupBox("Lower")
- lowerHistogramLayout = qt.QHBoxLayout()
- lowerHistogramLayout.setContentsMargins(0, 3, 0, 3)
- lowerGroupBox.setLayout(lowerHistogramLayout)
- histogramItemFrame.addWidget(lowerGroupBox)
- self.histogramLowerMethodButtonGroup = qt.QButtonGroup()
- self.histogramLowerMethodButtonGroup.setExclusive(True)
-
- self.histogramLowerThresholdMinimumButton = qt.QToolButton()
- self.histogramLowerThresholdMinimumButton.setText("Min")
- self.histogramLowerThresholdMinimumButton.setToolTip("Minimum")
- self.histogramLowerThresholdMinimumButton.setCheckable(True)
- self.histogramLowerThresholdMinimumButton.clicked.connect(self.updateMRMLFromGUI)
- lowerHistogramLayout.addWidget(self.histogramLowerThresholdMinimumButton)
- self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdMinimumButton)
-
- self.histogramLowerThresholdLowerButton = qt.QToolButton()
- self.histogramLowerThresholdLowerButton.setText("Lower")
- self.histogramLowerThresholdLowerButton.setCheckable(True)
- self.histogramLowerThresholdLowerButton.clicked.connect(self.updateMRMLFromGUI)
- lowerHistogramLayout.addWidget(self.histogramLowerThresholdLowerButton)
- self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdLowerButton)
-
- self.histogramLowerThresholdAverageButton = qt.QToolButton()
- self.histogramLowerThresholdAverageButton.setText("Mean")
- self.histogramLowerThresholdAverageButton.setCheckable(True)
- self.histogramLowerThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI)
- lowerHistogramLayout.addWidget(self.histogramLowerThresholdAverageButton)
- self.histogramLowerMethodButtonGroup.addButton(self.histogramLowerThresholdAverageButton)
-
- ###
- # Upper histogram threshold buttons
-
- upperGroupBox = qt.QGroupBox("Upper")
- upperHistogramLayout = qt.QHBoxLayout()
- upperHistogramLayout.setContentsMargins(0, 3, 0, 3)
- upperGroupBox.setLayout(upperHistogramLayout)
- histogramItemFrame.addWidget(upperGroupBox)
- self.histogramUpperMethodButtonGroup = qt.QButtonGroup()
- self.histogramUpperMethodButtonGroup.setExclusive(True)
-
- self.histogramUpperThresholdAverageButton = qt.QToolButton()
- self.histogramUpperThresholdAverageButton.setText("Mean")
- self.histogramUpperThresholdAverageButton.setCheckable(True)
- self.histogramUpperThresholdAverageButton.clicked.connect(self.updateMRMLFromGUI)
- upperHistogramLayout.addWidget(self.histogramUpperThresholdAverageButton)
- self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdAverageButton)
-
- self.histogramUpperThresholdUpperButton = qt.QToolButton()
- self.histogramUpperThresholdUpperButton.setText("Upper")
- self.histogramUpperThresholdUpperButton.setCheckable(True)
- self.histogramUpperThresholdUpperButton.clicked.connect(self.updateMRMLFromGUI)
- upperHistogramLayout.addWidget(self.histogramUpperThresholdUpperButton)
- self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdUpperButton)
-
- self.histogramUpperThresholdMaximumButton = qt.QToolButton()
- self.histogramUpperThresholdMaximumButton.setText("Max")
- self.histogramUpperThresholdMaximumButton.setToolTip("Maximum")
- self.histogramUpperThresholdMaximumButton.setCheckable(True)
- self.histogramUpperThresholdMaximumButton.clicked.connect(self.updateMRMLFromGUI)
- upperHistogramLayout.addWidget(self.histogramUpperThresholdMaximumButton)
- self.histogramUpperMethodButtonGroup.addButton(self.histogramUpperThresholdMaximumButton)
-
- histogramGroupBox = ctk.ctkCollapsibleGroupBox()
- histogramGroupBox.setTitle("Local histogram")
- histogramGroupBox.setLayout(histogramFrame)
- histogramGroupBox.collapsed = True
- self.scriptedEffect.addOptionsWidget(histogramGroupBox)
-
- self.useForPaintButton = qt.QPushButton("Use for masking")
- self.useForPaintButton.setToolTip("Use specified intensity range for masking and switch to Paint effect.")
- self.scriptedEffect.addOptionsWidget(self.useForPaintButton)
-
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Fill selected segment in regions that are in the specified intensity range.")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
-
- self.useForPaintButton.connect('clicked()', self.onUseForPaint)
- self.thresholdSlider.connect('valuesChanged(double,double)', self.onThresholdValuesChanged)
- self.autoThresholdMethodSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod)
- self.autoThresholdModeSelectorComboBox.connect("activated(int)", self.onSelectedAutoThresholdMethod)
- self.selectPreviousAutoThresholdButton.connect('clicked()', self.onSelectPreviousAutoThresholdMethod)
- self.selectNextAutoThresholdButton.connect('clicked()', self.onSelectNextAutoThresholdMethod)
- self.setAutoThresholdButton.connect('clicked()', self.onAutoThreshold)
- self.applyButton.connect('clicked()', self.onApply)
-
- def masterVolumeNodeChanged(self):
- # Set scalar range of master volume image data to threshold slider
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- if masterImageData:
- lo, hi = masterImageData.GetScalarRange()
- self.thresholdSlider.setRange(lo, hi)
- self.thresholdSlider.singleStep = (hi - lo) / 1000.
- if (self.scriptedEffect.doubleParameter("MinimumThreshold") == self.scriptedEffect.doubleParameter("MaximumThreshold")):
- # has not been initialized yet
- self.scriptedEffect.setParameter("MinimumThreshold", lo + (hi - lo) * 0.25)
- self.scriptedEffect.setParameter("MaximumThreshold", hi)
-
- def layoutChanged(self):
- self.setupPreviewDisplay()
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("MinimumThreshold", 0.)
- self.scriptedEffect.setParameterDefault("MaximumThreshold", 0)
- self.scriptedEffect.setParameterDefault("AutoThresholdMethod", METHOD_OTSU)
- self.scriptedEffect.setParameterDefault("AutoThresholdMode", MODE_SET_LOWER_MAX)
- self.scriptedEffect.setParameterDefault(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, HISTOGRAM_BRUSH_TYPE_CIRCLE)
- self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_LOWER_PARAMETER_NAME, HISTOGRAM_SET_LOWER)
- self.scriptedEffect.setParameterDefault(HISTOGRAM_SET_UPPER_PARAMETER_NAME, HISTOGRAM_SET_UPPER)
-
- def updateGUIFromMRML(self):
- self.thresholdSlider.blockSignals(True)
- self.thresholdSlider.setMinimumValue(self.scriptedEffect.doubleParameter("MinimumThreshold"))
- self.thresholdSlider.setMaximumValue(self.scriptedEffect.doubleParameter("MaximumThreshold"))
- self.thresholdSlider.blockSignals(False)
-
- autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMethod"))
- wasBlocked = self.autoThresholdMethodSelectorComboBox.blockSignals(True)
- self.autoThresholdMethodSelectorComboBox.setCurrentIndex(autoThresholdMethod)
- self.autoThresholdMethodSelectorComboBox.blockSignals(wasBlocked)
-
- autoThresholdMode = self.autoThresholdModeSelectorComboBox.findData(self.scriptedEffect.parameter("AutoThresholdMode"))
- wasBlocked = self.autoThresholdModeSelectorComboBox.blockSignals(True)
- self.autoThresholdModeSelectorComboBox.setCurrentIndex(autoThresholdMode)
- self.autoThresholdModeSelectorComboBox.blockSignals(wasBlocked)
-
- histogramBrushType = self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME)
- self.boxROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_BOX)
- self.circleROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_CIRCLE)
- self.drawROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_DRAW)
- self.lineROIButton.checked = (histogramBrushType == HISTOGRAM_BRUSH_TYPE_LINE)
-
- histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
- self.histogramLowerThresholdMinimumButton.checked = (histogramSetModeLower == HISTOGRAM_SET_MINIMUM)
- self.histogramLowerThresholdLowerButton.checked = (histogramSetModeLower == HISTOGRAM_SET_LOWER)
- self.histogramLowerThresholdAverageButton.checked = (histogramSetModeLower == HISTOGRAM_SET_AVERAGE)
-
- histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
- self.histogramUpperThresholdAverageButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_AVERAGE)
- self.histogramUpperThresholdUpperButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_UPPER)
- self.histogramUpperThresholdMaximumButton.checked = (histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM)
-
- self.updateHistogramBackground()
-
- def updateMRMLFromGUI(self):
- with slicer.util.NodeModify(self.scriptedEffect.parameterSetNode()):
- self.scriptedEffect.setParameter("MinimumThreshold", self.thresholdSlider.minimumValue)
- self.scriptedEffect.setParameter("MaximumThreshold", self.thresholdSlider.maximumValue)
-
- methodIndex = self.autoThresholdMethodSelectorComboBox.currentIndex
- autoThresholdMethod = self.autoThresholdMethodSelectorComboBox.itemData(methodIndex)
- self.scriptedEffect.setParameter("AutoThresholdMethod", autoThresholdMethod)
-
- modeIndex = self.autoThresholdModeSelectorComboBox.currentIndex
- autoThresholdMode = self.autoThresholdModeSelectorComboBox.itemData(modeIndex)
- self.scriptedEffect.setParameter("AutoThresholdMode", autoThresholdMode)
-
- histogramParameterChanged = False
-
- histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
- if self.boxROIButton.checked:
- histogramBrushType = HISTOGRAM_BRUSH_TYPE_BOX
- elif self.circleROIButton.checked:
- histogramBrushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
- elif self.drawROIButton.checked:
- histogramBrushType = HISTOGRAM_BRUSH_TYPE_DRAW
- elif self.lineROIButton.checked:
- histogramBrushType = HISTOGRAM_BRUSH_TYPE_LINE
-
- if histogramBrushType != self.scriptedEffect.parameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME):
- self.scriptedEffect.setParameter(HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME, histogramBrushType)
- histogramParameterChanged = True
-
- histogramSetModeLower = HISTOGRAM_SET_LOWER
- if self.histogramLowerThresholdMinimumButton.checked:
- histogramSetModeLower = HISTOGRAM_SET_MINIMUM
- elif self.histogramLowerThresholdLowerButton.checked:
- histogramSetModeLower = HISTOGRAM_SET_LOWER
- elif self.histogramLowerThresholdAverageButton.checked:
- histogramSetModeLower = HISTOGRAM_SET_AVERAGE
- if histogramSetModeLower != self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME):
- self.scriptedEffect.setParameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME, histogramSetModeLower)
- histogramParameterChanged = True
-
- histogramSetModeUpper = HISTOGRAM_SET_UPPER
- if self.histogramUpperThresholdAverageButton.checked:
- histogramSetModeUpper = HISTOGRAM_SET_AVERAGE
- elif self.histogramUpperThresholdUpperButton.checked:
- histogramSetModeUpper = HISTOGRAM_SET_UPPER
- elif self.histogramUpperThresholdMaximumButton.checked:
- histogramSetModeUpper = HISTOGRAM_SET_MAXIMUM
- if histogramSetModeUpper != self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME):
- self.scriptedEffect.setParameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME, histogramSetModeUpper)
- histogramParameterChanged = True
-
- if histogramParameterChanged:
+ def onHistogramMouseMove(self, pos, button):
+ self.selectionEndPosition = pos
+ if (button == qt.Qt.RightButton):
+ return
self.updateHistogram()
- #
- # Effect specific methods (the above ones are the API methods to override)
- #
- def onThresholdValuesChanged(self, min, max):
- self.scriptedEffect.updateMRMLFromGUI()
-
- def onUseForPaint(self):
- parameterSetNode = self.scriptedEffect.parameterSetNode()
- parameterSetNode.MasterVolumeIntensityMaskOn()
- parameterSetNode.SetMasterVolumeIntensityMaskRange(self.thresholdSlider.minimumValue, self.thresholdSlider.maximumValue)
- # Switch to paint effect
- self.scriptedEffect.selectEffect("Paint")
-
- def onSelectPreviousAutoThresholdMethod(self):
- self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex - 1) \
- % self.autoThresholdMethodSelectorComboBox.count
- self.onSelectedAutoThresholdMethod()
-
- def onSelectNextAutoThresholdMethod(self):
- self.autoThresholdMethodSelectorComboBox.currentIndex = (self.autoThresholdMethodSelectorComboBox.currentIndex + 1) \
- % self.autoThresholdMethodSelectorComboBox.count
- self.onSelectedAutoThresholdMethod()
-
- def onSelectedAutoThresholdMethod(self):
- self.updateMRMLFromGUI()
- self.onAutoThreshold()
- self.updateGUIFromMRML()
-
- def onAutoThreshold(self):
- autoThresholdMethod = self.scriptedEffect.parameter("AutoThresholdMethod")
- autoThresholdMode = self.scriptedEffect.parameter("AutoThresholdMode")
- self.autoThreshold(autoThresholdMethod, autoThresholdMode)
-
- def autoThreshold(self, autoThresholdMethod, autoThresholdMode):
- if autoThresholdMethod == METHOD_HUANG:
- self.autoThresholdCalculator.SetMethodToHuang()
- elif autoThresholdMethod == METHOD_INTERMODES:
- self.autoThresholdCalculator.SetMethodToIntermodes()
- elif autoThresholdMethod == METHOD_ISO_DATA:
- self.autoThresholdCalculator.SetMethodToIsoData()
- elif autoThresholdMethod == METHOD_KITTLER_ILLINGWORTH:
- self.autoThresholdCalculator.SetMethodToKittlerIllingworth()
- elif autoThresholdMethod == METHOD_LI:
- self.autoThresholdCalculator.SetMethodToLi()
- elif autoThresholdMethod == METHOD_MAXIMUM_ENTROPY:
- self.autoThresholdCalculator.SetMethodToMaximumEntropy()
- elif autoThresholdMethod == METHOD_MOMENTS:
- self.autoThresholdCalculator.SetMethodToMoments()
- elif autoThresholdMethod == METHOD_OTSU:
- self.autoThresholdCalculator.SetMethodToOtsu()
- elif autoThresholdMethod == METHOD_RENYI_ENTROPY:
- self.autoThresholdCalculator.SetMethodToRenyiEntropy()
- elif autoThresholdMethod == METHOD_SHANBHAG:
- self.autoThresholdCalculator.SetMethodToShanbhag()
- elif autoThresholdMethod == METHOD_TRIANGLE:
- self.autoThresholdCalculator.SetMethodToTriangle()
- elif autoThresholdMethod == METHOD_YEN:
- self.autoThresholdCalculator.SetMethodToYen()
- else:
- logging.error(f"Unknown AutoThresholdMethod {autoThresholdMethod}")
-
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- self.autoThresholdCalculator.SetInputData(masterImageData)
-
- self.autoThresholdCalculator.Update()
- computedThreshold = self.autoThresholdCalculator.GetThreshold()
-
- masterVolumeMin, masterVolumeMax = masterImageData.GetScalarRange()
-
- if autoThresholdMode == MODE_SET_UPPER:
- self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
- elif autoThresholdMode == MODE_SET_LOWER:
- self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
- elif autoThresholdMode == MODE_SET_MIN_UPPER:
- self.scriptedEffect.setParameter("MinimumThreshold", masterVolumeMin)
- self.scriptedEffect.setParameter("MaximumThreshold", computedThreshold)
- elif autoThresholdMode == MODE_SET_LOWER_MAX:
- self.scriptedEffect.setParameter("MinimumThreshold", computedThreshold)
- self.scriptedEffect.setParameter("MaximumThreshold", masterVolumeMax)
- else:
- logging.error(f"Unknown AutoThresholdMode {autoThresholdMode}")
-
- def onApply(self):
- if not self.scriptedEffect.confirmCurrentSegmentVisible():
- return
-
- try:
- # Get master volume image data
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- # Get modifier labelmap
- modifierLabelmap = self.scriptedEffect.defaultModifierLabelmap()
- originalImageToWorldMatrix = vtk.vtkMatrix4x4()
- modifierLabelmap.GetImageToWorldMatrix(originalImageToWorldMatrix)
- # Get parameters
- min = self.scriptedEffect.doubleParameter("MinimumThreshold")
- max = self.scriptedEffect.doubleParameter("MaximumThreshold")
-
- self.scriptedEffect.saveStateForUndo()
-
- # Perform thresholding
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(masterImageData)
- thresh.ThresholdBetween(min, max)
- thresh.SetInValue(1)
- thresh.SetOutValue(0)
- thresh.SetOutputScalarType(modifierLabelmap.GetScalarType())
- thresh.Update()
- modifierLabelmap.DeepCopy(thresh.GetOutput())
- except IndexError:
- logging.error('apply: Failed to threshold master volume!')
- pass
-
- # Apply changes
- self.scriptedEffect.modifySelectedSegmentByLabelmap(modifierLabelmap, slicer.qSlicerSegmentEditorAbstractEffect.ModificationModeSet)
-
- # De-select effect
- self.scriptedEffect.selectEffect("")
-
- def clearPreviewDisplay(self):
- for sliceWidget, pipeline in self.previewPipelines.items():
- self.scriptedEffect.removeActor2D(sliceWidget, pipeline.actor)
- self.previewPipelines = {}
-
- def clearHistogramDisplay(self):
- if self.histogramPipeline is None:
- return
- self.histogramPipeline.removeActors()
- self.histogramPipeline = None
-
- def setupPreviewDisplay(self):
- # Clear previous pipelines before setting up the new ones
- self.clearPreviewDisplay()
-
- layoutManager = slicer.app.layoutManager()
- if layoutManager is None:
- return
-
- # Add a pipeline for each 2D slice view
- for sliceViewName in layoutManager.sliceViewNames():
- sliceWidget = layoutManager.sliceWidget(sliceViewName)
- if not self.scriptedEffect.segmentationDisplayableInView(sliceWidget.mrmlSliceNode()):
- continue
- renderer = self.scriptedEffect.renderer(sliceWidget)
- if renderer is None:
- logging.error("setupPreviewDisplay: Failed to get renderer!")
- continue
-
- # Create pipeline
- pipeline = PreviewPipeline()
- self.previewPipelines[sliceWidget] = pipeline
-
- # Add actor
- self.scriptedEffect.addActor2D(sliceWidget, pipeline.actor)
-
- def preview(self):
-
- opacity = 0.5 + self.previewState / (2. * self.previewSteps)
- min = self.scriptedEffect.doubleParameter("MinimumThreshold")
- max = self.scriptedEffect.doubleParameter("MaximumThreshold")
-
- # Get color of edited segment
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- if not segmentationNode:
- # scene was closed while preview was active
- return
- displayNode = segmentationNode.GetDisplayNode()
- if displayNode is None:
- logging.error("preview: Invalid segmentation display node!")
- color = [0.5, 0.5, 0.5]
- segmentID = self.scriptedEffect.parameterSetNode().GetSelectedSegmentID()
-
- # Make sure we keep the currently selected segment hidden (the user may have changed selection)
- if segmentID != self.previewedSegmentID:
- self.setCurrentSegmentTransparent()
-
- r, g, b = segmentationNode.GetSegmentation().GetSegment(segmentID).GetColor()
-
- # Set values to pipelines
- for sliceWidget in self.previewPipelines:
- pipeline = self.previewPipelines[sliceWidget]
- pipeline.lookupTable.SetTableValue(1, r, g, b, opacity)
- layerLogic = self.getMasterVolumeLayerLogic(sliceWidget)
- pipeline.thresholdFilter.SetInputConnection(layerLogic.GetReslice().GetOutputPort())
- pipeline.thresholdFilter.ThresholdBetween(min, max)
- pipeline.actor.VisibilityOn()
- sliceWidget.sliceView().scheduleRender()
-
- self.previewState += self.previewStep
- if self.previewState >= self.previewSteps:
- self.previewStep = -1
- if self.previewState <= 0:
- self.previewStep = 1
-
- def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
- abortEvent = False
-
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- if masterImageData is None:
- return abortEvent
-
- # Only allow for slice views
- if viewWidget.className() != "qMRMLSliceWidget":
- return abortEvent
-
- anyModifierKeyPressed = callerInteractor.GetShiftKey() or callerInteractor.GetControlKey() or callerInteractor.GetAltKey()
-
- # Clicking in a view should remove all previous pipelines
- if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
- self.clearHistogramDisplay()
-
- if self.histogramPipeline is None:
- self.createHistogramPipeline(viewWidget)
-
- xy = callerInteractor.GetEventPosition()
- ras = self.xyToRas(xy, viewWidget)
-
- if eventId == vtk.vtkCommand.LeftButtonPressEvent and not anyModifierKeyPressed:
- self.histogramPipeline.state = HISTOGRAM_STATE_MOVING
- self.histogramPipeline.addPoint(ras)
- self.updateHistogram()
- abortEvent = True
- elif eventId == vtk.vtkCommand.LeftButtonReleaseEvent:
- if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
- self.histogramPipeline.state = HISTOGRAM_STATE_PLACED
- abortEvent = True
- elif eventId == vtk.vtkCommand.MouseMoveEvent:
- if self.histogramPipeline.state == HISTOGRAM_STATE_MOVING:
- self.histogramPipeline.addPoint(ras)
+ def onHistogramMouseRelease(self, pos, button):
+ self.selectionEndPosition = pos
+ if (button == qt.Qt.RightButton):
+ return
self.updateHistogram()
- return abortEvent
-
- def createHistogramPipeline(self, sliceWidget):
- brushType = HISTOGRAM_BRUSH_TYPE_CIRCLE
- if self.boxROIButton.checked:
- brushType = HISTOGRAM_BRUSH_TYPE_BOX
- elif self.drawROIButton.checked:
- brushType = HISTOGRAM_BRUSH_TYPE_DRAW
- elif self.lineROIButton.checked:
- brushType = HISTOGRAM_BRUSH_TYPE_LINE
- pipeline = HistogramPipeline(self, self.scriptedEffect, sliceWidget, brushType)
- self.histogramPipeline = pipeline
-
- def processViewNodeEvents(self, callerViewNode, eventId, viewWidget):
- if self.histogramPipeline is not None:
- self.histogramPipeline.updateBrushModel()
-
- def onHistogramMouseClick(self, pos, button):
- self.selectionStartPosition = pos
- self.selectionEndPosition = pos
- if (button == qt.Qt.RightButton):
- self.selectionStartPosition = None
- self.selectionEndPosition = None
- self.minMaxFunction.RemoveAllPoints()
- self.averageFunction.RemoveAllPoints()
- self.updateHistogram()
-
- def onHistogramMouseMove(self, pos, button):
- self.selectionEndPosition = pos
- if (button == qt.Qt.RightButton):
- return
- self.updateHistogram()
-
- def onHistogramMouseRelease(self, pos, button):
- self.selectionEndPosition = pos
- if (button == qt.Qt.RightButton):
- return
- self.updateHistogram()
-
- def getMasterVolumeLayerLogic(self, sliceWidget):
- masterVolumeNode = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
- sliceLogic = sliceWidget.sliceLogic()
-
- backgroundLogic = sliceLogic.GetBackgroundLayer()
- backgroundVolumeNode = backgroundLogic.GetVolumeNode()
- if masterVolumeNode == backgroundVolumeNode:
- return backgroundLogic
-
- foregroundLogic = sliceLogic.GetForegroundLayer()
- foregroundVolumeNode = foregroundLogic.GetVolumeNode()
- if masterVolumeNode == foregroundVolumeNode:
- return foregroundLogic
-
- logging.warning("Master volume is not set as either the foreground or background")
-
- foregroundOpacity = 0.0
- if foregroundVolumeNode:
- compositeNode = sliceLogic.GetSliceCompositeNode()
- foregroundOpacity = compositeNode.GetForegroundOpacity()
-
- if foregroundOpacity > 0.5:
- return foregroundLogic
-
- return backgroundLogic
-
- def updateHistogram(self):
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- if masterImageData is None or self.histogramPipeline is None:
- self.histogramFunction.RemoveAllPoints()
- return
-
- # Ensure that the brush is in the correct location
- self.histogramPipeline.updateBrushModel()
-
- self.stencil.SetInputConnection(self.histogramPipeline.worldToSliceTransformer.GetOutputPort())
-
- self.histogramPipeline.worldToSliceTransformer.Update()
- brushPolydata = self.histogramPipeline.worldToSliceTransformer.GetOutput()
- brushBounds = brushPolydata.GetBounds()
- brushExtent = [0, -1, 0, -1, 0, -1]
- for i in range(3):
- brushExtent[2 * i] = vtk.vtkMath.Floor(brushBounds[2 * i])
- brushExtent[2 * i + 1] = vtk.vtkMath.Ceil(brushBounds[2 * i + 1])
- if brushExtent[0] > brushExtent[1] or brushExtent[2] > brushExtent[3] or brushExtent[4] > brushExtent[5]:
- self.histogramFunction.RemoveAllPoints()
- return
-
- layerLogic = self.getMasterVolumeLayerLogic(self.histogramPipeline.sliceWidget)
- self.reslice.SetInputConnection(layerLogic.GetReslice().GetInputConnection(0, 0))
- self.reslice.SetResliceTransform(layerLogic.GetReslice().GetResliceTransform())
- self.reslice.SetInterpolationMode(layerLogic.GetReslice().GetInterpolationMode())
- self.reslice.SetOutputExtent(brushExtent)
-
- maxNumberOfBins = 1000
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- scalarRange = masterImageData.GetScalarRange()
- scalarType = masterImageData.GetScalarType()
- if scalarType == vtk.VTK_FLOAT or scalarType == vtk.VTK_DOUBLE:
- numberOfBins = maxNumberOfBins
- else:
- numberOfBins = int(scalarRange[1] - scalarRange[0]) + 1
- if numberOfBins > maxNumberOfBins:
- numberOfBins = maxNumberOfBins
- binSpacing = (scalarRange[1] - scalarRange[0] + 1) / numberOfBins
-
- self.imageAccumulate.SetComponentExtent(0, numberOfBins - 1, 0, 0, 0, 0)
- self.imageAccumulate.SetComponentSpacing(binSpacing, binSpacing, binSpacing)
- self.imageAccumulate.SetComponentOrigin(scalarRange[0], scalarRange[0], scalarRange[0])
-
- self.imageAccumulate.Update()
-
- self.histogramFunction.RemoveAllPoints()
- tableSize = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetNumberOfTuples()
- for i in range(tableSize):
- value = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetTuple1(i)
- self.histogramFunction.AddPoint(binSpacing * i + scalarRange[0], value)
- self.histogramFunction.AdjustRange(scalarRange)
-
- lower = self.imageAccumulate.GetMin()[0]
- average = self.imageAccumulate.GetMean()[0]
- upper = self.imageAccumulate.GetMax()[0]
-
- # If there is a selection, then set the threshold based on that
- if self.selectionStartPosition is not None and self.selectionEndPosition is not None:
-
- # Clamp selection based on scalar range
- startX = min(scalarRange[1], max(scalarRange[0], self.selectionStartPosition[0]))
- endX = min(scalarRange[1], max(scalarRange[0], self.selectionEndPosition[0]))
-
- lower = min(startX, endX)
- average = (startX + endX) / 2.0
- upper = max(startX, endX)
-
- epsilon = 0.00001
- self.minMaxFunction.RemoveAllPoints()
- self.minMaxFunction.AddPoint(lower - epsilon, 0.0)
- self.minMaxFunction.AddPoint(lower, 1.0)
- self.minMaxFunction.AddPoint(lower + epsilon, 0.0)
- self.minMaxFunction.AddPoint(upper - epsilon, 0.0)
- self.minMaxFunction.AddPoint(upper, 1.0)
- self.minMaxFunction.AddPoint(upper + epsilon, 0.0)
- self.minMaxFunction.AdjustRange(scalarRange)
-
- self.averageFunction.RemoveAllPoints()
- self.averageFunction.AddPoint(average - epsilon, 0.0)
- self.averageFunction.AddPoint(average, 1.0)
- self.averageFunction.AddPoint(average + epsilon, 0.0)
- self.averageFunction.AdjustRange(scalarRange)
-
- minimumThreshold = lower
- maximumThreshold = upper
-
- histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
- if histogramSetModeLower == HISTOGRAM_SET_MINIMUM:
- minimumThreshold = scalarRange[0]
- elif histogramSetModeLower == HISTOGRAM_SET_LOWER:
- minimumThreshold = lower
- elif histogramSetModeLower == HISTOGRAM_SET_AVERAGE:
- minimumThreshold = average
-
- histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
- if histogramSetModeUpper == HISTOGRAM_SET_AVERAGE:
- maximumThreshold = average
- elif histogramSetModeUpper == HISTOGRAM_SET_UPPER:
- maximumThreshold = upper
- elif histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM:
- maximumThreshold = scalarRange[1]
-
- self.scriptedEffect.setParameter("MinimumThreshold", minimumThreshold)
- self.scriptedEffect.setParameter("MaximumThreshold", maximumThreshold)
-
- def updateHistogramBackground(self):
- self.backgroundFunction.RemoveAllPoints()
-
- masterImageData = self.scriptedEffect.masterVolumeImageData()
- if masterImageData is None:
- return
-
- scalarRange = masterImageData.GetScalarRange()
-
- epsilon = 0.00001
- low = self.scriptedEffect.doubleParameter("MinimumThreshold")
- upper = self.scriptedEffect.doubleParameter("MaximumThreshold")
- low = max(scalarRange[0] + epsilon, low)
- upper = min(scalarRange[1] - epsilon, upper)
-
- self.backgroundFunction.AddRGBPoint(scalarRange[0], 1, 1, 1)
- self.backgroundFunction.AddRGBPoint(low - epsilon, 1, 1, 1)
- self.backgroundFunction.AddRGBPoint(low, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
- self.backgroundFunction.AddRGBPoint(upper, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
- self.backgroundFunction.AddRGBPoint(upper + epsilon, 1, 1, 1)
- self.backgroundFunction.AddRGBPoint(scalarRange[1], 1, 1, 1)
- self.backgroundFunction.SetAlpha(1.0)
- self.backgroundFunction.Build()
+
+ def getMasterVolumeLayerLogic(self, sliceWidget):
+ masterVolumeNode = self.scriptedEffect.parameterSetNode().GetMasterVolumeNode()
+ sliceLogic = sliceWidget.sliceLogic()
+
+ backgroundLogic = sliceLogic.GetBackgroundLayer()
+ backgroundVolumeNode = backgroundLogic.GetVolumeNode()
+ if masterVolumeNode == backgroundVolumeNode:
+ return backgroundLogic
+
+ foregroundLogic = sliceLogic.GetForegroundLayer()
+ foregroundVolumeNode = foregroundLogic.GetVolumeNode()
+ if masterVolumeNode == foregroundVolumeNode:
+ return foregroundLogic
+
+ logging.warning("Master volume is not set as either the foreground or background")
+
+ foregroundOpacity = 0.0
+ if foregroundVolumeNode:
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ foregroundOpacity = compositeNode.GetForegroundOpacity()
+
+ if foregroundOpacity > 0.5:
+ return foregroundLogic
+
+ return backgroundLogic
+
+ def updateHistogram(self):
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ if masterImageData is None or self.histogramPipeline is None:
+ self.histogramFunction.RemoveAllPoints()
+ return
+
+ # Ensure that the brush is in the correct location
+ self.histogramPipeline.updateBrushModel()
+
+ self.stencil.SetInputConnection(self.histogramPipeline.worldToSliceTransformer.GetOutputPort())
+
+ self.histogramPipeline.worldToSliceTransformer.Update()
+ brushPolydata = self.histogramPipeline.worldToSliceTransformer.GetOutput()
+ brushBounds = brushPolydata.GetBounds()
+ brushExtent = [0, -1, 0, -1, 0, -1]
+ for i in range(3):
+ brushExtent[2 * i] = vtk.vtkMath.Floor(brushBounds[2 * i])
+ brushExtent[2 * i + 1] = vtk.vtkMath.Ceil(brushBounds[2 * i + 1])
+ if brushExtent[0] > brushExtent[1] or brushExtent[2] > brushExtent[3] or brushExtent[4] > brushExtent[5]:
+ self.histogramFunction.RemoveAllPoints()
+ return
+
+ layerLogic = self.getMasterVolumeLayerLogic(self.histogramPipeline.sliceWidget)
+ self.reslice.SetInputConnection(layerLogic.GetReslice().GetInputConnection(0, 0))
+ self.reslice.SetResliceTransform(layerLogic.GetReslice().GetResliceTransform())
+ self.reslice.SetInterpolationMode(layerLogic.GetReslice().GetInterpolationMode())
+ self.reslice.SetOutputExtent(brushExtent)
+
+ maxNumberOfBins = 1000
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ scalarRange = masterImageData.GetScalarRange()
+ scalarType = masterImageData.GetScalarType()
+ if scalarType == vtk.VTK_FLOAT or scalarType == vtk.VTK_DOUBLE:
+ numberOfBins = maxNumberOfBins
+ else:
+ numberOfBins = int(scalarRange[1] - scalarRange[0]) + 1
+ if numberOfBins > maxNumberOfBins:
+ numberOfBins = maxNumberOfBins
+ binSpacing = (scalarRange[1] - scalarRange[0] + 1) / numberOfBins
+
+ self.imageAccumulate.SetComponentExtent(0, numberOfBins - 1, 0, 0, 0, 0)
+ self.imageAccumulate.SetComponentSpacing(binSpacing, binSpacing, binSpacing)
+ self.imageAccumulate.SetComponentOrigin(scalarRange[0], scalarRange[0], scalarRange[0])
+
+ self.imageAccumulate.Update()
+
+ self.histogramFunction.RemoveAllPoints()
+ tableSize = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetNumberOfTuples()
+ for i in range(tableSize):
+ value = self.imageAccumulate.GetOutput().GetPointData().GetScalars().GetTuple1(i)
+ self.histogramFunction.AddPoint(binSpacing * i + scalarRange[0], value)
+ self.histogramFunction.AdjustRange(scalarRange)
+
+ lower = self.imageAccumulate.GetMin()[0]
+ average = self.imageAccumulate.GetMean()[0]
+ upper = self.imageAccumulate.GetMax()[0]
+
+ # If there is a selection, then set the threshold based on that
+ if self.selectionStartPosition is not None and self.selectionEndPosition is not None:
+
+ # Clamp selection based on scalar range
+ startX = min(scalarRange[1], max(scalarRange[0], self.selectionStartPosition[0]))
+ endX = min(scalarRange[1], max(scalarRange[0], self.selectionEndPosition[0]))
+
+ lower = min(startX, endX)
+ average = (startX + endX) / 2.0
+ upper = max(startX, endX)
+
+ epsilon = 0.00001
+ self.minMaxFunction.RemoveAllPoints()
+ self.minMaxFunction.AddPoint(lower - epsilon, 0.0)
+ self.minMaxFunction.AddPoint(lower, 1.0)
+ self.minMaxFunction.AddPoint(lower + epsilon, 0.0)
+ self.minMaxFunction.AddPoint(upper - epsilon, 0.0)
+ self.minMaxFunction.AddPoint(upper, 1.0)
+ self.minMaxFunction.AddPoint(upper + epsilon, 0.0)
+ self.minMaxFunction.AdjustRange(scalarRange)
+
+ self.averageFunction.RemoveAllPoints()
+ self.averageFunction.AddPoint(average - epsilon, 0.0)
+ self.averageFunction.AddPoint(average, 1.0)
+ self.averageFunction.AddPoint(average + epsilon, 0.0)
+ self.averageFunction.AdjustRange(scalarRange)
+
+ minimumThreshold = lower
+ maximumThreshold = upper
+
+ histogramSetModeLower = self.scriptedEffect.parameter(HISTOGRAM_SET_LOWER_PARAMETER_NAME)
+ if histogramSetModeLower == HISTOGRAM_SET_MINIMUM:
+ minimumThreshold = scalarRange[0]
+ elif histogramSetModeLower == HISTOGRAM_SET_LOWER:
+ minimumThreshold = lower
+ elif histogramSetModeLower == HISTOGRAM_SET_AVERAGE:
+ minimumThreshold = average
+
+ histogramSetModeUpper = self.scriptedEffect.parameter(HISTOGRAM_SET_UPPER_PARAMETER_NAME)
+ if histogramSetModeUpper == HISTOGRAM_SET_AVERAGE:
+ maximumThreshold = average
+ elif histogramSetModeUpper == HISTOGRAM_SET_UPPER:
+ maximumThreshold = upper
+ elif histogramSetModeUpper == HISTOGRAM_SET_MAXIMUM:
+ maximumThreshold = scalarRange[1]
+
+ self.scriptedEffect.setParameter("MinimumThreshold", minimumThreshold)
+ self.scriptedEffect.setParameter("MaximumThreshold", maximumThreshold)
+
+ def updateHistogramBackground(self):
+ self.backgroundFunction.RemoveAllPoints()
+
+ masterImageData = self.scriptedEffect.masterVolumeImageData()
+ if masterImageData is None:
+ return
+
+ scalarRange = masterImageData.GetScalarRange()
+
+ epsilon = 0.00001
+ low = self.scriptedEffect.doubleParameter("MinimumThreshold")
+ upper = self.scriptedEffect.doubleParameter("MaximumThreshold")
+ low = max(scalarRange[0] + epsilon, low)
+ upper = min(scalarRange[1] - epsilon, upper)
+
+ self.backgroundFunction.AddRGBPoint(scalarRange[0], 1, 1, 1)
+ self.backgroundFunction.AddRGBPoint(low - epsilon, 1, 1, 1)
+ self.backgroundFunction.AddRGBPoint(low, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
+ self.backgroundFunction.AddRGBPoint(upper, self.backgroundColor[0], self.backgroundColor[1], self.backgroundColor[2])
+ self.backgroundFunction.AddRGBPoint(upper + epsilon, 1, 1, 1)
+ self.backgroundFunction.AddRGBPoint(scalarRange[1], 1, 1, 1)
+ self.backgroundFunction.SetAlpha(1.0)
+ self.backgroundFunction.Build()
#
# PreviewPipeline
#
class PreviewPipeline:
- """ Visualization objects and pipeline for each slice view for threshold preview
- """
-
- def __init__(self):
- self.lookupTable = vtk.vtkLookupTable()
- self.lookupTable.SetRampToLinear()
- self.lookupTable.SetNumberOfTableValues(2)
- self.lookupTable.SetTableRange(0, 1)
- self.lookupTable.SetTableValue(0, 0, 0, 0, 0)
- self.colorMapper = vtk.vtkImageMapToRGBA()
- self.colorMapper.SetOutputFormatToRGBA()
- self.colorMapper.SetLookupTable(self.lookupTable)
- self.thresholdFilter = vtk.vtkImageThreshold()
- self.thresholdFilter.SetInValue(1)
- self.thresholdFilter.SetOutValue(0)
- self.thresholdFilter.SetOutputScalarTypeToUnsignedChar()
-
- # Feedback actor
- self.mapper = vtk.vtkImageMapper()
- self.dummyImage = vtk.vtkImageData()
- self.dummyImage.AllocateScalars(vtk.VTK_UNSIGNED_INT, 1)
- self.mapper.SetInputData(self.dummyImage)
- self.actor = vtk.vtkActor2D()
- self.actor.VisibilityOff()
- self.actor.SetMapper(self.mapper)
- self.mapper.SetColorWindow(255)
- self.mapper.SetColorLevel(128)
-
- # Setup pipeline
- self.colorMapper.SetInputConnection(self.thresholdFilter.GetOutputPort())
- self.mapper.SetInputConnection(self.colorMapper.GetOutputPort())
+ """ Visualization objects and pipeline for each slice view for threshold preview
+ """
+
+ def __init__(self):
+ self.lookupTable = vtk.vtkLookupTable()
+ self.lookupTable.SetRampToLinear()
+ self.lookupTable.SetNumberOfTableValues(2)
+ self.lookupTable.SetTableRange(0, 1)
+ self.lookupTable.SetTableValue(0, 0, 0, 0, 0)
+ self.colorMapper = vtk.vtkImageMapToRGBA()
+ self.colorMapper.SetOutputFormatToRGBA()
+ self.colorMapper.SetLookupTable(self.lookupTable)
+ self.thresholdFilter = vtk.vtkImageThreshold()
+ self.thresholdFilter.SetInValue(1)
+ self.thresholdFilter.SetOutValue(0)
+ self.thresholdFilter.SetOutputScalarTypeToUnsignedChar()
+
+ # Feedback actor
+ self.mapper = vtk.vtkImageMapper()
+ self.dummyImage = vtk.vtkImageData()
+ self.dummyImage.AllocateScalars(vtk.VTK_UNSIGNED_INT, 1)
+ self.mapper.SetInputData(self.dummyImage)
+ self.actor = vtk.vtkActor2D()
+ self.actor.VisibilityOff()
+ self.actor.SetMapper(self.mapper)
+ self.mapper.SetColorWindow(255)
+ self.mapper.SetColorLevel(128)
+
+ # Setup pipeline
+ self.colorMapper.SetInputConnection(self.thresholdFilter.GetOutputPort())
+ self.mapper.SetInputConnection(self.colorMapper.GetOutputPort())
###
@@ -973,238 +973,238 @@ def __init__(self):
# Histogram threshold
#
class HistogramEventFilter(qt.QObject):
- thresholdEffect = None
+ thresholdEffect = None
- def setThresholdEffect(self, thresholdEffect):
- self.thresholdEffect = thresholdEffect
+ def setThresholdEffect(self, thresholdEffect):
+ self.thresholdEffect = thresholdEffect
- def eventFilter(self, object, event):
- if self.thresholdEffect is None:
- return
+ def eventFilter(self, object, event):
+ if self.thresholdEffect is None:
+ return
- if (event.type() == qt.QEvent.GraphicsSceneMousePress or
- event.type() == qt.QEvent.GraphicsSceneMouseMove or
- event.type() == qt.QEvent.GraphicsSceneMouseRelease):
- transferFunction = object.transferFunction()
- if transferFunction is None:
- return
+ if (event.type() == qt.QEvent.GraphicsSceneMousePress or
+ event.type() == qt.QEvent.GraphicsSceneMouseMove or
+ event.type() == qt.QEvent.GraphicsSceneMouseRelease):
+ transferFunction = object.transferFunction()
+ if transferFunction is None:
+ return
- representation = transferFunction.representation()
- x = representation.mapXFromScene(event.pos().x())
- y = representation.mapYFromScene(event.pos().y())
- position = (x, y)
+ representation = transferFunction.representation()
+ x = representation.mapXFromScene(event.pos().x())
+ y = representation.mapYFromScene(event.pos().y())
+ position = (x, y)
- if event.type() == qt.QEvent.GraphicsSceneMousePress:
- self.thresholdEffect.onHistogramMouseClick(position, event.button())
- elif event.type() == qt.QEvent.GraphicsSceneMouseMove:
- self.thresholdEffect.onHistogramMouseMove(position, event.button())
- elif event.type() == qt.QEvent.GraphicsSceneMouseRelease:
- self.thresholdEffect.onHistogramMouseRelease(position, event.button())
- return True
- return False
+ if event.type() == qt.QEvent.GraphicsSceneMousePress:
+ self.thresholdEffect.onHistogramMouseClick(position, event.button())
+ elif event.type() == qt.QEvent.GraphicsSceneMouseMove:
+ self.thresholdEffect.onHistogramMouseMove(position, event.button())
+ elif event.type() == qt.QEvent.GraphicsSceneMouseRelease:
+ self.thresholdEffect.onHistogramMouseRelease(position, event.button())
+ return True
+ return False
class HistogramPipeline:
- def __init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode):
- self.thresholdEffect = thresholdEffect
- self.scriptedEffect = scriptedEffect
- self.sliceWidget = sliceWidget
- self.brushMode = brushMode
- self.state = HISTOGRAM_STATE_OFF
-
- self.point1 = None
- self.point2 = None
-
- # Actor setup
- self.brushCylinderSource = vtk.vtkCylinderSource()
- self.brushCylinderSource.SetResolution(32)
-
- self.brushCubeSource = vtk.vtkCubeSource()
-
- self.brushLineSource = vtk.vtkLineSource()
- self.brushTubeSource = vtk.vtkTubeFilter()
- self.brushTubeSource.SetInputConnection(self.brushLineSource.GetOutputPort())
- self.brushTubeSource.SetNumberOfSides(50)
- self.brushTubeSource.SetCapping(True)
-
- self.brushToWorldOriginTransform = vtk.vtkTransform()
- self.brushToWorldOriginTransformer = vtk.vtkTransformPolyDataFilter()
- self.brushToWorldOriginTransformer.SetTransform(self.brushToWorldOriginTransform)
- self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
-
- self.normalFilter = vtk.vtkPolyDataNormals()
- self.normalFilter.AutoOrientNormalsOn()
- self.normalFilter.SetInputConnection(self.brushToWorldOriginTransformer.GetOutputPort())
-
- # Brush to RAS transform
- self.worldOriginToWorldTransform = vtk.vtkTransform()
- self.worldOriginToWorldTransformer = vtk.vtkTransformPolyDataFilter()
- self.worldOriginToWorldTransformer.SetTransform(self.worldOriginToWorldTransform)
- self.worldOriginToWorldTransformer.SetInputConnection(self.normalFilter.GetOutputPort())
-
- # RAS to XY transform
- self.worldToSliceTransform = vtk.vtkTransform()
- self.worldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
- self.worldToSliceTransformer.SetTransform(self.worldToSliceTransform)
- self.worldToSliceTransformer.SetInputConnection(self.worldOriginToWorldTransformer.GetOutputPort())
-
- # Cutting takes place in XY coordinates
- self.slicePlane = vtk.vtkPlane()
- self.slicePlane.SetNormal(0, 0, 1)
- self.slicePlane.SetOrigin(0, 0, 0)
- self.cutter = vtk.vtkCutter()
- self.cutter.SetCutFunction(self.slicePlane)
- self.cutter.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
-
- self.rasPoints = vtk.vtkPoints()
- lines = vtk.vtkCellArray()
- self.polyData = vtk.vtkPolyData()
- self.polyData.SetPoints(self.rasPoints)
- self.polyData.SetLines(lines)
-
- # Thin line
- self.thinRASPoints = vtk.vtkPoints()
- thinLines = vtk.vtkCellArray()
- self.thinPolyData = vtk.vtkPolyData()
- self.thinPolyData.SetPoints(self.rasPoints)
- self.thinPolyData.SetLines(thinLines)
-
- self.mapper = vtk.vtkPolyDataMapper2D()
- self.mapper.SetInputConnection(self.cutter.GetOutputPort())
-
- # Add actor
- self.actor = vtk.vtkActor2D()
- self.actor.SetMapper(self.mapper)
- actorProperty = self.actor.GetProperty()
- actorProperty.SetColor(1, 1, 0)
- actorProperty.SetLineWidth(2)
- renderer = self.scriptedEffect.renderer(sliceWidget)
- if renderer is None:
- logging.error("pipelineForWidget: Failed to get renderer!")
- return None
- self.scriptedEffect.addActor2D(sliceWidget, self.actor)
-
- self.thinActor = None
- if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
- self.worldToSliceTransformer.SetInputData(self.polyData)
- self.mapper.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
-
- self.thinWorldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
- self.thinWorldToSliceTransformer.SetInputData(self.thinPolyData)
- self.thinWorldToSliceTransformer.SetTransform(self.worldToSliceTransform)
-
- self.thinMapper = vtk.vtkPolyDataMapper2D()
- self.thinMapper.SetInputConnection(self.thinWorldToSliceTransformer.GetOutputPort())
-
- self.thinActor = vtk.vtkActor2D()
- self.thinActor.SetMapper(self.thinMapper)
- thinActorProperty = self.thinActor.GetProperty()
- thinActorProperty.SetColor(1, 1, 0)
- thinActorProperty.SetLineWidth(1)
- self.scriptedEffect.addActor2D(sliceWidget, self.thinActor)
- elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
- self.worldToSliceTransformer.SetInputConnection(self.brushTubeSource.GetOutputPort())
-
- def removeActors(self):
- if self.actor is not None:
- self.scriptedEffect.removeActor2D(self.sliceWidget, self.actor)
- if self.thinActor is not None:
- self.scriptedEffect.removeActor2D(self.sliceWidget, self.thinActor)
-
- def setPoint1(self, ras):
- self.point1 = ras
- self.updateBrushModel()
-
- def setPoint2(self, ras):
- self.point2 = ras
- self.updateBrushModel()
-
- def addPoint(self, ras):
- if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
- newPointIndex = self.rasPoints.InsertNextPoint(ras)
- previousPointIndex = newPointIndex - 1
- if (previousPointIndex >= 0):
- idList = vtk.vtkIdList()
- idList.InsertNextId(previousPointIndex)
- idList.InsertNextId(newPointIndex)
- self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
-
- thinLines = self.thinPolyData.GetLines()
- thinLines.Initialize()
- idList = vtk.vtkIdList()
- idList.InsertNextId(newPointIndex)
- idList.InsertNextId(0)
- self.thinPolyData.InsertNextCell(vtk.VTK_LINE, idList)
-
- else:
- if self.point1 is None:
- self.setPoint1(ras)
- self.setPoint2(ras)
-
- def updateBrushModel(self):
- if self.brushMode != HISTOGRAM_BRUSH_TYPE_DRAW and (self.point1 is None or self.point2 is None):
- return
-
- # Update slice cutting plane position and orientation
- sliceXyToRas = self.sliceWidget.sliceLogic().GetSliceNode().GetXYToRAS()
- rasToSliceXy = vtk.vtkMatrix4x4()
- vtk.vtkMatrix4x4.Invert(sliceXyToRas, rasToSliceXy)
- self.worldToSliceTransform.SetMatrix(rasToSliceXy)
-
- # brush is rotated to the slice widget plane
- brushToWorldOriginTransformMatrix = vtk.vtkMatrix4x4()
- brushToWorldOriginTransformMatrix.DeepCopy(self.sliceWidget.sliceLogic().GetSliceNode().GetSliceToRAS())
- brushToWorldOriginTransformMatrix.SetElement(0, 3, 0)
- brushToWorldOriginTransformMatrix.SetElement(1, 3, 0)
- brushToWorldOriginTransformMatrix.SetElement(2, 3, 0)
-
- self.brushToWorldOriginTransform.Identity()
- self.brushToWorldOriginTransform.Concatenate(brushToWorldOriginTransformMatrix)
- self.brushToWorldOriginTransform.RotateX(90) # cylinder's long axis is the Y axis, we need to rotate it to Z axis
-
- sliceSpacingMm = self.scriptedEffect.sliceSpacing(self.sliceWidget)
-
- center = [0, 0, 0]
- if self.brushMode == HISTOGRAM_BRUSH_TYPE_CIRCLE:
- center = self.point1
-
- point1ToPoint2 = [0, 0, 0]
- vtk.vtkMath.Subtract(self.point1, self.point2, point1ToPoint2)
- radius = vtk.vtkMath.Normalize(point1ToPoint2)
-
- self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
- self.brushCylinderSource.SetRadius(radius)
- self.brushCylinderSource.SetHeight(sliceSpacingMm)
-
- elif self.brushMode == HISTOGRAM_BRUSH_TYPE_BOX:
- self.brushToWorldOriginTransformer.SetInputConnection(self.brushCubeSource.GetOutputPort())
-
- length = [0, 0, 0]
- for i in range(3):
- center[i] = (self.point1[i] + self.point2[i]) / 2.0
- length[i] = abs(self.point1[i] - self.point2[i])
-
- xVector = [1, 0, 0, 0]
- self.brushToWorldOriginTransform.MultiplyPoint(xVector, xVector)
- xLength = abs(vtk.vtkMath.Dot(xVector[:3], length))
- self.brushCubeSource.SetXLength(xLength)
-
- zVector = [0, 0, 1, 0]
- self.brushToWorldOriginTransform.MultiplyPoint(zVector, zVector)
- zLength = abs(vtk.vtkMath.Dot(zVector[:3], length))
- self.brushCubeSource.SetZLength(zLength)
- self.brushCubeSource.SetYLength(sliceSpacingMm)
-
- elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
- self.brushLineSource.SetPoint1(self.point1)
- self.brushLineSource.SetPoint2(self.point2)
- self.brushTubeSource.SetRadius(sliceSpacingMm)
-
- self.worldOriginToWorldTransform.Identity()
- self.worldOriginToWorldTransform.Translate(center)
-
- self.sliceWidget.sliceView().scheduleRender()
+ def __init__(self, thresholdEffect, scriptedEffect, sliceWidget, brushMode):
+ self.thresholdEffect = thresholdEffect
+ self.scriptedEffect = scriptedEffect
+ self.sliceWidget = sliceWidget
+ self.brushMode = brushMode
+ self.state = HISTOGRAM_STATE_OFF
+
+ self.point1 = None
+ self.point2 = None
+
+ # Actor setup
+ self.brushCylinderSource = vtk.vtkCylinderSource()
+ self.brushCylinderSource.SetResolution(32)
+
+ self.brushCubeSource = vtk.vtkCubeSource()
+
+ self.brushLineSource = vtk.vtkLineSource()
+ self.brushTubeSource = vtk.vtkTubeFilter()
+ self.brushTubeSource.SetInputConnection(self.brushLineSource.GetOutputPort())
+ self.brushTubeSource.SetNumberOfSides(50)
+ self.brushTubeSource.SetCapping(True)
+
+ self.brushToWorldOriginTransform = vtk.vtkTransform()
+ self.brushToWorldOriginTransformer = vtk.vtkTransformPolyDataFilter()
+ self.brushToWorldOriginTransformer.SetTransform(self.brushToWorldOriginTransform)
+ self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
+
+ self.normalFilter = vtk.vtkPolyDataNormals()
+ self.normalFilter.AutoOrientNormalsOn()
+ self.normalFilter.SetInputConnection(self.brushToWorldOriginTransformer.GetOutputPort())
+
+ # Brush to RAS transform
+ self.worldOriginToWorldTransform = vtk.vtkTransform()
+ self.worldOriginToWorldTransformer = vtk.vtkTransformPolyDataFilter()
+ self.worldOriginToWorldTransformer.SetTransform(self.worldOriginToWorldTransform)
+ self.worldOriginToWorldTransformer.SetInputConnection(self.normalFilter.GetOutputPort())
+
+ # RAS to XY transform
+ self.worldToSliceTransform = vtk.vtkTransform()
+ self.worldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
+ self.worldToSliceTransformer.SetTransform(self.worldToSliceTransform)
+ self.worldToSliceTransformer.SetInputConnection(self.worldOriginToWorldTransformer.GetOutputPort())
+
+ # Cutting takes place in XY coordinates
+ self.slicePlane = vtk.vtkPlane()
+ self.slicePlane.SetNormal(0, 0, 1)
+ self.slicePlane.SetOrigin(0, 0, 0)
+ self.cutter = vtk.vtkCutter()
+ self.cutter.SetCutFunction(self.slicePlane)
+ self.cutter.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
+
+ self.rasPoints = vtk.vtkPoints()
+ lines = vtk.vtkCellArray()
+ self.polyData = vtk.vtkPolyData()
+ self.polyData.SetPoints(self.rasPoints)
+ self.polyData.SetLines(lines)
+
+ # Thin line
+ self.thinRASPoints = vtk.vtkPoints()
+ thinLines = vtk.vtkCellArray()
+ self.thinPolyData = vtk.vtkPolyData()
+ self.thinPolyData.SetPoints(self.rasPoints)
+ self.thinPolyData.SetLines(thinLines)
+
+ self.mapper = vtk.vtkPolyDataMapper2D()
+ self.mapper.SetInputConnection(self.cutter.GetOutputPort())
+
+ # Add actor
+ self.actor = vtk.vtkActor2D()
+ self.actor.SetMapper(self.mapper)
+ actorProperty = self.actor.GetProperty()
+ actorProperty.SetColor(1, 1, 0)
+ actorProperty.SetLineWidth(2)
+ renderer = self.scriptedEffect.renderer(sliceWidget)
+ if renderer is None:
+ logging.error("pipelineForWidget: Failed to get renderer!")
+ return None
+ self.scriptedEffect.addActor2D(sliceWidget, self.actor)
+
+ self.thinActor = None
+ if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
+ self.worldToSliceTransformer.SetInputData(self.polyData)
+ self.mapper.SetInputConnection(self.worldToSliceTransformer.GetOutputPort())
+
+ self.thinWorldToSliceTransformer = vtk.vtkTransformPolyDataFilter()
+ self.thinWorldToSliceTransformer.SetInputData(self.thinPolyData)
+ self.thinWorldToSliceTransformer.SetTransform(self.worldToSliceTransform)
+
+ self.thinMapper = vtk.vtkPolyDataMapper2D()
+ self.thinMapper.SetInputConnection(self.thinWorldToSliceTransformer.GetOutputPort())
+
+ self.thinActor = vtk.vtkActor2D()
+ self.thinActor.SetMapper(self.thinMapper)
+ thinActorProperty = self.thinActor.GetProperty()
+ thinActorProperty.SetColor(1, 1, 0)
+ thinActorProperty.SetLineWidth(1)
+ self.scriptedEffect.addActor2D(sliceWidget, self.thinActor)
+ elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
+ self.worldToSliceTransformer.SetInputConnection(self.brushTubeSource.GetOutputPort())
+
+ def removeActors(self):
+ if self.actor is not None:
+ self.scriptedEffect.removeActor2D(self.sliceWidget, self.actor)
+ if self.thinActor is not None:
+ self.scriptedEffect.removeActor2D(self.sliceWidget, self.thinActor)
+
+ def setPoint1(self, ras):
+ self.point1 = ras
+ self.updateBrushModel()
+
+ def setPoint2(self, ras):
+ self.point2 = ras
+ self.updateBrushModel()
+
+ def addPoint(self, ras):
+ if self.brushMode == HISTOGRAM_BRUSH_TYPE_DRAW:
+ newPointIndex = self.rasPoints.InsertNextPoint(ras)
+ previousPointIndex = newPointIndex - 1
+ if (previousPointIndex >= 0):
+ idList = vtk.vtkIdList()
+ idList.InsertNextId(previousPointIndex)
+ idList.InsertNextId(newPointIndex)
+ self.polyData.InsertNextCell(vtk.VTK_LINE, idList)
+
+ thinLines = self.thinPolyData.GetLines()
+ thinLines.Initialize()
+ idList = vtk.vtkIdList()
+ idList.InsertNextId(newPointIndex)
+ idList.InsertNextId(0)
+ self.thinPolyData.InsertNextCell(vtk.VTK_LINE, idList)
+
+ else:
+ if self.point1 is None:
+ self.setPoint1(ras)
+ self.setPoint2(ras)
+
+ def updateBrushModel(self):
+ if self.brushMode != HISTOGRAM_BRUSH_TYPE_DRAW and (self.point1 is None or self.point2 is None):
+ return
+
+ # Update slice cutting plane position and orientation
+ sliceXyToRas = self.sliceWidget.sliceLogic().GetSliceNode().GetXYToRAS()
+ rasToSliceXy = vtk.vtkMatrix4x4()
+ vtk.vtkMatrix4x4.Invert(sliceXyToRas, rasToSliceXy)
+ self.worldToSliceTransform.SetMatrix(rasToSliceXy)
+
+ # brush is rotated to the slice widget plane
+ brushToWorldOriginTransformMatrix = vtk.vtkMatrix4x4()
+ brushToWorldOriginTransformMatrix.DeepCopy(self.sliceWidget.sliceLogic().GetSliceNode().GetSliceToRAS())
+ brushToWorldOriginTransformMatrix.SetElement(0, 3, 0)
+ brushToWorldOriginTransformMatrix.SetElement(1, 3, 0)
+ brushToWorldOriginTransformMatrix.SetElement(2, 3, 0)
+
+ self.brushToWorldOriginTransform.Identity()
+ self.brushToWorldOriginTransform.Concatenate(brushToWorldOriginTransformMatrix)
+ self.brushToWorldOriginTransform.RotateX(90) # cylinder's long axis is the Y axis, we need to rotate it to Z axis
+
+ sliceSpacingMm = self.scriptedEffect.sliceSpacing(self.sliceWidget)
+
+ center = [0, 0, 0]
+ if self.brushMode == HISTOGRAM_BRUSH_TYPE_CIRCLE:
+ center = self.point1
+
+ point1ToPoint2 = [0, 0, 0]
+ vtk.vtkMath.Subtract(self.point1, self.point2, point1ToPoint2)
+ radius = vtk.vtkMath.Normalize(point1ToPoint2)
+
+ self.brushToWorldOriginTransformer.SetInputConnection(self.brushCylinderSource.GetOutputPort())
+ self.brushCylinderSource.SetRadius(radius)
+ self.brushCylinderSource.SetHeight(sliceSpacingMm)
+
+ elif self.brushMode == HISTOGRAM_BRUSH_TYPE_BOX:
+ self.brushToWorldOriginTransformer.SetInputConnection(self.brushCubeSource.GetOutputPort())
+
+ length = [0, 0, 0]
+ for i in range(3):
+ center[i] = (self.point1[i] + self.point2[i]) / 2.0
+ length[i] = abs(self.point1[i] - self.point2[i])
+
+ xVector = [1, 0, 0, 0]
+ self.brushToWorldOriginTransform.MultiplyPoint(xVector, xVector)
+ xLength = abs(vtk.vtkMath.Dot(xVector[:3], length))
+ self.brushCubeSource.SetXLength(xLength)
+
+ zVector = [0, 0, 1, 0]
+ self.brushToWorldOriginTransform.MultiplyPoint(zVector, zVector)
+ zLength = abs(vtk.vtkMath.Dot(zVector[:3], length))
+ self.brushCubeSource.SetZLength(zLength)
+ self.brushCubeSource.SetYLength(sliceSpacingMm)
+
+ elif self.brushMode == HISTOGRAM_BRUSH_TYPE_LINE:
+ self.brushLineSource.SetPoint1(self.point1)
+ self.brushLineSource.SetPoint2(self.point2)
+ self.brushTubeSource.SetRadius(sliceSpacingMm)
+
+ self.worldOriginToWorldTransform.Identity()
+ self.worldOriginToWorldTransform.Translate(center)
+
+ self.sliceWidget.sliceView().scheduleRender()
HISTOGRAM_BRUSH_TYPE_PARAMETER_NAME = "BrushType"
diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py
index f648427a0c9..301a4c47eb6 100644
--- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py
+++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationWidgetsTest1.py
@@ -9,403 +9,403 @@
class SegmentationWidgetsTest1(ScriptedLoadableModuleTest):
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_SegmentationWidgetsTest1()
-
- # ------------------------------------------------------------------------------
- def test_SegmentationWidgetsTest1(self):
- # Check for modules
- self.assertIsNotNone(slicer.modules.segmentations)
-
- self.TestSection_00_SetupPathsAndNames()
- self.TestSection_01_GenerateInputData()
- self.TestSection_02_qMRMLSegmentsTableView()
- self.TestSection_03_qMRMLSegmentationGeometryWidget()
- self.TestSection_04_qMRMLSegmentEditorWidget()
-
- logging.info('Test finished')
-
- # ------------------------------------------------------------------------------
- def TestSection_00_SetupPathsAndNames(self):
- logging.info('Test section 0: SetupPathsAndNames')
- self.inputSegmentationNode = None
-
- # ------------------------------------------------------------------------------
- def TestSection_01_GenerateInputData(self):
- logging.info('Test section 1: GenerateInputData')
- self.inputSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
-
- # Create new segments
- import random
- for segmentName in ['first', 'second', 'third']:
- sphereSegment = slicer.vtkSegment()
- sphereSegment.SetName(segmentName)
- sphereSegment.SetColor(random.uniform(0.0, 1.0), random.uniform(0.0, 1.0), random.uniform(0.0, 1.0))
-
- sphere = vtk.vtkSphereSource()
- sphere.SetCenter(random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100))
- sphere.SetRadius(random.uniform(20, 30))
- sphere.Update()
- spherePolyData = sphere.GetOutput()
- sphereSegment.AddRepresentation(
- slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(),
- spherePolyData)
-
- self.inputSegmentationNode.GetSegmentation().AddSegment(sphereSegment)
-
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
-
- self.inputSegmentationNode.CreateDefaultDisplayNodes()
- displayNode = self.inputSegmentationNode.GetDisplayNode()
- self.assertIsNotNone(displayNode)
-
- # ------------------------------------------------------------------------------
- def TestSection_02_qMRMLSegmentsTableView(self):
- logging.info('Test section 2: qMRMLSegmentsTableView')
-
- displayNode = self.inputSegmentationNode.GetDisplayNode()
- self.assertIsNotNone(displayNode)
-
- segmentsTableView = slicer.qMRMLSegmentsTableView()
- segmentsTableView.setMRMLScene(slicer.mrmlScene)
- segmentsTableView.setSegmentationNode(self.inputSegmentationNode)
- self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 3)
- segmentsTableView.show()
- slicer.app.processEvents()
- slicer.util.delayDisplay("All shown")
-
- segmentsTableView.setHideSegments(['second'])
- self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2)
- slicer.app.processEvents()
- slicer.util.delayDisplay("Hidden 'second'")
-
- segmentsTableView.setHideSegments([])
- segmentsTableView.filterBarVisible = True
- segmentsTableView.textFilter = "third"
- self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 1)
- slicer.app.processEvents()
- slicer.util.delayDisplay("All but 'third' filtered")
-
- segmentsTableView.textFilter = ""
- firstSegment = self.inputSegmentationNode.GetSegmentation().GetSegment("first")
- logic = slicer.modules.segmentations.logic()
- logic.SetSegmentStatus(firstSegment, logic.InProgress)
- sortFilterProxyModel = segmentsTableView.sortFilterProxyModel()
- sortFilterProxyModel.setShowStatus(logic.NotStarted, True)
- self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2)
- slicer.app.processEvents()
- slicer.util.delayDisplay("'NotStarted' shown")
-
- segmentsTableView.setSelectedSegmentIDs(["third"])
- segmentsTableView.setHideSegments(['second'])
- segmentsTableView.showOnlySelectedSegments()
- self.assertEqual(displayNode.GetSegmentVisibility("first"), False)
- self.assertEqual(displayNode.GetSegmentVisibility("second"), True)
- self.assertEqual(displayNode.GetSegmentVisibility("third"), True)
- slicer.app.processEvents()
- slicer.util.delayDisplay("Show only selected segments")
-
- displayNode.SetSegmentVisibility("first", True)
- displayNode.SetSegmentVisibility("second", True)
- displayNode.SetSegmentVisibility("third", True)
- segmentsTableView.filterBarVisible = False
-
- # Reset the filtering parameters in the segmentation node to avoid interference with other tests that
- # use this segmentation node
- self.inputSegmentationNode.SetSegmentListFilterEnabled(False)
- self.inputSegmentationNode.SetSegmentListFilterOptions("")
-
- # ------------------------------------------------------------------------------
- def compareOutputGeometry(self, orientedImageData, spacing, origin, directions):
- if orientedImageData is None:
- logging.error('Invalid input oriented image data')
- return False
- if (not isinstance(spacing, list) and not isinstance(spacing, tuple)) \
- or (not isinstance(origin, list) and not isinstance(origin, tuple)) \
- or not isinstance(directions, list):
- logging.error('Invalid baseline object types - need lists')
- return False
- if len(spacing) != 3 or len(origin) != 3 or len(directions) != 3 \
- or len(directions[0]) != 3 or len(directions[1]) != 3 or len(directions[2]) != 3:
- logging.error('Baseline lists need to contain 3 elements each, the directions 3 lists of 3')
- return False
- import numpy
- tolerance = 0.0001
- actualSpacing = orientedImageData.GetSpacing()
- actualOrigin = orientedImageData.GetOrigin()
- actualDirections = [[0] * 3, [0] * 3, [0] * 3]
- orientedImageData.GetDirections(actualDirections)
- for i in [0, 1, 2]:
- if not numpy.isclose(spacing[i], actualSpacing[i], tolerance):
- logging.warning('Spacing discrepancy: ' + str(spacing) + ' != ' + str(actualSpacing))
- return False
- if not numpy.isclose(origin[i], actualOrigin[i], tolerance):
- logging.warning('Origin discrepancy: ' + str(origin) + ' != ' + str(actualOrigin))
- return False
- for j in [0, 1, 2]:
- if not numpy.isclose(directions[i][j], actualDirections[i][j], tolerance):
- logging.warning('Directions discrepancy: ' + str(directions) + ' != ' + str(actualDirections))
- return False
- return True
-
- # ------------------------------------------------------------------------------
- def getForegroundVoxelCount(self, imageData):
- if imageData is None:
- logging.error('Invalid input image data')
- return False
- imageAccumulate = vtk.vtkImageAccumulate()
- imageAccumulate.SetInputData(imageData)
- imageAccumulate.SetIgnoreZero(1)
- imageAccumulate.Update()
- return imageAccumulate.GetVoxelCount()
-
- # ------------------------------------------------------------------------------
- def TestSection_03_qMRMLSegmentationGeometryWidget(self):
- logging.info('Test section 2: qMRMLSegmentationGeometryWidget')
-
- binaryLabelmapReprName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName()
- closedSurfaceReprName = slicer.vtkSegmentationConverter.GetClosedSurfaceRepresentationName()
-
- # Use MRHead and Tinypatient for testing
- import SampleData
- mrVolumeNode = SampleData.downloadSample("MRHead")
- [tinyVolumeNode, tinySegmentationNode] = SampleData.downloadSamples('TinyPatient')
-
- # Convert MRHead to oriented image data
- import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic
- mrOrientedImageData = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.CreateOrientedImageDataFromVolumeNode(mrVolumeNode)
- mrOrientedImageData.UnRegister(None)
-
- # Create segmentation node with binary labelmap master and one segment with MRHead geometry
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- segmentationNode.GetSegmentation().SetMasterRepresentationName(binaryLabelmapReprName)
- geometryStr = slicer.vtkSegmentationConverter.SerializeImageGeometry(mrOrientedImageData)
- segmentationNode.GetSegmentation().SetConversionParameter(
- slicer.vtkSegmentationConverter.GetReferenceImageGeometryParameterName(), geometryStr)
-
- threshold = vtk.vtkImageThreshold()
- threshold.SetInputData(mrOrientedImageData)
- threshold.ThresholdByUpper(16.0)
- threshold.SetInValue(1)
- threshold.SetOutValue(0)
- threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
- threshold.Update()
- segmentOrientedImageData = slicer.vtkOrientedImageData()
- segmentOrientedImageData.DeepCopy(threshold.GetOutput())
- mrImageToWorldMatrix = vtk.vtkMatrix4x4()
- mrOrientedImageData.GetImageToWorldMatrix(mrImageToWorldMatrix)
- segmentOrientedImageData.SetImageToWorldMatrix(mrImageToWorldMatrix)
- segment = slicer.vtkSegment()
- segment.SetName('Brain')
- segment.SetColor(0.0, 0.0, 1.0)
- segment.AddRepresentation(binaryLabelmapReprName, segmentOrientedImageData)
- segmentationNode.GetSegmentation().AddSegment(segment)
-
- # Create geometry widget
- geometryWidget = slicer.qMRMLSegmentationGeometryWidget()
- geometryWidget.setSegmentationNode(segmentationNode)
- geometryWidget.editEnabled = True
- geometryImageData = slicer.vtkOrientedImageData() # To contain the output later
-
- # Volume source with no transforms
- geometryWidget.setSourceNode(tinyVolumeNode)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (49, 49, 23), (248.8439, 248.2890, -123.75),
- [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92)
-
- # Transformed volume source
- translationTransformMatrix = vtk.vtkMatrix4x4()
- translationTransformMatrix.SetElement(0, 3, 24.5)
- translationTransformMatrix.SetElement(1, 3, 24.5)
- translationTransformMatrix.SetElement(2, 3, 11.5)
- translationTransformNode = slicer.vtkMRMLLinearTransformNode()
- translationTransformNode.SetName('TestTranslation')
- slicer.mrmlScene.AddNode(translationTransformNode)
- translationTransformNode.SetMatrixTransformToParent(translationTransformMatrix)
-
- tinyVolumeNode.SetAndObserveTransformNodeID(translationTransformNode.GetID())
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (49, 49, 23), (273.3439, 272.7890, -112.25),
- [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 94)
-
- # Volume source with isotropic spacing
- tinyVolumeNode.SetAndObserveTransformNodeID(None)
- geometryWidget.setIsotropicSpacing(True)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (23, 23, 23), (248.8439, 248.2890, -123.75),
- [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 414)
-
- # Volume source with oversampling
- geometryWidget.setIsotropicSpacing(False)
- geometryWidget.setOversamplingFactor(2.0)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (24.5, 24.5, 11.5), (261.0939, 260.5390, -129.5),
- [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 751)
- slicer.util.delayDisplay('Volume source cases - OK')
-
- # Segmentation source with binary labelmap master
- geometryWidget.setOversamplingFactor(1.0)
- geometryWidget.setSourceNode(tinySegmentationNode)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (49, 49, 23), (248.8439, 248.2890, -123.75),
- [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92)
-
- # Segmentation source with closed surface master
- tinySegmentationNode.GetSegmentation().SetConversionParameter('Smoothing factor', '0.0')
- self.assertTrue(tinySegmentationNode.GetSegmentation().CreateRepresentation(closedSurfaceReprName))
- tinySegmentationNode.GetSegmentation().SetMasterRepresentationName(closedSurfaceReprName)
- tinySegmentationNode.Modified() # Trigger re-calculation of geometry (only generic Modified event is observed)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept
- [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
- slicer.util.delayDisplay('Segmentation source cases - OK')
-
- # Model source with no transform
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- outputFolderId = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'ModelsFolder')
- success = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.ExportVisibleSegmentsToModels(
- tinySegmentationNode, outputFolderId)
- self.assertTrue(success)
- modelNode = slicer.util.getNode('Body_Contour')
- geometryWidget.setSourceNode(modelNode)
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept
- [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
- slicer.util.delayDisplay('Model source - OK')
-
- # Transformed model source
- rotationTransform = vtk.vtkTransform()
- rotationTransform.RotateX(45)
- rotationTransformMatrix = vtk.vtkMatrix4x4()
- rotationTransform.GetMatrix(rotationTransformMatrix)
- rotationTransformNode = slicer.vtkMRMLLinearTransformNode()
- rotationTransformNode.SetName('TestRotation')
- slicer.mrmlScene.AddNode(rotationTransformNode)
- rotationTransformNode.SetMatrixTransformToParent(rotationTransformMatrix)
-
- modelNode.SetAndObserveTransformNodeID(rotationTransformNode.GetID())
- modelNode.Modified()
- geometryWidget.geometryImageData(geometryImageData)
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (1, 1, 1), (-86.645, 177.282, -12.122),
- [[0.0, 0.0, 1.0], [-0.7071, -0.7071, 0.0], [0.7071, -0.7071, 0.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5229164)
- slicer.util.delayDisplay('Transformed model source - OK')
-
- # ROI source
- roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode", 'SourceROI')
- rasDimensions = [0.0, 0.0, 0.0]
- rasCenter = [0.0, 0.0, 0.0]
- slicer.vtkMRMLSliceLogic.GetVolumeRASBox(tinyVolumeNode, rasDimensions, rasCenter)
- print(f"rasDimensions={rasDimensions}, rasCenter={rasCenter}")
- rasRadius = [x / 2.0 for x in rasDimensions]
- roiNode.SetCenter(rasCenter)
- roiNode.SetRadiusXYZ(rasRadius)
- geometryWidget.setSourceNode(roiNode)
- geometryWidget.geometryImageData(geometryImageData)
- print(f"geometryImageData: {geometryImageData}")
- self.assertTrue(self.compareOutputGeometry(geometryImageData,
- (1, 1, 1), (28.344, 27.789, -20.25),
- [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]))
- slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
- self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
- slicer.util.delayDisplay('ROI source - OK')
-
- slicer.util.delayDisplay('Segmentation geometry widget test passed')
-
- # ------------------------------------------------------------------------------
- def TestSection_04_qMRMLSegmentEditorWidget(self):
- logging.info('Test section 4: qMRMLSegmentEditorWidget')
-
- self.segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentEditorNode')
- self.assertIsNotNone(self.segmentEditorNode)
-
- self.inputSegmentationNode.SetSegmentListFilterEnabled(False)
- self.inputSegmentationNode.SetSegmentListFilterOptions("")
-
- displayNode = self.inputSegmentationNode.GetDisplayNode()
- self.assertIsNotNone(displayNode)
-
- segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
- segmentEditorWidget.setMRMLSegmentEditorNode(self.segmentEditorNode)
- segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
- segmentEditorWidget.setSegmentationNode(self.inputSegmentationNode)
- segmentEditorWidget.installKeyboardShortcuts(segmentEditorWidget)
- segmentEditorWidget.setFocus(qt.Qt.OtherFocusReason)
- segmentEditorWidget.show()
-
- self.segmentEditorNode.SetSelectedSegmentID('first')
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- slicer.app.processEvents()
- slicer.util.delayDisplay("First selected")
-
- segmentEditorWidget.selectNextSegment()
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'second')
- slicer.app.processEvents()
- slicer.util.delayDisplay("Next segment")
-
- segmentEditorWidget.selectPreviousSegment()
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- slicer.app.processEvents()
- slicer.util.delayDisplay("Previous segment")
-
- displayNode.SetSegmentVisibility('second', False)
- segmentEditorWidget.selectNextSegment()
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'third')
- slicer.app.processEvents()
- slicer.util.delayDisplay("Next segment (with second segment hidden)")
-
- # Trying to go out of bounds past first segment
- segmentEditorWidget.selectPreviousSegment() # First
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- segmentEditorWidget.selectPreviousSegment() # First
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- segmentEditorWidget.selectPreviousSegment() # First
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- slicer.app.processEvents()
- slicer.util.delayDisplay("Multiple previous segment")
-
- # Wrap around
- self.segmentEditorNode.SetSelectedSegmentID('third')
- segmentEditorWidget.selectNextSegment()
- self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
- slicer.util.delayDisplay("Wrap around segments")
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SegmentationWidgetsTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_SegmentationWidgetsTest1(self):
+ # Check for modules
+ self.assertIsNotNone(slicer.modules.segmentations)
+
+ self.TestSection_00_SetupPathsAndNames()
+ self.TestSection_01_GenerateInputData()
+ self.TestSection_02_qMRMLSegmentsTableView()
+ self.TestSection_03_qMRMLSegmentationGeometryWidget()
+ self.TestSection_04_qMRMLSegmentEditorWidget()
+
+ logging.info('Test finished')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_00_SetupPathsAndNames(self):
+ logging.info('Test section 0: SetupPathsAndNames')
+ self.inputSegmentationNode = None
+
+ # ------------------------------------------------------------------------------
+ def TestSection_01_GenerateInputData(self):
+ logging.info('Test section 1: GenerateInputData')
+ self.inputSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+
+ # Create new segments
+ import random
+ for segmentName in ['first', 'second', 'third']:
+ sphereSegment = slicer.vtkSegment()
+ sphereSegment.SetName(segmentName)
+ sphereSegment.SetColor(random.uniform(0.0, 1.0), random.uniform(0.0, 1.0), random.uniform(0.0, 1.0))
+
+ sphere = vtk.vtkSphereSource()
+ sphere.SetCenter(random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100))
+ sphere.SetRadius(random.uniform(20, 30))
+ sphere.Update()
+ spherePolyData = sphere.GetOutput()
+ sphereSegment.AddRepresentation(
+ slicer.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(),
+ spherePolyData)
+
+ self.inputSegmentationNode.GetSegmentation().AddSegment(sphereSegment)
+
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
+
+ self.inputSegmentationNode.CreateDefaultDisplayNodes()
+ displayNode = self.inputSegmentationNode.GetDisplayNode()
+ self.assertIsNotNone(displayNode)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_02_qMRMLSegmentsTableView(self):
+ logging.info('Test section 2: qMRMLSegmentsTableView')
+
+ displayNode = self.inputSegmentationNode.GetDisplayNode()
+ self.assertIsNotNone(displayNode)
+
+ segmentsTableView = slicer.qMRMLSegmentsTableView()
+ segmentsTableView.setMRMLScene(slicer.mrmlScene)
+ segmentsTableView.setSegmentationNode(self.inputSegmentationNode)
+ self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 3)
+ segmentsTableView.show()
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("All shown")
+
+ segmentsTableView.setHideSegments(['second'])
+ self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2)
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Hidden 'second'")
+
+ segmentsTableView.setHideSegments([])
+ segmentsTableView.filterBarVisible = True
+ segmentsTableView.textFilter = "third"
+ self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 1)
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("All but 'third' filtered")
+
+ segmentsTableView.textFilter = ""
+ firstSegment = self.inputSegmentationNode.GetSegmentation().GetSegment("first")
+ logic = slicer.modules.segmentations.logic()
+ logic.SetSegmentStatus(firstSegment, logic.InProgress)
+ sortFilterProxyModel = segmentsTableView.sortFilterProxyModel()
+ sortFilterProxyModel.setShowStatus(logic.NotStarted, True)
+ self.assertEqual(len(segmentsTableView.displayedSegmentIDs()), 2)
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("'NotStarted' shown")
+
+ segmentsTableView.setSelectedSegmentIDs(["third"])
+ segmentsTableView.setHideSegments(['second'])
+ segmentsTableView.showOnlySelectedSegments()
+ self.assertEqual(displayNode.GetSegmentVisibility("first"), False)
+ self.assertEqual(displayNode.GetSegmentVisibility("second"), True)
+ self.assertEqual(displayNode.GetSegmentVisibility("third"), True)
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Show only selected segments")
+
+ displayNode.SetSegmentVisibility("first", True)
+ displayNode.SetSegmentVisibility("second", True)
+ displayNode.SetSegmentVisibility("third", True)
+ segmentsTableView.filterBarVisible = False
+
+ # Reset the filtering parameters in the segmentation node to avoid interference with other tests that
+ # use this segmentation node
+ self.inputSegmentationNode.SetSegmentListFilterEnabled(False)
+ self.inputSegmentationNode.SetSegmentListFilterOptions("")
+
+ # ------------------------------------------------------------------------------
+ def compareOutputGeometry(self, orientedImageData, spacing, origin, directions):
+ if orientedImageData is None:
+ logging.error('Invalid input oriented image data')
+ return False
+ if (not isinstance(spacing, list) and not isinstance(spacing, tuple)) \
+ or (not isinstance(origin, list) and not isinstance(origin, tuple)) \
+ or not isinstance(directions, list):
+ logging.error('Invalid baseline object types - need lists')
+ return False
+ if len(spacing) != 3 or len(origin) != 3 or len(directions) != 3 \
+ or len(directions[0]) != 3 or len(directions[1]) != 3 or len(directions[2]) != 3:
+ logging.error('Baseline lists need to contain 3 elements each, the directions 3 lists of 3')
+ return False
+ import numpy
+ tolerance = 0.0001
+ actualSpacing = orientedImageData.GetSpacing()
+ actualOrigin = orientedImageData.GetOrigin()
+ actualDirections = [[0] * 3, [0] * 3, [0] * 3]
+ orientedImageData.GetDirections(actualDirections)
+ for i in [0, 1, 2]:
+ if not numpy.isclose(spacing[i], actualSpacing[i], tolerance):
+ logging.warning('Spacing discrepancy: ' + str(spacing) + ' != ' + str(actualSpacing))
+ return False
+ if not numpy.isclose(origin[i], actualOrigin[i], tolerance):
+ logging.warning('Origin discrepancy: ' + str(origin) + ' != ' + str(actualOrigin))
+ return False
+ for j in [0, 1, 2]:
+ if not numpy.isclose(directions[i][j], actualDirections[i][j], tolerance):
+ logging.warning('Directions discrepancy: ' + str(directions) + ' != ' + str(actualDirections))
+ return False
+ return True
+
+ # ------------------------------------------------------------------------------
+ def getForegroundVoxelCount(self, imageData):
+ if imageData is None:
+ logging.error('Invalid input image data')
+ return False
+ imageAccumulate = vtk.vtkImageAccumulate()
+ imageAccumulate.SetInputData(imageData)
+ imageAccumulate.SetIgnoreZero(1)
+ imageAccumulate.Update()
+ return imageAccumulate.GetVoxelCount()
+
+ # ------------------------------------------------------------------------------
+ def TestSection_03_qMRMLSegmentationGeometryWidget(self):
+ logging.info('Test section 2: qMRMLSegmentationGeometryWidget')
+
+ binaryLabelmapReprName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName()
+ closedSurfaceReprName = slicer.vtkSegmentationConverter.GetClosedSurfaceRepresentationName()
+
+ # Use MRHead and Tinypatient for testing
+ import SampleData
+ mrVolumeNode = SampleData.downloadSample("MRHead")
+ [tinyVolumeNode, tinySegmentationNode] = SampleData.downloadSamples('TinyPatient')
+
+ # Convert MRHead to oriented image data
+ import vtkSlicerSegmentationsModuleLogicPython as vtkSlicerSegmentationsModuleLogic
+ mrOrientedImageData = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.CreateOrientedImageDataFromVolumeNode(mrVolumeNode)
+ mrOrientedImageData.UnRegister(None)
+
+ # Create segmentation node with binary labelmap master and one segment with MRHead geometry
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ segmentationNode.GetSegmentation().SetMasterRepresentationName(binaryLabelmapReprName)
+ geometryStr = slicer.vtkSegmentationConverter.SerializeImageGeometry(mrOrientedImageData)
+ segmentationNode.GetSegmentation().SetConversionParameter(
+ slicer.vtkSegmentationConverter.GetReferenceImageGeometryParameterName(), geometryStr)
+
+ threshold = vtk.vtkImageThreshold()
+ threshold.SetInputData(mrOrientedImageData)
+ threshold.ThresholdByUpper(16.0)
+ threshold.SetInValue(1)
+ threshold.SetOutValue(0)
+ threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+ threshold.Update()
+ segmentOrientedImageData = slicer.vtkOrientedImageData()
+ segmentOrientedImageData.DeepCopy(threshold.GetOutput())
+ mrImageToWorldMatrix = vtk.vtkMatrix4x4()
+ mrOrientedImageData.GetImageToWorldMatrix(mrImageToWorldMatrix)
+ segmentOrientedImageData.SetImageToWorldMatrix(mrImageToWorldMatrix)
+ segment = slicer.vtkSegment()
+ segment.SetName('Brain')
+ segment.SetColor(0.0, 0.0, 1.0)
+ segment.AddRepresentation(binaryLabelmapReprName, segmentOrientedImageData)
+ segmentationNode.GetSegmentation().AddSegment(segment)
+
+ # Create geometry widget
+ geometryWidget = slicer.qMRMLSegmentationGeometryWidget()
+ geometryWidget.setSegmentationNode(segmentationNode)
+ geometryWidget.editEnabled = True
+ geometryImageData = slicer.vtkOrientedImageData() # To contain the output later
+
+ # Volume source with no transforms
+ geometryWidget.setSourceNode(tinyVolumeNode)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (49, 49, 23), (248.8439, 248.2890, -123.75),
+ [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92)
+
+ # Transformed volume source
+ translationTransformMatrix = vtk.vtkMatrix4x4()
+ translationTransformMatrix.SetElement(0, 3, 24.5)
+ translationTransformMatrix.SetElement(1, 3, 24.5)
+ translationTransformMatrix.SetElement(2, 3, 11.5)
+ translationTransformNode = slicer.vtkMRMLLinearTransformNode()
+ translationTransformNode.SetName('TestTranslation')
+ slicer.mrmlScene.AddNode(translationTransformNode)
+ translationTransformNode.SetMatrixTransformToParent(translationTransformMatrix)
+
+ tinyVolumeNode.SetAndObserveTransformNodeID(translationTransformNode.GetID())
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (49, 49, 23), (273.3439, 272.7890, -112.25),
+ [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 94)
+
+ # Volume source with isotropic spacing
+ tinyVolumeNode.SetAndObserveTransformNodeID(None)
+ geometryWidget.setIsotropicSpacing(True)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (23, 23, 23), (248.8439, 248.2890, -123.75),
+ [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 414)
+
+ # Volume source with oversampling
+ geometryWidget.setIsotropicSpacing(False)
+ geometryWidget.setOversamplingFactor(2.0)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (24.5, 24.5, 11.5), (261.0939, 260.5390, -129.5),
+ [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 751)
+ slicer.util.delayDisplay('Volume source cases - OK')
+
+ # Segmentation source with binary labelmap master
+ geometryWidget.setOversamplingFactor(1.0)
+ geometryWidget.setSourceNode(tinySegmentationNode)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (49, 49, 23), (248.8439, 248.2890, -123.75),
+ [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 92)
+
+ # Segmentation source with closed surface master
+ tinySegmentationNode.GetSegmentation().SetConversionParameter('Smoothing factor', '0.0')
+ self.assertTrue(tinySegmentationNode.GetSegmentation().CreateRepresentation(closedSurfaceReprName))
+ tinySegmentationNode.GetSegmentation().SetMasterRepresentationName(closedSurfaceReprName)
+ tinySegmentationNode.Modified() # Trigger re-calculation of geometry (only generic Modified event is observed)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept
+ [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
+ slicer.util.delayDisplay('Segmentation source cases - OK')
+
+ # Model source with no transform
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ outputFolderId = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'ModelsFolder')
+ success = vtkSlicerSegmentationsModuleLogic.vtkSlicerSegmentationsModuleLogic.ExportVisibleSegmentsToModels(
+ tinySegmentationNode, outputFolderId)
+ self.assertTrue(success)
+ modelNode = slicer.util.getNode('Body_Contour')
+ geometryWidget.setSourceNode(modelNode)
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (1, 1, 1), (-86.645, 133.929, 116.786), # current origin of the segmentation is kept
+ [[0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
+ slicer.util.delayDisplay('Model source - OK')
+
+ # Transformed model source
+ rotationTransform = vtk.vtkTransform()
+ rotationTransform.RotateX(45)
+ rotationTransformMatrix = vtk.vtkMatrix4x4()
+ rotationTransform.GetMatrix(rotationTransformMatrix)
+ rotationTransformNode = slicer.vtkMRMLLinearTransformNode()
+ rotationTransformNode.SetName('TestRotation')
+ slicer.mrmlScene.AddNode(rotationTransformNode)
+ rotationTransformNode.SetMatrixTransformToParent(rotationTransformMatrix)
+
+ modelNode.SetAndObserveTransformNodeID(rotationTransformNode.GetID())
+ modelNode.Modified()
+ geometryWidget.geometryImageData(geometryImageData)
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (1, 1, 1), (-86.645, 177.282, -12.122),
+ [[0.0, 0.0, 1.0], [-0.7071, -0.7071, 0.0], [0.7071, -0.7071, 0.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5229164)
+ slicer.util.delayDisplay('Transformed model source - OK')
+
+ # ROI source
+ roiNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsROINode", 'SourceROI')
+ rasDimensions = [0.0, 0.0, 0.0]
+ rasCenter = [0.0, 0.0, 0.0]
+ slicer.vtkMRMLSliceLogic.GetVolumeRASBox(tinyVolumeNode, rasDimensions, rasCenter)
+ print(f"rasDimensions={rasDimensions}, rasCenter={rasCenter}")
+ rasRadius = [x / 2.0 for x in rasDimensions]
+ roiNode.SetCenter(rasCenter)
+ roiNode.SetRadiusXYZ(rasRadius)
+ geometryWidget.setSourceNode(roiNode)
+ geometryWidget.geometryImageData(geometryImageData)
+ print(f"geometryImageData: {geometryImageData}")
+ self.assertTrue(self.compareOutputGeometry(geometryImageData,
+ (1, 1, 1), (28.344, 27.789, -20.25),
+ [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]))
+ slicer.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentOrientedImageData, geometryImageData, geometryImageData, False, True)
+ self.assertEqual(self.getForegroundVoxelCount(geometryImageData), 5223040)
+ slicer.util.delayDisplay('ROI source - OK')
+
+ slicer.util.delayDisplay('Segmentation geometry widget test passed')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_04_qMRMLSegmentEditorWidget(self):
+ logging.info('Test section 4: qMRMLSegmentEditorWidget')
+
+ self.segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentEditorNode')
+ self.assertIsNotNone(self.segmentEditorNode)
+
+ self.inputSegmentationNode.SetSegmentListFilterEnabled(False)
+ self.inputSegmentationNode.SetSegmentListFilterOptions("")
+
+ displayNode = self.inputSegmentationNode.GetDisplayNode()
+ self.assertIsNotNone(displayNode)
+
+ segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
+ segmentEditorWidget.setMRMLSegmentEditorNode(self.segmentEditorNode)
+ segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
+ segmentEditorWidget.setSegmentationNode(self.inputSegmentationNode)
+ segmentEditorWidget.installKeyboardShortcuts(segmentEditorWidget)
+ segmentEditorWidget.setFocus(qt.Qt.OtherFocusReason)
+ segmentEditorWidget.show()
+
+ self.segmentEditorNode.SetSelectedSegmentID('first')
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("First selected")
+
+ segmentEditorWidget.selectNextSegment()
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'second')
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Next segment")
+
+ segmentEditorWidget.selectPreviousSegment()
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Previous segment")
+
+ displayNode.SetSegmentVisibility('second', False)
+ segmentEditorWidget.selectNextSegment()
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'third')
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Next segment (with second segment hidden)")
+
+ # Trying to go out of bounds past first segment
+ segmentEditorWidget.selectPreviousSegment() # First
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ segmentEditorWidget.selectPreviousSegment() # First
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ segmentEditorWidget.selectPreviousSegment() # First
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ slicer.app.processEvents()
+ slicer.util.delayDisplay("Multiple previous segment")
+
+ # Wrap around
+ self.segmentEditorNode.SetSelectedSegmentID('third')
+ segmentEditorWidget.selectNextSegment()
+ self.assertEqual(self.segmentEditorNode.GetSelectedSegmentID(), 'first')
+ slicer.util.delayDisplay("Wrap around segments")
diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py
index 1c51f1a3255..7bbe0a04b04 100644
--- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py
+++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest1.py
@@ -12,497 +12,497 @@
class SegmentationsModuleTest1(unittest.TestCase):
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_SegmentationsModuleTest1()
-
- # ------------------------------------------------------------------------------
- def test_SegmentationsModuleTest1(self):
- # Check for modules
- self.assertIsNotNone(slicer.modules.segmentations)
-
- self.TestSection_SetupPathsAndNames()
- self.TestSection_RetrieveInputData()
- self.TestSection_LoadInputData()
- self.TestSection_AddRemoveSegment()
- self.TestSection_MergeLabelmapWithDifferentGeometries()
- self.TestSection_ImportExportSegment()
- self.TestSection_ImportExportSegment2()
- self.TestSection_SubjectHierarchy()
-
- logging.info('Test finished')
-
- # ------------------------------------------------------------------------------
- def TestSection_SetupPathsAndNames(self):
- # Set up paths used for this test
- self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest'
- if not os.access(self.segmentationsModuleTestDir, os.F_OK):
- os.mkdir(self.segmentationsModuleTestDir)
-
- self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg'
- if not os.access(self.dataDir, os.F_OK):
- os.mkdir(self.dataDir)
- self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg'
-
- self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip'
-
- # Define variables
- self.expectedNumOfFilesInDataDir = 4
- self.expectedNumOfFilesInDataSegDir = 2
- self.inputSegmentationNode = None
- self.bodySegmentName = 'Body_Contour'
- self.tumorSegmentName = 'Tumor_Contour'
- self.secondSegmentationNode = None
- self.sphereSegment = None
- self.sphereSegmentName = 'Sphere'
- self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
- self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()
-
- # ------------------------------------------------------------------------------
- def TestSection_RetrieveInputData(self):
- try:
- slicer.util.downloadAndExtractArchive(
- TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
- self.dataZipFilePath, self.segmentationsModuleTestDir,
- checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7')
-
- numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)])
- self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir)
- self.assertTrue(os.access(self.dataSegDir, os.F_OK))
- numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)])
- self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir)
-
- except Exception as e:
- import traceback
- traceback.print_exc()
- logging.error('Test caused exception!\n' + str(e))
-
- # ------------------------------------------------------------------------------
- def TestSection_LoadInputData(self):
- # Load into Slicer
- slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd')
- slicer.util.loadNodeFromFile(self.dataDir + '/TinyPatient_Structures.seg.vtm', "SegmentationFile", {})
-
- # Change master representation to closed surface (so that conversion is possible when adding segment)
- self.inputSegmentationNode = slicer.util.getNode('vtkMRMLSegmentationNode1')
- self.inputSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
-
- # ------------------------------------------------------------------------------
- def TestSection_AddRemoveSegment(self):
- # Add/remove segment from segmentation (check display properties, color table, etc.)
- logging.info('Test section: Add/remove segment')
-
- # Get baseline values
- displayNode = self.inputSegmentationNode.GetDisplayNode()
- self.assertIsNotNone(displayNode)
- # If segments are not found then the returned color is the pre-defined invalid color
- bodyColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName).GetColor()
- logging.info(f"bodyColor: {bodyColor}")
- self.assertEqual(int(bodyColor[0] * 100), 33)
- self.assertEqual(int(bodyColor[1] * 100), 66)
- self.assertEqual(int(bodyColor[2] * 100), 0)
- tumorColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName).GetColor()
- logging.info(f"tumorColor: {tumorColor}")
- self.assertEqual(int(tumorColor[0] * 100), 100)
- self.assertEqual(int(tumorColor[1] * 100), 0)
- self.assertEqual(int(tumorColor[2] * 100), 0)
-
- # Create new segment
- sphere = vtk.vtkSphereSource()
- sphere.SetCenter(0, 50, 0)
- sphere.SetRadius(80)
- sphere.Update()
- spherePolyData = vtk.vtkPolyData()
- spherePolyData.DeepCopy(sphere.GetOutput())
-
- self.sphereSegment = vtkSegmentationCore.vtkSegment()
- self.sphereSegment.SetName(self.sphereSegmentName)
- self.sphereSegment.SetColor(0.0, 0.0, 1.0)
- self.sphereSegment.AddRepresentation(self.closedSurfaceReprName, spherePolyData)
-
- # Add segment to segmentation
- self.inputSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment)
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
-
- # Check merged labelmap
- mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName)
- self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(mergedLabelmap)
- imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0)
- imageStat.SetComponentOrigin(0, 0, 0)
- imageStat.SetComponentSpacing(1, 1, 1)
- imageStat.Update()
- imageStatResult = imageStat.GetOutput()
- for i in range(4):
- logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
- self.assertEqual(imageStat.GetVoxelCount(), 1000)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 4)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 40)
-
- # Check if segment reorder is taken into account in merged labelmap generation
- # Change segment order
- sphereSegmentId = self.inputSegmentationNode.GetSegmentation().GetSegmentIdBySegment(self.sphereSegment)
- self.inputSegmentationNode.GetSegmentation().SetSegmentIndex(sphereSegmentId, 1)
- # Re-generate merged labelmap
- self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
- imageStat.SetInputData(mergedLabelmap)
- imageStat.Update()
- imageStatResult = imageStat.GetOutput()
- for i in range(4):
- logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
- self.assertEqual(imageStat.GetVoxelCount(), 1000)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 39)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 5)
-
- # Remove segment from segmentation
- self.inputSegmentationNode.GetSegmentation().RemoveSegment(self.sphereSegmentName)
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
-
- # ------------------------------------------------------------------------------
- def TestSection_MergeLabelmapWithDifferentGeometries(self):
- # Merge labelmap when segments containing labelmaps with different geometries (both same directions, different directions)
- logging.info('Test section: Merge labelmap with different geometries')
-
- self.assertIsNotNone(self.sphereSegment)
- self.sphereSegment.RemoveRepresentation(self.binaryLabelmapReprName)
- self.assertIsNone(self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName))
-
- # Create new segmentation with sphere segment
- self.secondSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'Second')
- self.secondSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
-
- self.secondSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment)
-
- # Check automatically converted labelmap. It is supposed to have the default geometry
- # (which is different than the one in the input segmentation)
- sphereLabelmap = self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName)
- self.assertIsNotNone(sphereLabelmap)
- sphereLabelmapSpacing = sphereLabelmap.GetSpacing()
- self.assertAlmostEqual(sphereLabelmapSpacing[0], 0.629257364931788, 8)
- self.assertAlmostEqual(sphereLabelmapSpacing[1], 0.629257364931788, 8)
- self.assertAlmostEqual(sphereLabelmapSpacing[2], 0.629257364931788, 8)
-
- # Create binary labelmap in segmentation that will create the merged labelmap from
- # different geometries so that labelmap is not removed from sphere segment when adding
- self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName)
-
- # Copy segment to input segmentation
- self.inputSegmentationNode.GetSegmentation().CopySegmentFromSegmentation(self.secondSegmentationNode.GetSegmentation(), self.sphereSegmentName)
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
-
- # Check merged labelmap
- # Reference geometry has the tiny patient spacing, and it is oversampled to have similar
- # voxel size as the sphere labelmap with the uniform 0.629mm spacing
- mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
- mergedLabelmapSpacing = mergedLabelmap.GetSpacing()
- self.assertAlmostEqual(mergedLabelmapSpacing[0], 0.80327868852459, 8)
- self.assertAlmostEqual(mergedLabelmapSpacing[1], 0.80327868852459, 8)
- self.assertAlmostEqual(mergedLabelmapSpacing[2], 0.377049180327869, 8)
-
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(mergedLabelmap)
- imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0)
- imageStat.SetComponentOrigin(0, 0, 0)
- imageStat.SetComponentSpacing(1, 1, 1)
- imageStat.Update()
- imageStatResult = imageStat.GetOutput()
- for i in range(5):
- logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
- self.assertEqual(imageStat.GetVoxelCount(), 226981000)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 178838889)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(4, 0, 0, 0), 0) # Built from color table and color four is removed in previous test section
-
- # ------------------------------------------------------------------------------
- def TestSection_ImportExportSegment(self):
- # Import/export, both one label and all labels
- logging.info('Test section: Import/export segment')
-
- # Export single segment to model node
- bodyModelNode = slicer.vtkMRMLModelNode()
- bodyModelNode.SetName('BodyModel')
- slicer.mrmlScene.AddNode(bodyModelNode)
-
- bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNode)
- self.assertTrue(result)
- self.assertIsNotNone(bodyModelNode.GetPolyData())
- # TODO: Number of points increased to 1677 due to end-capping, need to investigate!
- # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfPoints(), 302)
- # TODO: On Linux and Windows it is 588, on Mac it is 580. Need to investigate
- # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfCells(), 588)
- # self.assertTrue(bodyModelNode.GetPolyData().GetNumberOfCells() == 588 or bodyModelNode.GetPolyData().GetNumberOfCells() == 580)
-
- # Export single segment to volume node
- bodyLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
- bodyLabelmapNode.SetName('BodyLabelmap')
- slicer.mrmlScene.AddNode(bodyLabelmapNode)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode)
- self.assertTrue(result)
- bodyImageData = bodyLabelmapNode.GetImageData()
- self.assertIsNotNone(bodyImageData)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(bodyImageData)
- imageStat.Update()
- self.assertEqual(imageStat.GetVoxelCount(), 792)
- self.assertEqual(imageStat.GetMin()[0], 0)
- self.assertEqual(imageStat.GetMax()[0], 1)
-
- # Export multiple segments to volume node
- allSegmentsLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
- allSegmentsLabelmapNode.SetName('AllSegmentsLabelmap')
- slicer.mrmlScene.AddNode(allSegmentsLabelmapNode)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportAllSegmentsToLabelmapNode(self.inputSegmentationNode, allSegmentsLabelmapNode)
- self.assertTrue(result)
- allSegmentsImageData = allSegmentsLabelmapNode.GetImageData()
- self.assertIsNotNone(allSegmentsImageData)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(allSegmentsImageData)
- imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0)
- imageStat.SetComponentOrigin(0, 0, 0)
- imageStat.SetComponentSpacing(1, 1, 1)
- imageStat.Update()
- imageStatResult = imageStat.GetOutput()
- for i in range(4):
- logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
- self.assertEqual(imageStat.GetVoxelCount(), 127109360)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 78967249)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883)
- self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940)
- # Import model to segment
- modelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImport')
- modelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
- modelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNode)
- modelSegment.UnRegister(None) # Need to release ownership
- self.assertIsNotNone(modelSegment)
- self.assertIsNotNone(modelSegment.GetRepresentation(self.closedSurfaceReprName))
-
- # Import multi-label labelmap to segmentation
- multiLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'MultiLabelImport')
- multiLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(allSegmentsLabelmapNode, multiLabelImportSegmentationNode)
- self.assertTrue(result)
- self.assertEqual(multiLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
-
- # Import labelmap into single segment
- singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport')
- singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
- # Should not import multi-label labelmap to segment
- nullSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode)
- self.assertIsNone(nullSegment)
- logging.info('(This error message is a result of testing an impossible scenario, it is supposed to appear)')
- # Make labelmap single-label and import again
- threshold = vtk.vtkImageThreshold()
- threshold.SetInValue(0)
- threshold.SetOutValue(1)
- threshold.ReplaceInOn()
- threshold.ThresholdByLower(0)
- threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
- threshold.SetInputData(allSegmentsLabelmapNode.GetImageData())
- threshold.Update()
- allSegmentsLabelmapNode.GetImageData().ShallowCopy(threshold.GetOutput())
- labelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode)
- labelSegment.UnRegister(None) # Need to release ownership
- self.assertIsNotNone(labelSegment)
- self.assertIsNotNone(labelSegment.GetRepresentation(self.binaryLabelmapReprName))
-
- # Import/export with transforms
- logging.info('Test subsection: Import/export with transforms')
-
- # Create transform node that will be used to transform the tested nodes
- bodyModelTransformNode = slicer.vtkMRMLLinearTransformNode()
- slicer.mrmlScene.AddNode(bodyModelTransformNode)
- bodyModelTransform = vtk.vtkTransform()
- bodyModelTransform.Translate(1000.0, 0.0, 0.0)
- bodyModelTransformNode.ApplyTransformMatrix(bodyModelTransform.GetMatrix())
-
- # Set transform as parent to input segmentation node
- self.inputSegmentationNode.SetAndObserveTransformNodeID(bodyModelTransformNode.GetID())
-
- # Export single segment to model node from transformed segmentation
- bodyModelNodeTransformed = slicer.vtkMRMLModelNode()
- bodyModelNodeTransformed.SetName('BodyModelTransformed')
- slicer.mrmlScene.AddNode(bodyModelNodeTransformed)
- bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNodeTransformed)
- self.assertTrue(result)
- self.assertIsNotNone(bodyModelNodeTransformed.GetParentTransformNode())
-
- # Export single segment to volume node from transformed segmentation
- bodyLabelmapNodeTransformed = slicer.vtkMRMLLabelMapVolumeNode()
- bodyLabelmapNodeTransformed.SetName('BodyLabelmapTransformed')
- slicer.mrmlScene.AddNode(bodyLabelmapNodeTransformed)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNodeTransformed)
- self.assertTrue(result)
- self.assertIsNotNone(bodyLabelmapNodeTransformed.GetParentTransformNode())
-
- # Create transform node that will be used to transform the tested nodes
- modelTransformedImportSegmentationTransformNode = slicer.vtkMRMLLinearTransformNode()
- slicer.mrmlScene.AddNode(modelTransformedImportSegmentationTransformNode)
- modelTransformedImportSegmentationTransform = vtk.vtkTransform()
- modelTransformedImportSegmentationTransform.Translate(-500.0, 0.0, 0.0)
- modelTransformedImportSegmentationTransformNode.ApplyTransformMatrix(modelTransformedImportSegmentationTransform.GetMatrix())
-
- # Import transformed model to segment in transformed segmentation
- modelTransformedImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImportTransformed')
- modelTransformedImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
- modelTransformedImportSegmentationNode.SetAndObserveTransformNodeID(modelTransformedImportSegmentationTransformNode.GetID())
- modelSegmentTranformed = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNodeTransformed, modelTransformedImportSegmentationNode)
- modelSegmentTranformed.UnRegister(None) # Need to release ownership
- self.assertIsNotNone(modelSegmentTranformed)
- modelSegmentTransformedPolyData = modelSegmentTranformed.GetRepresentation(self.closedSurfaceReprName)
- self.assertIsNotNone(modelSegmentTransformedPolyData)
- self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[0]), 1332)
- self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[1]), 1675)
-
- # Clean up temporary nodes
- slicer.mrmlScene.RemoveNode(bodyModelNode)
- slicer.mrmlScene.RemoveNode(bodyLabelmapNode)
- slicer.mrmlScene.RemoveNode(allSegmentsLabelmapNode)
- slicer.mrmlScene.RemoveNode(modelImportSegmentationNode)
- slicer.mrmlScene.RemoveNode(multiLabelImportSegmentationNode)
- slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode)
- slicer.mrmlScene.RemoveNode(bodyModelTransformNode)
- slicer.mrmlScene.RemoveNode(bodyModelNodeTransformed)
- slicer.mrmlScene.RemoveNode(bodyLabelmapNodeTransformed)
- slicer.mrmlScene.RemoveNode(modelTransformedImportSegmentationNode)
-
- def TestSection_ImportExportSegment2(self):
- # Testing sequential add of individual segments to a segmentation through ImportLabelmapToSegmentationNode
- logging.info('Test section: Import/export segment 2')
-
- # Export body segment to volume node
- bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
- bodyLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'BodyLabelmap')
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode)
- self.assertTrue(result)
- bodyImageData = bodyLabelmapNode.GetImageData()
- self.assertIsNotNone(bodyImageData)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(bodyImageData)
- imageStat.Update()
- self.assertEqual(imageStat.GetVoxelCount(), 792)
- self.assertEqual(imageStat.GetMin()[0], 0)
- self.assertEqual(imageStat.GetMax()[0], 1)
-
- # Export tumor segment to volume node
- tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName)
- tumorLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'TumorLabelmap')
- result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(tumorSegment, tumorLabelmapNode)
- self.assertTrue(result)
- tumorImageData = tumorLabelmapNode.GetImageData()
- self.assertIsNotNone(tumorImageData)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(tumorImageData)
- imageStat.Update()
- self.assertEqual(imageStat.GetVoxelCount(), 12)
- self.assertEqual(imageStat.GetMin()[0], 0)
- self.assertEqual(imageStat.GetMax()[0], 1)
-
- # Import single-label labelmap to segmentation
- singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport')
- singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
-
- bodySegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('BodyLabelmap')
- bodySegmentIDArray = vtk.vtkStringArray()
- bodySegmentIDArray.SetNumberOfValues(1)
- bodySegmentIDArray.SetValue(0, bodySegmentID)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(bodyLabelmapNode, singleLabelImportSegmentationNode, bodySegmentIDArray)
-
- self.assertTrue(result)
- self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1)
-
- tumorSegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('TumorLabelmap')
- tumorSegmentIDArray = vtk.vtkStringArray()
- tumorSegmentIDArray.SetNumberOfValues(1)
- tumorSegmentIDArray.SetValue(0, tumorSegmentID)
- result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(tumorLabelmapNode, singleLabelImportSegmentationNode, tumorSegmentIDArray)
- self.assertTrue(result)
- self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
-
- bodyLabelmap = slicer.vtkOrientedImageData()
- singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(bodySegmentID, bodyLabelmap)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(bodyLabelmap)
- imageStat.Update()
- self.assertEqual(imageStat.GetVoxelCount(), 792)
- self.assertEqual(imageStat.GetMin()[0], 0)
- self.assertEqual(imageStat.GetMax()[0], 1)
-
- tumorLabelmap = slicer.vtkOrientedImageData()
- singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(tumorSegmentID, tumorLabelmap)
- self.assertIsNotNone(tumorLabelmap)
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(tumorLabelmap)
- imageStat.Update()
- self.assertEqual(imageStat.GetVoxelCount(), 12)
- self.assertEqual(imageStat.GetMin()[0], 0)
- self.assertEqual(imageStat.GetMax()[0], 1)
-
- # Clean up temporary nodes
- slicer.mrmlScene.RemoveNode(bodyLabelmapNode)
- slicer.mrmlScene.RemoveNode(tumorLabelmapNode)
- slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode)
-
- # ------------------------------------------------------------------------------
- def TestSection_SubjectHierarchy(self):
- # Subject hierarchy plugin: item creation, removal, renaming
- logging.info('Test section: Subject hierarchy')
-
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- self.assertIsNotNone(shNode)
-
- # Check if subject hierarchy items have been created
- segmentationShItemID = shNode.GetItemByDataNode(self.inputSegmentationNode)
- self.assertIsNotNone(segmentationShItemID)
-
- bodyItemID = shNode.GetItemChildWithName(segmentationShItemID, self.bodySegmentName)
- self.assertIsNotNone(bodyItemID)
- tumorItemID = shNode.GetItemChildWithName(segmentationShItemID, self.tumorSegmentName)
- self.assertIsNotNone(tumorItemID)
- sphereItemID = shNode.GetItemChildWithName(segmentationShItemID, self.sphereSegmentName)
- self.assertIsNotNone(sphereItemID)
-
- # Rename segment
- bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
- bodySegment.SetName('Body')
- qt.QApplication.processEvents()
- self.assertEqual(shNode.GetItemName(bodyItemID), 'Body')
-
- tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName)
- shNode.SetItemName(tumorItemID, 'Tumor')
- qt.QApplication.processEvents()
- self.assertEqual(tumorSegment.GetName(), 'Tumor')
-
- # Remove segment
- self.inputSegmentationNode.GetSegmentation().RemoveSegment(bodySegment)
- qt.QApplication.processEvents()
- logging.info('(The error messages below are results of testing invalidity of objects, they are supposed to appear)')
- self.assertEqual(shNode.GetItemChildWithName(segmentationShItemID, 'Body'), 0)
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
-
- shNode.RemoveItem(tumorItemID)
- qt.QApplication.processEvents()
- self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1)
-
- # Remove segmentation
- slicer.mrmlScene.RemoveNode(self.inputSegmentationNode)
- self.assertEqual(shNode.GetItemName(segmentationShItemID), '')
- self.assertEqual(shNode.GetItemName(sphereItemID), '')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SegmentationsModuleTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_SegmentationsModuleTest1(self):
+ # Check for modules
+ self.assertIsNotNone(slicer.modules.segmentations)
+
+ self.TestSection_SetupPathsAndNames()
+ self.TestSection_RetrieveInputData()
+ self.TestSection_LoadInputData()
+ self.TestSection_AddRemoveSegment()
+ self.TestSection_MergeLabelmapWithDifferentGeometries()
+ self.TestSection_ImportExportSegment()
+ self.TestSection_ImportExportSegment2()
+ self.TestSection_SubjectHierarchy()
+
+ logging.info('Test finished')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_SetupPathsAndNames(self):
+ # Set up paths used for this test
+ self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest'
+ if not os.access(self.segmentationsModuleTestDir, os.F_OK):
+ os.mkdir(self.segmentationsModuleTestDir)
+
+ self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg'
+ if not os.access(self.dataDir, os.F_OK):
+ os.mkdir(self.dataDir)
+ self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg'
+
+ self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip'
+
+ # Define variables
+ self.expectedNumOfFilesInDataDir = 4
+ self.expectedNumOfFilesInDataSegDir = 2
+ self.inputSegmentationNode = None
+ self.bodySegmentName = 'Body_Contour'
+ self.tumorSegmentName = 'Tumor_Contour'
+ self.secondSegmentationNode = None
+ self.sphereSegment = None
+ self.sphereSegmentName = 'Sphere'
+ self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
+ self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()
+
+ # ------------------------------------------------------------------------------
+ def TestSection_RetrieveInputData(self):
+ try:
+ slicer.util.downloadAndExtractArchive(
+ TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
+ self.dataZipFilePath, self.segmentationsModuleTestDir,
+ checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7')
+
+ numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)])
+ self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir)
+ self.assertTrue(os.access(self.dataSegDir, os.F_OK))
+ numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)])
+ self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir)
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ logging.error('Test caused exception!\n' + str(e))
+
+ # ------------------------------------------------------------------------------
+ def TestSection_LoadInputData(self):
+ # Load into Slicer
+ slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd')
+ slicer.util.loadNodeFromFile(self.dataDir + '/TinyPatient_Structures.seg.vtm', "SegmentationFile", {})
+
+ # Change master representation to closed surface (so that conversion is possible when adding segment)
+ self.inputSegmentationNode = slicer.util.getNode('vtkMRMLSegmentationNode1')
+ self.inputSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_AddRemoveSegment(self):
+ # Add/remove segment from segmentation (check display properties, color table, etc.)
+ logging.info('Test section: Add/remove segment')
+
+ # Get baseline values
+ displayNode = self.inputSegmentationNode.GetDisplayNode()
+ self.assertIsNotNone(displayNode)
+ # If segments are not found then the returned color is the pre-defined invalid color
+ bodyColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName).GetColor()
+ logging.info(f"bodyColor: {bodyColor}")
+ self.assertEqual(int(bodyColor[0] * 100), 33)
+ self.assertEqual(int(bodyColor[1] * 100), 66)
+ self.assertEqual(int(bodyColor[2] * 100), 0)
+ tumorColor = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName).GetColor()
+ logging.info(f"tumorColor: {tumorColor}")
+ self.assertEqual(int(tumorColor[0] * 100), 100)
+ self.assertEqual(int(tumorColor[1] * 100), 0)
+ self.assertEqual(int(tumorColor[2] * 100), 0)
+
+ # Create new segment
+ sphere = vtk.vtkSphereSource()
+ sphere.SetCenter(0, 50, 0)
+ sphere.SetRadius(80)
+ sphere.Update()
+ spherePolyData = vtk.vtkPolyData()
+ spherePolyData.DeepCopy(sphere.GetOutput())
+
+ self.sphereSegment = vtkSegmentationCore.vtkSegment()
+ self.sphereSegment.SetName(self.sphereSegmentName)
+ self.sphereSegment.SetColor(0.0, 0.0, 1.0)
+ self.sphereSegment.AddRepresentation(self.closedSurfaceReprName, spherePolyData)
+
+ # Add segment to segmentation
+ self.inputSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment)
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
+
+ # Check merged labelmap
+ mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName)
+ self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(mergedLabelmap)
+ imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0)
+ imageStat.SetComponentOrigin(0, 0, 0)
+ imageStat.SetComponentSpacing(1, 1, 1)
+ imageStat.Update()
+ imageStatResult = imageStat.GetOutput()
+ for i in range(4):
+ logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
+ self.assertEqual(imageStat.GetVoxelCount(), 1000)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 4)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 40)
+
+ # Check if segment reorder is taken into account in merged labelmap generation
+ # Change segment order
+ sphereSegmentId = self.inputSegmentationNode.GetSegmentation().GetSegmentIdBySegment(self.sphereSegment)
+ self.inputSegmentationNode.GetSegmentation().SetSegmentIndex(sphereSegmentId, 1)
+ # Re-generate merged labelmap
+ self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
+ imageStat.SetInputData(mergedLabelmap)
+ imageStat.Update()
+ imageStatResult = imageStat.GetOutput()
+ for i in range(4):
+ logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
+ self.assertEqual(imageStat.GetVoxelCount(), 1000)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 786)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 170)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 39)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 5)
+
+ # Remove segment from segmentation
+ self.inputSegmentationNode.GetSegmentation().RemoveSegment(self.sphereSegmentName)
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_MergeLabelmapWithDifferentGeometries(self):
+ # Merge labelmap when segments containing labelmaps with different geometries (both same directions, different directions)
+ logging.info('Test section: Merge labelmap with different geometries')
+
+ self.assertIsNotNone(self.sphereSegment)
+ self.sphereSegment.RemoveRepresentation(self.binaryLabelmapReprName)
+ self.assertIsNone(self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName))
+
+ # Create new segmentation with sphere segment
+ self.secondSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'Second')
+ self.secondSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
+
+ self.secondSegmentationNode.GetSegmentation().AddSegment(self.sphereSegment)
+
+ # Check automatically converted labelmap. It is supposed to have the default geometry
+ # (which is different than the one in the input segmentation)
+ sphereLabelmap = self.sphereSegment.GetRepresentation(self.binaryLabelmapReprName)
+ self.assertIsNotNone(sphereLabelmap)
+ sphereLabelmapSpacing = sphereLabelmap.GetSpacing()
+ self.assertAlmostEqual(sphereLabelmapSpacing[0], 0.629257364931788, 8)
+ self.assertAlmostEqual(sphereLabelmapSpacing[1], 0.629257364931788, 8)
+ self.assertAlmostEqual(sphereLabelmapSpacing[2], 0.629257364931788, 8)
+
+ # Create binary labelmap in segmentation that will create the merged labelmap from
+ # different geometries so that labelmap is not removed from sphere segment when adding
+ self.inputSegmentationNode.GetSegmentation().CreateRepresentation(self.binaryLabelmapReprName)
+
+ # Copy segment to input segmentation
+ self.inputSegmentationNode.GetSegmentation().CopySegmentFromSegmentation(self.secondSegmentationNode.GetSegmentation(), self.sphereSegmentName)
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
+
+ # Check merged labelmap
+ # Reference geometry has the tiny patient spacing, and it is oversampled to have similar
+ # voxel size as the sphere labelmap with the uniform 0.629mm spacing
+ mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ self.inputSegmentationNode.GenerateMergedLabelmapForAllSegments(mergedLabelmap, 0)
+ mergedLabelmapSpacing = mergedLabelmap.GetSpacing()
+ self.assertAlmostEqual(mergedLabelmapSpacing[0], 0.80327868852459, 8)
+ self.assertAlmostEqual(mergedLabelmapSpacing[1], 0.80327868852459, 8)
+ self.assertAlmostEqual(mergedLabelmapSpacing[2], 0.377049180327869, 8)
+
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(mergedLabelmap)
+ imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0)
+ imageStat.SetComponentOrigin(0, 0, 0)
+ imageStat.SetComponentSpacing(1, 1, 1)
+ imageStat.Update()
+ imageStatResult = imageStat.GetOutput()
+ for i in range(5):
+ logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
+ self.assertEqual(imageStat.GetVoxelCount(), 226981000)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 178838889)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(4, 0, 0, 0), 0) # Built from color table and color four is removed in previous test section
+
+ # ------------------------------------------------------------------------------
+ def TestSection_ImportExportSegment(self):
+ # Import/export, both one label and all labels
+ logging.info('Test section: Import/export segment')
+
+ # Export single segment to model node
+ bodyModelNode = slicer.vtkMRMLModelNode()
+ bodyModelNode.SetName('BodyModel')
+ slicer.mrmlScene.AddNode(bodyModelNode)
+
+ bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNode)
+ self.assertTrue(result)
+ self.assertIsNotNone(bodyModelNode.GetPolyData())
+ # TODO: Number of points increased to 1677 due to end-capping, need to investigate!
+ # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfPoints(), 302)
+ # TODO: On Linux and Windows it is 588, on Mac it is 580. Need to investigate
+ # self.assertEqual(bodyModelNode.GetPolyData().GetNumberOfCells(), 588)
+ # self.assertTrue(bodyModelNode.GetPolyData().GetNumberOfCells() == 588 or bodyModelNode.GetPolyData().GetNumberOfCells() == 580)
+
+ # Export single segment to volume node
+ bodyLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
+ bodyLabelmapNode.SetName('BodyLabelmap')
+ slicer.mrmlScene.AddNode(bodyLabelmapNode)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode)
+ self.assertTrue(result)
+ bodyImageData = bodyLabelmapNode.GetImageData()
+ self.assertIsNotNone(bodyImageData)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(bodyImageData)
+ imageStat.Update()
+ self.assertEqual(imageStat.GetVoxelCount(), 792)
+ self.assertEqual(imageStat.GetMin()[0], 0)
+ self.assertEqual(imageStat.GetMax()[0], 1)
+
+ # Export multiple segments to volume node
+ allSegmentsLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
+ allSegmentsLabelmapNode.SetName('AllSegmentsLabelmap')
+ slicer.mrmlScene.AddNode(allSegmentsLabelmapNode)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportAllSegmentsToLabelmapNode(self.inputSegmentationNode, allSegmentsLabelmapNode)
+ self.assertTrue(result)
+ allSegmentsImageData = allSegmentsLabelmapNode.GetImageData()
+ self.assertIsNotNone(allSegmentsImageData)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(allSegmentsImageData)
+ imageStat.SetComponentExtent(0, 5, 0, 0, 0, 0)
+ imageStat.SetComponentOrigin(0, 0, 0)
+ imageStat.SetComponentSpacing(1, 1, 1)
+ imageStat.Update()
+ imageStatResult = imageStat.GetOutput()
+ for i in range(4):
+ logging.info(f"Volume {i}: {imageStatResult.GetScalarComponentAsDouble(i,0,0,0)}")
+ self.assertEqual(imageStat.GetVoxelCount(), 127109360)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(0, 0, 0, 0), 78967249)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(1, 0, 0, 0), 39705288)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(2, 0, 0, 0), 890883)
+ self.assertEqual(imageStatResult.GetScalarComponentAsDouble(3, 0, 0, 0), 7545940)
+ # Import model to segment
+ modelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImport')
+ modelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
+ modelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNode)
+ modelSegment.UnRegister(None) # Need to release ownership
+ self.assertIsNotNone(modelSegment)
+ self.assertIsNotNone(modelSegment.GetRepresentation(self.closedSurfaceReprName))
+
+ # Import multi-label labelmap to segmentation
+ multiLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'MultiLabelImport')
+ multiLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(allSegmentsLabelmapNode, multiLabelImportSegmentationNode)
+ self.assertTrue(result)
+ self.assertEqual(multiLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 3)
+
+ # Import labelmap into single segment
+ singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport')
+ singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
+ # Should not import multi-label labelmap to segment
+ nullSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode)
+ self.assertIsNone(nullSegment)
+ logging.info('(This error message is a result of testing an impossible scenario, it is supposed to appear)')
+ # Make labelmap single-label and import again
+ threshold = vtk.vtkImageThreshold()
+ threshold.SetInValue(0)
+ threshold.SetOutValue(1)
+ threshold.ReplaceInOn()
+ threshold.ThresholdByLower(0)
+ threshold.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+ threshold.SetInputData(allSegmentsLabelmapNode.GetImageData())
+ threshold.Update()
+ allSegmentsLabelmapNode.GetImageData().ShallowCopy(threshold.GetOutput())
+ labelSegment = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromLabelmapVolumeNode(allSegmentsLabelmapNode)
+ labelSegment.UnRegister(None) # Need to release ownership
+ self.assertIsNotNone(labelSegment)
+ self.assertIsNotNone(labelSegment.GetRepresentation(self.binaryLabelmapReprName))
+
+ # Import/export with transforms
+ logging.info('Test subsection: Import/export with transforms')
+
+ # Create transform node that will be used to transform the tested nodes
+ bodyModelTransformNode = slicer.vtkMRMLLinearTransformNode()
+ slicer.mrmlScene.AddNode(bodyModelTransformNode)
+ bodyModelTransform = vtk.vtkTransform()
+ bodyModelTransform.Translate(1000.0, 0.0, 0.0)
+ bodyModelTransformNode.ApplyTransformMatrix(bodyModelTransform.GetMatrix())
+
+ # Set transform as parent to input segmentation node
+ self.inputSegmentationNode.SetAndObserveTransformNodeID(bodyModelTransformNode.GetID())
+
+ # Export single segment to model node from transformed segmentation
+ bodyModelNodeTransformed = slicer.vtkMRMLModelNode()
+ bodyModelNodeTransformed.SetName('BodyModelTransformed')
+ slicer.mrmlScene.AddNode(bodyModelNodeTransformed)
+ bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyModelNodeTransformed)
+ self.assertTrue(result)
+ self.assertIsNotNone(bodyModelNodeTransformed.GetParentTransformNode())
+
+ # Export single segment to volume node from transformed segmentation
+ bodyLabelmapNodeTransformed = slicer.vtkMRMLLabelMapVolumeNode()
+ bodyLabelmapNodeTransformed.SetName('BodyLabelmapTransformed')
+ slicer.mrmlScene.AddNode(bodyLabelmapNodeTransformed)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNodeTransformed)
+ self.assertTrue(result)
+ self.assertIsNotNone(bodyLabelmapNodeTransformed.GetParentTransformNode())
+
+ # Create transform node that will be used to transform the tested nodes
+ modelTransformedImportSegmentationTransformNode = slicer.vtkMRMLLinearTransformNode()
+ slicer.mrmlScene.AddNode(modelTransformedImportSegmentationTransformNode)
+ modelTransformedImportSegmentationTransform = vtk.vtkTransform()
+ modelTransformedImportSegmentationTransform.Translate(-500.0, 0.0, 0.0)
+ modelTransformedImportSegmentationTransformNode.ApplyTransformMatrix(modelTransformedImportSegmentationTransform.GetMatrix())
+
+ # Import transformed model to segment in transformed segmentation
+ modelTransformedImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'ModelImportTransformed')
+ modelTransformedImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.closedSurfaceReprName)
+ modelTransformedImportSegmentationNode.SetAndObserveTransformNodeID(modelTransformedImportSegmentationTransformNode.GetID())
+ modelSegmentTranformed = slicer.vtkSlicerSegmentationsModuleLogic.CreateSegmentFromModelNode(bodyModelNodeTransformed, modelTransformedImportSegmentationNode)
+ modelSegmentTranformed.UnRegister(None) # Need to release ownership
+ self.assertIsNotNone(modelSegmentTranformed)
+ modelSegmentTransformedPolyData = modelSegmentTranformed.GetRepresentation(self.closedSurfaceReprName)
+ self.assertIsNotNone(modelSegmentTransformedPolyData)
+ self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[0]), 1332)
+ self.assertEqual(int(modelSegmentTransformedPolyData.GetBounds()[1]), 1675)
+
+ # Clean up temporary nodes
+ slicer.mrmlScene.RemoveNode(bodyModelNode)
+ slicer.mrmlScene.RemoveNode(bodyLabelmapNode)
+ slicer.mrmlScene.RemoveNode(allSegmentsLabelmapNode)
+ slicer.mrmlScene.RemoveNode(modelImportSegmentationNode)
+ slicer.mrmlScene.RemoveNode(multiLabelImportSegmentationNode)
+ slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode)
+ slicer.mrmlScene.RemoveNode(bodyModelTransformNode)
+ slicer.mrmlScene.RemoveNode(bodyModelNodeTransformed)
+ slicer.mrmlScene.RemoveNode(bodyLabelmapNodeTransformed)
+ slicer.mrmlScene.RemoveNode(modelTransformedImportSegmentationNode)
+
+ def TestSection_ImportExportSegment2(self):
+ # Testing sequential add of individual segments to a segmentation through ImportLabelmapToSegmentationNode
+ logging.info('Test section: Import/export segment 2')
+
+ # Export body segment to volume node
+ bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
+ bodyLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'BodyLabelmap')
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(bodySegment, bodyLabelmapNode)
+ self.assertTrue(result)
+ bodyImageData = bodyLabelmapNode.GetImageData()
+ self.assertIsNotNone(bodyImageData)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(bodyImageData)
+ imageStat.Update()
+ self.assertEqual(imageStat.GetVoxelCount(), 792)
+ self.assertEqual(imageStat.GetMin()[0], 0)
+ self.assertEqual(imageStat.GetMax()[0], 1)
+
+ # Export tumor segment to volume node
+ tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName)
+ tumorLabelmapNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLLabelMapVolumeNode', 'TumorLabelmap')
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentToRepresentationNode(tumorSegment, tumorLabelmapNode)
+ self.assertTrue(result)
+ tumorImageData = tumorLabelmapNode.GetImageData()
+ self.assertIsNotNone(tumorImageData)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(tumorImageData)
+ imageStat.Update()
+ self.assertEqual(imageStat.GetVoxelCount(), 12)
+ self.assertEqual(imageStat.GetMin()[0], 0)
+ self.assertEqual(imageStat.GetMax()[0], 1)
+
+ # Import single-label labelmap to segmentation
+ singleLabelImportSegmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode', 'SingleLabelImport')
+ singleLabelImportSegmentationNode.GetSegmentation().SetMasterRepresentationName(self.binaryLabelmapReprName)
+
+ bodySegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('BodyLabelmap')
+ bodySegmentIDArray = vtk.vtkStringArray()
+ bodySegmentIDArray.SetNumberOfValues(1)
+ bodySegmentIDArray.SetValue(0, bodySegmentID)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(bodyLabelmapNode, singleLabelImportSegmentationNode, bodySegmentIDArray)
+
+ self.assertTrue(result)
+ self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1)
+
+ tumorSegmentID = singleLabelImportSegmentationNode.GetSegmentation().AddEmptySegment('TumorLabelmap')
+ tumorSegmentIDArray = vtk.vtkStringArray()
+ tumorSegmentIDArray.SetNumberOfValues(1)
+ tumorSegmentIDArray.SetValue(0, tumorSegmentID)
+ result = slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(tumorLabelmapNode, singleLabelImportSegmentationNode, tumorSegmentIDArray)
+ self.assertTrue(result)
+ self.assertEqual(singleLabelImportSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
+
+ bodyLabelmap = slicer.vtkOrientedImageData()
+ singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(bodySegmentID, bodyLabelmap)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(bodyLabelmap)
+ imageStat.Update()
+ self.assertEqual(imageStat.GetVoxelCount(), 792)
+ self.assertEqual(imageStat.GetMin()[0], 0)
+ self.assertEqual(imageStat.GetMax()[0], 1)
+
+ tumorLabelmap = slicer.vtkOrientedImageData()
+ singleLabelImportSegmentationNode.GetBinaryLabelmapRepresentation(tumorSegmentID, tumorLabelmap)
+ self.assertIsNotNone(tumorLabelmap)
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(tumorLabelmap)
+ imageStat.Update()
+ self.assertEqual(imageStat.GetVoxelCount(), 12)
+ self.assertEqual(imageStat.GetMin()[0], 0)
+ self.assertEqual(imageStat.GetMax()[0], 1)
+
+ # Clean up temporary nodes
+ slicer.mrmlScene.RemoveNode(bodyLabelmapNode)
+ slicer.mrmlScene.RemoveNode(tumorLabelmapNode)
+ slicer.mrmlScene.RemoveNode(singleLabelImportSegmentationNode)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_SubjectHierarchy(self):
+ # Subject hierarchy plugin: item creation, removal, renaming
+ logging.info('Test section: Subject hierarchy')
+
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ self.assertIsNotNone(shNode)
+
+ # Check if subject hierarchy items have been created
+ segmentationShItemID = shNode.GetItemByDataNode(self.inputSegmentationNode)
+ self.assertIsNotNone(segmentationShItemID)
+
+ bodyItemID = shNode.GetItemChildWithName(segmentationShItemID, self.bodySegmentName)
+ self.assertIsNotNone(bodyItemID)
+ tumorItemID = shNode.GetItemChildWithName(segmentationShItemID, self.tumorSegmentName)
+ self.assertIsNotNone(tumorItemID)
+ sphereItemID = shNode.GetItemChildWithName(segmentationShItemID, self.sphereSegmentName)
+ self.assertIsNotNone(sphereItemID)
+
+ # Rename segment
+ bodySegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.bodySegmentName)
+ bodySegment.SetName('Body')
+ qt.QApplication.processEvents()
+ self.assertEqual(shNode.GetItemName(bodyItemID), 'Body')
+
+ tumorSegment = self.inputSegmentationNode.GetSegmentation().GetSegment(self.tumorSegmentName)
+ shNode.SetItemName(tumorItemID, 'Tumor')
+ qt.QApplication.processEvents()
+ self.assertEqual(tumorSegment.GetName(), 'Tumor')
+
+ # Remove segment
+ self.inputSegmentationNode.GetSegmentation().RemoveSegment(bodySegment)
+ qt.QApplication.processEvents()
+ logging.info('(The error messages below are results of testing invalidity of objects, they are supposed to appear)')
+ self.assertEqual(shNode.GetItemChildWithName(segmentationShItemID, 'Body'), 0)
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 2)
+
+ shNode.RemoveItem(tumorItemID)
+ qt.QApplication.processEvents()
+ self.assertEqual(self.inputSegmentationNode.GetSegmentation().GetNumberOfSegments(), 1)
+
+ # Remove segmentation
+ slicer.mrmlScene.RemoveNode(self.inputSegmentationNode)
+ self.assertEqual(shNode.GetItemName(segmentationShItemID), '')
+ self.assertEqual(shNode.GetItemName(sphereItemID), '')
diff --git a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py
index e9aa3eab3c9..1ca22dbd13e 100644
--- a/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py
+++ b/Modules/Loadable/Segmentations/Testing/Python/SegmentationsModuleTest2.py
@@ -18,501 +18,501 @@
class SegmentationsModuleTest2(unittest.TestCase):
- # ------------------------------------------------------------------------------
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- # ------------------------------------------------------------------------------
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_SegmentationsModuleTest2()
-
- # ------------------------------------------------------------------------------
- def test_SegmentationsModuleTest2(self):
- # Check for modules
- self.assertIsNotNone(slicer.modules.segmentations)
- self.assertIsNotNone(slicer.modules.segmenteditor)
-
- # Run tests
- self.TestSection_SetupPathsAndNames()
- self.TestSection_RetrieveInputData()
- self.TestSection_SetupScene()
- self.TestSection_SharedLabelmapMultipleLayerEditing()
- self.TestSection_IslandEffects()
- self.TestSection_MarginEffects()
- self.TestSection_MaskingSettings()
- logging.info('Test finished')
-
- # ------------------------------------------------------------------------------
- def TestSection_SetupPathsAndNames(self):
- # Set up paths used for this test
- self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest'
- if not os.access(self.segmentationsModuleTestDir, os.F_OK):
- os.mkdir(self.segmentationsModuleTestDir)
-
- self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg'
- if not os.access(self.dataDir, os.F_OK):
- os.mkdir(self.dataDir)
- self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg'
-
- self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip'
-
- # Define variables
- self.expectedNumOfFilesInDataDir = 4
- self.expectedNumOfFilesInDataSegDir = 2
- self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
- self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()
-
- # ------------------------------------------------------------------------------
- def TestSection_RetrieveInputData(self):
- try:
- slicer.util.downloadAndExtractArchive(
- TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
- self.dataZipFilePath, self.segmentationsModuleTestDir,
- checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7')
-
- numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)])
- self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir)
- self.assertTrue(os.access(self.dataSegDir, os.F_OK))
- numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)])
- self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir)
-
- except Exception as e:
- import traceback
- traceback.print_exc()
- logging.error('Test caused exception!\n' + str(e))
-
- # ------------------------------------------------------------------------------
- def TestSection_SetupScene(self):
- self.paintEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Paint")
- self.eraseEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Erase")
- self.islandEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Islands")
- self.thresholdEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Threshold")
-
- self.segmentEditorNode = slicer.util.getNode("SegmentEditor")
- self.assertIsNotNone(self.segmentEditorNode)
-
- self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
- self.assertIsNotNone(self.segmentationNode)
- self.segmentEditorNode.SetAndObserveSegmentationNode(self.segmentationNode)
-
- self.masterVolumeNode = slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd')
- self.assertIsNotNone(self.masterVolumeNode)
- self.segmentEditorNode.SetAndObserveMasterVolumeNode(self.masterVolumeNode)
-
- self.segmentation = self.segmentationNode.GetSegmentation()
- self.segmentation.SetMasterRepresentationName(self.binaryLabelmapReprName)
- self.assertIsNotNone(self.segmentation)
-
- # ------------------------------------------------------------------------------
- def TestSection_SharedLabelmapMultipleLayerEditing(self):
- self.segmentation.RemoveAllSegments()
- self.segmentation.AddEmptySegment("Segment_1")
- self.segmentation.AddEmptySegment("Segment_2")
-
- defaultModifierLabelmap = self.paintEffect.defaultModifierLabelmap()
- self.ijkToRas = vtk.vtkMatrix4x4()
- defaultModifierLabelmap.GetImageToWorldMatrix(self.ijkToRas)
-
- mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- mergedLabelmap.SetExtent(0, 10, 0, 10, 0, 10)
- mergedLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- mergedLabelmap.GetPointData().GetScalars().Fill(1)
-
- oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
-
- self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteAllSegments)
- self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
- self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
- self.segmentEditorNode.SetSelectedSegmentID("Segment_2")
- self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
-
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 1)
-
- self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone)
- self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
- self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 2)
-
- self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
- logging.info('Multiple layer editing successful')
-
- # ------------------------------------------------------------------------------
- def TestSection_IslandEffects(self):
- islandSizes = [1, 26, 11, 6, 8, 6, 2]
- islandSizes.sort(reverse=True)
-
- minimumSize = 3
- self.resetIslandSegments(islandSizes)
- self.islandEffect.setParameter('MinimumSize', minimumSize)
- self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND')
- self.islandEffect.self().onApply()
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 1)
-
- voxelCount = 0
- for size in islandSizes:
- if size < minimumSize:
- continue
- voxelCount = max(voxelCount, size)
- self.checkSegmentVoxelCount(0, voxelCount)
-
- minimumSize = 7
- self.resetIslandSegments(islandSizes)
- self.islandEffect.setParameter('MinimumSize', minimumSize)
- self.islandEffect.setParameter('Operation', 'REMOVE_SMALL_ISLANDS')
- self.islandEffect.self().onApply()
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 1)
-
- voxelCount = 0
- for size in islandSizes:
- if size < minimumSize:
- continue
- voxelCount += size
- self.checkSegmentVoxelCount(0, voxelCount)
-
- self.resetIslandSegments(islandSizes)
- minimumSize = 3
- self.islandEffect.setParameter('MinimumSize', minimumSize)
- self.islandEffect.setParameter('Operation', 'SPLIT_ISLANDS_TO_SEGMENTS')
- self.islandEffect.self().onApply()
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 1)
-
- for i in range(len(islandSizes)):
- size = islandSizes[i]
- if size < minimumSize:
- continue
- self.checkSegmentVoxelCount(i, size)
-
- # ------------------------------------------------------------------------------
- def resetIslandSegments(self, islandSizes):
- self.segmentation.RemoveAllSegments()
-
- totalSize = 0
- voxelSizeSum = 0
- for size in islandSizes:
- totalSize += size + 1
- voxelSizeSum += size
-
- mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- mergedLabelmapExtent = [0, totalSize - 1, 0, 0, 0, 0]
- self.setupIslandLabelmap(mergedLabelmap, mergedLabelmapExtent, 0)
-
- emptySegment = slicer.vtkSegment()
- emptySegment.SetName("Segment_1")
- emptySegment.AddRepresentation(self.binaryLabelmapReprName, mergedLabelmap)
- self.segmentation.AddSegment(emptySegment)
- self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
-
- startExtent = 0
- for size in islandSizes:
- islandLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- islandLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- islandExtent = [startExtent, startExtent + size - 1, 0, 0, 0, 0]
- self.setupIslandLabelmap(islandLabelmap, islandExtent)
- self.paintEffect.modifySelectedSegmentByLabelmap(islandLabelmap, self.paintEffect.ModificationModeAdd)
- startExtent += size + 1
- self.checkSegmentVoxelCount(0, voxelSizeSum)
-
- layerCount = self.segmentation.GetNumberOfLayers()
- self.assertEqual(layerCount, 1)
-
- # ------------------------------------------------------------------------------
- def checkSegmentVoxelCount(self, segmentIndex, expectedVoxelCount):
- segment = self.segmentation.GetNthSegment(segmentIndex)
- self.assertIsNotNone(segment)
-
- labelmap = slicer.vtkOrientedImageData()
- labelmap.SetImageToWorldMatrix(self.ijkToRas)
- segmentID = self.segmentation.GetNthSegmentID(segmentIndex)
- self.segmentationNode.GetBinaryLabelmapRepresentation(segmentID, labelmap)
-
- imageStat = vtk.vtkImageAccumulate()
- imageStat.SetInputData(labelmap)
- imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0)
- imageStat.SetComponentOrigin(0, 0, 0)
- imageStat.SetComponentSpacing(1, 1, 1)
- imageStat.IgnoreZeroOn()
- imageStat.Update()
-
- self.assertEqual(imageStat.GetVoxelCount(), expectedVoxelCount)
-
- # ------------------------------------------------------------------------------
- def setupIslandLabelmap(self, labelmap, extent, value=1):
- labelmap.SetExtent(extent)
- labelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- labelmap.GetPointData().GetScalars().Fill(value)
-
- # ------------------------------------------------------------------------------
- def TestSection_MarginEffects(self):
- logging.info("Running test on margin effect")
-
- slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin")
-
- self.segmentation.RemoveAllSegments()
- segment1Id = self.segmentation.AddEmptySegment("Segment_1")
- segment1 = self.segmentation.GetSegment(segment1Id)
- segment1.SetLabelValue(1)
- self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
-
- segment2Id = self.segmentation.AddEmptySegment("Segment_2")
- segment2 = self.segmentation.GetSegment(segment2Id)
- segment2.SetLabelValue(2)
-
- binaryLabelmapRepresentationName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName()
- dataTypes = [
- vtk.VTK_CHAR,
- vtk.VTK_SIGNED_CHAR,
- vtk.VTK_UNSIGNED_CHAR,
- vtk.VTK_SHORT,
- vtk.VTK_UNSIGNED_SHORT,
- vtk.VTK_INT,
- vtk.VTK_UNSIGNED_INT,
- # vtk.VTK_LONG, # On linux, VTK_LONG has the same size as VTK_LONG_LONG. This causes issues in vtkImageThreshold.
- # vtk.VTK_UNSIGNED_LONG, See https://github.com/Slicer/Slicer/issues/5427
- # vtk.VTK_FLOAT, # Since float can't represent all int, we jump straight to double.
- vtk.VTK_DOUBLE,
- # vtk.VTK_LONG_LONG, # These types are unsupported in ITK
- # vtk.VTK_UNSIGNED_LONG_LONG,
- ]
- logging.info("Testing shared labelmaps")
- for dataType in dataTypes:
- initialLabelmap = slicer.vtkOrientedImageData()
- initialLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- initialLabelmap.SetExtent(0, 10, 0, 10, 0, 10)
- initialLabelmap.AllocateScalars(dataType, 1)
- initialLabelmap.GetPointData().GetScalars().Fill(0)
- segment1.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap)
- segment2.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap)
-
- self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments)
- self.assertEqual(self.segmentation.GetNumberOfLayers(), 1)
- self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone)
- self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
-
- logging.info("Testing separate labelmaps")
- for dataType in dataTypes:
- segment1Labelmap = slicer.vtkOrientedImageData()
- segment1Labelmap.SetImageToWorldMatrix(self.ijkToRas)
- segment1Labelmap.SetExtent(0, 10, 0, 10, 0, 10)
- segment1Labelmap.AllocateScalars(dataType, 1)
- segment1Labelmap.GetPointData().GetScalars().Fill(0)
- segment1.AddRepresentation(binaryLabelmapRepresentationName, segment1Labelmap)
-
- segment2Labelmap = slicer.vtkOrientedImageData()
- segment2Labelmap.DeepCopy(segment1Labelmap)
- segment2.AddRepresentation(binaryLabelmapRepresentationName, segment2Labelmap)
-
- self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments)
- self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
- self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone)
- self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
-
- # ------------------------------------------------------------------------------
- def runMarginEffect(self, segment1, segment2, dataType, overwriteMode):
- logging.info(f"Running margin effect with data type: {dataType}, and overwriteMode {overwriteMode}")
- marginEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin")
-
- marginEffect.setParameter("MarginSizeMm", 50.0)
-
- oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
- self.segmentEditorNode.SetOverwriteMode(overwriteMode)
-
- segment1Labelmap = segment1.GetRepresentation(self.binaryLabelmapReprName)
- segment1Labelmap.AllocateScalars(dataType, 1)
- segment1Labelmap.GetPointData().GetScalars().Fill(0)
-
- segment2Labelmap = segment2.GetRepresentation(self.binaryLabelmapReprName)
- segment2Labelmap.AllocateScalars(dataType, 1)
- segment2Labelmap.GetPointData().GetScalars().Fill(0)
-
- segment1Position_IJK = [5, 5, 5]
- segment1Labelmap.SetScalarComponentFromDouble(segment1Position_IJK[0], segment1Position_IJK[1], segment1Position_IJK[2], 0, segment1.GetLabelValue())
- segment2Position_IJK = [6, 5, 6]
- segment2Labelmap.SetScalarComponentFromDouble(segment2Position_IJK[0], segment2Position_IJK[1], segment2Position_IJK[2], 0, segment2.GetLabelValue())
-
- self.checkSegmentVoxelCount(0, 1)
- self.checkSegmentVoxelCount(1, 1)
-
- marginEffect.self().onApply()
- self.checkSegmentVoxelCount(0, 9) # Margin grow
- self.checkSegmentVoxelCount(1, 1)
-
- marginEffect.self().onApply()
- self.checkSegmentVoxelCount(0, 37) # Margin grow
- if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments:
- self.checkSegmentVoxelCount(1, 0) # Overwritten
- else:
- self.checkSegmentVoxelCount(1, 1) # Not overwritten
-
- marginEffect.setParameter("MarginSizeMm", -50.0)
- marginEffect.self().onApply()
-
- self.checkSegmentVoxelCount(0, 9) # Margin shrink
- if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments:
- self.checkSegmentVoxelCount(1, 0) # Overwritten
- else:
- self.checkSegmentVoxelCount(1, 1) # Not overwritten
-
- self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
-
- # ------------------------------------------------------------------------------
- def TestSection_MaskingSettings(self):
- self.segmentation.RemoveAllSegments()
- segment1Id = self.segmentation.AddEmptySegment("Segment_1")
- segment2Id = self.segmentation.AddEmptySegment("Segment_2")
- segment3Id = self.segmentation.AddEmptySegment("Segment_3")
- segment4Id = self.segmentation.AddEmptySegment("Segment_4")
-
- oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
-
- # -------------------
- # Test applying threshold with no masking
- self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
- self.thresholdEffect.setParameter("MinimumThreshold", "-17")
- self.thresholdEffect.setParameter("MaximumThreshold", "848")
- self.thresholdEffect.self().onApply()
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.checkSegmentVoxelCount(1, 0) # Segment_2
-
- # -------------------
- # Add paint to segment 2. No overwrite
- paintModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- paintModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- paintModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
- paintModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- paintModifierLabelmap.GetPointData().GetScalars().Fill(1)
-
- self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone)
- self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
- self.paintEffect.modifySelectedSegmentByLabelmap(paintModifierLabelmap, self.paintEffect.ModificationModeAdd)
-
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.checkSegmentVoxelCount(1, 64) # Segment_2
-
- # -------------------
- # Test erasing with no masking
- eraseModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- eraseModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- eraseModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
- eraseModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- eraseModifierLabelmap.GetPointData().GetScalars().Fill(1)
-
- self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
- self.checkSegmentVoxelCount(0, 177) # Segment_1
- self.checkSegmentVoxelCount(1, 64) # Segment_2
-
- # -------------------
- # Test erasing with masking on empty segment
- self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
- self.thresholdEffect.self().onApply() # Reset Segment_1
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment)
- self.segmentEditorNode.SetMaskSegmentID(segment2Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
- self.checkSegmentVoxelCount(0, 177) # We expect to be able to erase the current segment regardless of masking
- self.checkSegmentVoxelCount(1, 64) # Segment_2
-
- # -------------------
- # Test erasing with masking on the same segment
- self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
- self.thresholdEffect.self().onApply() # Reset Segment_1
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.segmentEditorNode.SetMaskSegmentID(segment1Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
- self.checkSegmentVoxelCount(0, 177) # Segment_1
- self.checkSegmentVoxelCount(1, 64) # Segment_2
-
- # -------------------
- # Test erasing all segments
- self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere)
- self.thresholdEffect.self().onApply() # Reset Segment_1
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemoveAll)
- self.checkSegmentVoxelCount(0, 177) # Segment_1
- self.checkSegmentVoxelCount(1, 0) # Segment_2
-
- # -------------------
- # Test adding back segments
- self.thresholdEffect.self().onApply() # Reset Segment_1
- self.checkSegmentVoxelCount(0, 204) # Segment_1
- self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment)
- self.segmentEditorNode.SetMaskSegmentID(segment2Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
- self.checkSegmentVoxelCount(0, 177) # Segment_1
- self.checkSegmentVoxelCount(1, 27) # Segment_2
-
- # -------------------
- # Test threshold effect segment mask
- self.segmentEditorNode.SetMaskSegmentID(segment2Id) # Erase Segment_2
- self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
- self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
- self.segmentEditorNode.SetMaskSegmentID(segment1Id)
- self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
- self.thresholdEffect.self().onApply() # Threshold Segment_2 within Segment_1
- self.checkSegmentVoxelCount(0, 177) # Segment_1
- self.checkSegmentVoxelCount(1, 177) # Segment_2
-
- # -------------------
- # Test intensity masking with segment mask
- self.segmentEditorNode.MasterVolumeIntensityMaskOn()
- self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848)
- self.thresholdEffect.setParameter("MinimumThreshold", "-99999")
- self.thresholdEffect.setParameter("MaximumThreshold", "99999")
- self.segmentEditorNode.SetSelectedSegmentID(segment3Id)
- self.thresholdEffect.self().onApply() # Threshold Segment_3
- self.checkSegmentVoxelCount(2, 177) # Segment_3
-
- # -------------------
- # Test intensity masking with islands
- self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere)
- self.segmentEditorNode.MasterVolumeIntensityMaskOff()
- self.segmentEditorNode.SetSelectedSegmentID(segment4Id)
-
- island1ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- island1ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- island1ModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
- island1ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- island1ModifierLabelmap.GetPointData().GetScalars().Fill(1)
- self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd)
-
- island2ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- island2ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
- island2ModifierLabelmap.SetExtent(7, 9, 7, 9, 7, 9)
- island2ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- island2ModifierLabelmap.GetPointData().GetScalars().Fill(1)
- self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd)
- self.checkSegmentVoxelCount(3, 91) # Segment_4
-
- # Test that no masking works as expected
- minimumSize = 3
- self.islandEffect.setParameter('MinimumSize', minimumSize)
- self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND')
- self.islandEffect.self().onApply()
- self.checkSegmentVoxelCount(3, 64) # Segment_4
-
- # Reset Segment_4 islands
- self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd)
- self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd)
-
- # Test intensity masking
- self.segmentEditorNode.MasterVolumeIntensityMaskOn()
- self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848)
- self.islandEffect.self().onApply()
- self.checkSegmentVoxelCount(3, 87) # Segment_4
-
- # Restore old overwrite setting
- self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
- self.segmentEditorNode.MasterVolumeIntensityMaskOff()
+ # ------------------------------------------------------------------------------
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ # ------------------------------------------------------------------------------
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SegmentationsModuleTest2()
+
+ # ------------------------------------------------------------------------------
+ def test_SegmentationsModuleTest2(self):
+ # Check for modules
+ self.assertIsNotNone(slicer.modules.segmentations)
+ self.assertIsNotNone(slicer.modules.segmenteditor)
+
+ # Run tests
+ self.TestSection_SetupPathsAndNames()
+ self.TestSection_RetrieveInputData()
+ self.TestSection_SetupScene()
+ self.TestSection_SharedLabelmapMultipleLayerEditing()
+ self.TestSection_IslandEffects()
+ self.TestSection_MarginEffects()
+ self.TestSection_MaskingSettings()
+ logging.info('Test finished')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_SetupPathsAndNames(self):
+ # Set up paths used for this test
+ self.segmentationsModuleTestDir = slicer.app.temporaryPath + '/SegmentationsModuleTest'
+ if not os.access(self.segmentationsModuleTestDir, os.F_OK):
+ os.mkdir(self.segmentationsModuleTestDir)
+
+ self.dataDir = self.segmentationsModuleTestDir + '/TinyPatient_Seg'
+ if not os.access(self.dataDir, os.F_OK):
+ os.mkdir(self.dataDir)
+ self.dataSegDir = self.dataDir + '/TinyPatient_Structures.seg'
+
+ self.dataZipFilePath = self.segmentationsModuleTestDir + '/TinyPatient_Seg.zip'
+
+ # Define variables
+ self.expectedNumOfFilesInDataDir = 4
+ self.expectedNumOfFilesInDataSegDir = 2
+ self.closedSurfaceReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
+ self.binaryLabelmapReprName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName()
+
+ # ------------------------------------------------------------------------------
+ def TestSection_RetrieveInputData(self):
+ try:
+ slicer.util.downloadAndExtractArchive(
+ TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
+ self.dataZipFilePath, self.segmentationsModuleTestDir,
+ checksum='SHA256:b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7')
+
+ numOfFilesInDataDirTest = len([name for name in os.listdir(self.dataDir) if os.path.isfile(self.dataDir + '/' + name)])
+ self.assertEqual(numOfFilesInDataDirTest, self.expectedNumOfFilesInDataDir)
+ self.assertTrue(os.access(self.dataSegDir, os.F_OK))
+ numOfFilesInDataSegDirTest = len([name for name in os.listdir(self.dataSegDir) if os.path.isfile(self.dataSegDir + '/' + name)])
+ self.assertEqual(numOfFilesInDataSegDirTest, self.expectedNumOfFilesInDataSegDir)
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ logging.error('Test caused exception!\n' + str(e))
+
+ # ------------------------------------------------------------------------------
+ def TestSection_SetupScene(self):
+ self.paintEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Paint")
+ self.eraseEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Erase")
+ self.islandEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Islands")
+ self.thresholdEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Threshold")
+
+ self.segmentEditorNode = slicer.util.getNode("SegmentEditor")
+ self.assertIsNotNone(self.segmentEditorNode)
+
+ self.segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
+ self.assertIsNotNone(self.segmentationNode)
+ self.segmentEditorNode.SetAndObserveSegmentationNode(self.segmentationNode)
+
+ self.masterVolumeNode = slicer.util.loadVolume(self.dataDir + '/TinyPatient_CT.nrrd')
+ self.assertIsNotNone(self.masterVolumeNode)
+ self.segmentEditorNode.SetAndObserveMasterVolumeNode(self.masterVolumeNode)
+
+ self.segmentation = self.segmentationNode.GetSegmentation()
+ self.segmentation.SetMasterRepresentationName(self.binaryLabelmapReprName)
+ self.assertIsNotNone(self.segmentation)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_SharedLabelmapMultipleLayerEditing(self):
+ self.segmentation.RemoveAllSegments()
+ self.segmentation.AddEmptySegment("Segment_1")
+ self.segmentation.AddEmptySegment("Segment_2")
+
+ defaultModifierLabelmap = self.paintEffect.defaultModifierLabelmap()
+ self.ijkToRas = vtk.vtkMatrix4x4()
+ defaultModifierLabelmap.GetImageToWorldMatrix(self.ijkToRas)
+
+ mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ mergedLabelmap.SetExtent(0, 10, 0, 10, 0, 10)
+ mergedLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ mergedLabelmap.GetPointData().GetScalars().Fill(1)
+
+ oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
+
+ self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteAllSegments)
+ self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
+ self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
+ self.segmentEditorNode.SetSelectedSegmentID("Segment_2")
+ self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
+
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 1)
+
+ self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone)
+ self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
+ self.paintEffect.modifySelectedSegmentByLabelmap(mergedLabelmap, self.paintEffect.ModificationModeAdd)
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 2)
+
+ self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
+ logging.info('Multiple layer editing successful')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_IslandEffects(self):
+ islandSizes = [1, 26, 11, 6, 8, 6, 2]
+ islandSizes.sort(reverse=True)
+
+ minimumSize = 3
+ self.resetIslandSegments(islandSizes)
+ self.islandEffect.setParameter('MinimumSize', minimumSize)
+ self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND')
+ self.islandEffect.self().onApply()
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 1)
+
+ voxelCount = 0
+ for size in islandSizes:
+ if size < minimumSize:
+ continue
+ voxelCount = max(voxelCount, size)
+ self.checkSegmentVoxelCount(0, voxelCount)
+
+ minimumSize = 7
+ self.resetIslandSegments(islandSizes)
+ self.islandEffect.setParameter('MinimumSize', minimumSize)
+ self.islandEffect.setParameter('Operation', 'REMOVE_SMALL_ISLANDS')
+ self.islandEffect.self().onApply()
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 1)
+
+ voxelCount = 0
+ for size in islandSizes:
+ if size < minimumSize:
+ continue
+ voxelCount += size
+ self.checkSegmentVoxelCount(0, voxelCount)
+
+ self.resetIslandSegments(islandSizes)
+ minimumSize = 3
+ self.islandEffect.setParameter('MinimumSize', minimumSize)
+ self.islandEffect.setParameter('Operation', 'SPLIT_ISLANDS_TO_SEGMENTS')
+ self.islandEffect.self().onApply()
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 1)
+
+ for i in range(len(islandSizes)):
+ size = islandSizes[i]
+ if size < minimumSize:
+ continue
+ self.checkSegmentVoxelCount(i, size)
+
+ # ------------------------------------------------------------------------------
+ def resetIslandSegments(self, islandSizes):
+ self.segmentation.RemoveAllSegments()
+
+ totalSize = 0
+ voxelSizeSum = 0
+ for size in islandSizes:
+ totalSize += size + 1
+ voxelSizeSum += size
+
+ mergedLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ mergedLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ mergedLabelmapExtent = [0, totalSize - 1, 0, 0, 0, 0]
+ self.setupIslandLabelmap(mergedLabelmap, mergedLabelmapExtent, 0)
+
+ emptySegment = slicer.vtkSegment()
+ emptySegment.SetName("Segment_1")
+ emptySegment.AddRepresentation(self.binaryLabelmapReprName, mergedLabelmap)
+ self.segmentation.AddSegment(emptySegment)
+ self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
+
+ startExtent = 0
+ for size in islandSizes:
+ islandLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ islandLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ islandExtent = [startExtent, startExtent + size - 1, 0, 0, 0, 0]
+ self.setupIslandLabelmap(islandLabelmap, islandExtent)
+ self.paintEffect.modifySelectedSegmentByLabelmap(islandLabelmap, self.paintEffect.ModificationModeAdd)
+ startExtent += size + 1
+ self.checkSegmentVoxelCount(0, voxelSizeSum)
+
+ layerCount = self.segmentation.GetNumberOfLayers()
+ self.assertEqual(layerCount, 1)
+
+ # ------------------------------------------------------------------------------
+ def checkSegmentVoxelCount(self, segmentIndex, expectedVoxelCount):
+ segment = self.segmentation.GetNthSegment(segmentIndex)
+ self.assertIsNotNone(segment)
+
+ labelmap = slicer.vtkOrientedImageData()
+ labelmap.SetImageToWorldMatrix(self.ijkToRas)
+ segmentID = self.segmentation.GetNthSegmentID(segmentIndex)
+ self.segmentationNode.GetBinaryLabelmapRepresentation(segmentID, labelmap)
+
+ imageStat = vtk.vtkImageAccumulate()
+ imageStat.SetInputData(labelmap)
+ imageStat.SetComponentExtent(0, 4, 0, 0, 0, 0)
+ imageStat.SetComponentOrigin(0, 0, 0)
+ imageStat.SetComponentSpacing(1, 1, 1)
+ imageStat.IgnoreZeroOn()
+ imageStat.Update()
+
+ self.assertEqual(imageStat.GetVoxelCount(), expectedVoxelCount)
+
+ # ------------------------------------------------------------------------------
+ def setupIslandLabelmap(self, labelmap, extent, value=1):
+ labelmap.SetExtent(extent)
+ labelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ labelmap.GetPointData().GetScalars().Fill(value)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_MarginEffects(self):
+ logging.info("Running test on margin effect")
+
+ slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin")
+
+ self.segmentation.RemoveAllSegments()
+ segment1Id = self.segmentation.AddEmptySegment("Segment_1")
+ segment1 = self.segmentation.GetSegment(segment1Id)
+ segment1.SetLabelValue(1)
+ self.segmentEditorNode.SetSelectedSegmentID("Segment_1")
+
+ segment2Id = self.segmentation.AddEmptySegment("Segment_2")
+ segment2 = self.segmentation.GetSegment(segment2Id)
+ segment2.SetLabelValue(2)
+
+ binaryLabelmapRepresentationName = slicer.vtkSegmentationConverter.GetBinaryLabelmapRepresentationName()
+ dataTypes = [
+ vtk.VTK_CHAR,
+ vtk.VTK_SIGNED_CHAR,
+ vtk.VTK_UNSIGNED_CHAR,
+ vtk.VTK_SHORT,
+ vtk.VTK_UNSIGNED_SHORT,
+ vtk.VTK_INT,
+ vtk.VTK_UNSIGNED_INT,
+ # vtk.VTK_LONG, # On linux, VTK_LONG has the same size as VTK_LONG_LONG. This causes issues in vtkImageThreshold.
+ # vtk.VTK_UNSIGNED_LONG, See https://github.com/Slicer/Slicer/issues/5427
+ # vtk.VTK_FLOAT, # Since float can't represent all int, we jump straight to double.
+ vtk.VTK_DOUBLE,
+ # vtk.VTK_LONG_LONG, # These types are unsupported in ITK
+ # vtk.VTK_UNSIGNED_LONG_LONG,
+ ]
+ logging.info("Testing shared labelmaps")
+ for dataType in dataTypes:
+ initialLabelmap = slicer.vtkOrientedImageData()
+ initialLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ initialLabelmap.SetExtent(0, 10, 0, 10, 0, 10)
+ initialLabelmap.AllocateScalars(dataType, 1)
+ initialLabelmap.GetPointData().GetScalars().Fill(0)
+ segment1.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap)
+ segment2.AddRepresentation(binaryLabelmapRepresentationName, initialLabelmap)
+
+ self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments)
+ self.assertEqual(self.segmentation.GetNumberOfLayers(), 1)
+ self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone)
+ self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
+
+ logging.info("Testing separate labelmaps")
+ for dataType in dataTypes:
+ segment1Labelmap = slicer.vtkOrientedImageData()
+ segment1Labelmap.SetImageToWorldMatrix(self.ijkToRas)
+ segment1Labelmap.SetExtent(0, 10, 0, 10, 0, 10)
+ segment1Labelmap.AllocateScalars(dataType, 1)
+ segment1Labelmap.GetPointData().GetScalars().Fill(0)
+ segment1.AddRepresentation(binaryLabelmapRepresentationName, segment1Labelmap)
+
+ segment2Labelmap = slicer.vtkOrientedImageData()
+ segment2Labelmap.DeepCopy(segment1Labelmap)
+ segment2.AddRepresentation(binaryLabelmapRepresentationName, segment2Labelmap)
+
+ self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteAllSegments)
+ self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
+ self.runMarginEffect(segment1, segment2, dataType, self.segmentEditorNode.OverwriteNone)
+ self.assertEqual(self.segmentation.GetNumberOfLayers(), 2)
+
+ # ------------------------------------------------------------------------------
+ def runMarginEffect(self, segment1, segment2, dataType, overwriteMode):
+ logging.info(f"Running margin effect with data type: {dataType}, and overwriteMode {overwriteMode}")
+ marginEffect = slicer.modules.segmenteditor.widgetRepresentation().self().editor.effectByName("Margin")
+
+ marginEffect.setParameter("MarginSizeMm", 50.0)
+
+ oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
+ self.segmentEditorNode.SetOverwriteMode(overwriteMode)
+
+ segment1Labelmap = segment1.GetRepresentation(self.binaryLabelmapReprName)
+ segment1Labelmap.AllocateScalars(dataType, 1)
+ segment1Labelmap.GetPointData().GetScalars().Fill(0)
+
+ segment2Labelmap = segment2.GetRepresentation(self.binaryLabelmapReprName)
+ segment2Labelmap.AllocateScalars(dataType, 1)
+ segment2Labelmap.GetPointData().GetScalars().Fill(0)
+
+ segment1Position_IJK = [5, 5, 5]
+ segment1Labelmap.SetScalarComponentFromDouble(segment1Position_IJK[0], segment1Position_IJK[1], segment1Position_IJK[2], 0, segment1.GetLabelValue())
+ segment2Position_IJK = [6, 5, 6]
+ segment2Labelmap.SetScalarComponentFromDouble(segment2Position_IJK[0], segment2Position_IJK[1], segment2Position_IJK[2], 0, segment2.GetLabelValue())
+
+ self.checkSegmentVoxelCount(0, 1)
+ self.checkSegmentVoxelCount(1, 1)
+
+ marginEffect.self().onApply()
+ self.checkSegmentVoxelCount(0, 9) # Margin grow
+ self.checkSegmentVoxelCount(1, 1)
+
+ marginEffect.self().onApply()
+ self.checkSegmentVoxelCount(0, 37) # Margin grow
+ if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments:
+ self.checkSegmentVoxelCount(1, 0) # Overwritten
+ else:
+ self.checkSegmentVoxelCount(1, 1) # Not overwritten
+
+ marginEffect.setParameter("MarginSizeMm", -50.0)
+ marginEffect.self().onApply()
+
+ self.checkSegmentVoxelCount(0, 9) # Margin shrink
+ if overwriteMode == slicer.vtkMRMLSegmentEditorNode.OverwriteAllSegments:
+ self.checkSegmentVoxelCount(1, 0) # Overwritten
+ else:
+ self.checkSegmentVoxelCount(1, 1) # Not overwritten
+
+ self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_MaskingSettings(self):
+ self.segmentation.RemoveAllSegments()
+ segment1Id = self.segmentation.AddEmptySegment("Segment_1")
+ segment2Id = self.segmentation.AddEmptySegment("Segment_2")
+ segment3Id = self.segmentation.AddEmptySegment("Segment_3")
+ segment4Id = self.segmentation.AddEmptySegment("Segment_4")
+
+ oldOverwriteMode = self.segmentEditorNode.GetOverwriteMode()
+
+ # -------------------
+ # Test applying threshold with no masking
+ self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
+ self.thresholdEffect.setParameter("MinimumThreshold", "-17")
+ self.thresholdEffect.setParameter("MaximumThreshold", "848")
+ self.thresholdEffect.self().onApply()
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.checkSegmentVoxelCount(1, 0) # Segment_2
+
+ # -------------------
+ # Add paint to segment 2. No overwrite
+ paintModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ paintModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ paintModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
+ paintModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ paintModifierLabelmap.GetPointData().GetScalars().Fill(1)
+
+ self.segmentEditorNode.SetOverwriteMode(self.segmentEditorNode.OverwriteNone)
+ self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
+ self.paintEffect.modifySelectedSegmentByLabelmap(paintModifierLabelmap, self.paintEffect.ModificationModeAdd)
+
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.checkSegmentVoxelCount(1, 64) # Segment_2
+
+ # -------------------
+ # Test erasing with no masking
+ eraseModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ eraseModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ eraseModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
+ eraseModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ eraseModifierLabelmap.GetPointData().GetScalars().Fill(1)
+
+ self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
+ self.checkSegmentVoxelCount(0, 177) # Segment_1
+ self.checkSegmentVoxelCount(1, 64) # Segment_2
+
+ # -------------------
+ # Test erasing with masking on empty segment
+ self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
+ self.thresholdEffect.self().onApply() # Reset Segment_1
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment)
+ self.segmentEditorNode.SetMaskSegmentID(segment2Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
+ self.checkSegmentVoxelCount(0, 177) # We expect to be able to erase the current segment regardless of masking
+ self.checkSegmentVoxelCount(1, 64) # Segment_2
+
+ # -------------------
+ # Test erasing with masking on the same segment
+ self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
+ self.thresholdEffect.self().onApply() # Reset Segment_1
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.segmentEditorNode.SetMaskSegmentID(segment1Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
+ self.checkSegmentVoxelCount(0, 177) # Segment_1
+ self.checkSegmentVoxelCount(1, 64) # Segment_2
+
+ # -------------------
+ # Test erasing all segments
+ self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere)
+ self.thresholdEffect.self().onApply() # Reset Segment_1
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.segmentEditorNode.SetSelectedSegmentID(segment1Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemoveAll)
+ self.checkSegmentVoxelCount(0, 177) # Segment_1
+ self.checkSegmentVoxelCount(1, 0) # Segment_2
+
+ # -------------------
+ # Test adding back segments
+ self.thresholdEffect.self().onApply() # Reset Segment_1
+ self.checkSegmentVoxelCount(0, 204) # Segment_1
+ self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedInsideSingleSegment)
+ self.segmentEditorNode.SetMaskSegmentID(segment2Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
+ self.checkSegmentVoxelCount(0, 177) # Segment_1
+ self.checkSegmentVoxelCount(1, 27) # Segment_2
+
+ # -------------------
+ # Test threshold effect segment mask
+ self.segmentEditorNode.SetMaskSegmentID(segment2Id) # Erase Segment_2
+ self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
+ self.eraseEffect.modifySelectedSegmentByLabelmap(eraseModifierLabelmap, self.paintEffect.ModificationModeRemove)
+ self.segmentEditorNode.SetMaskSegmentID(segment1Id)
+ self.segmentEditorNode.SetSelectedSegmentID(segment2Id)
+ self.thresholdEffect.self().onApply() # Threshold Segment_2 within Segment_1
+ self.checkSegmentVoxelCount(0, 177) # Segment_1
+ self.checkSegmentVoxelCount(1, 177) # Segment_2
+
+ # -------------------
+ # Test intensity masking with segment mask
+ self.segmentEditorNode.MasterVolumeIntensityMaskOn()
+ self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848)
+ self.thresholdEffect.setParameter("MinimumThreshold", "-99999")
+ self.thresholdEffect.setParameter("MaximumThreshold", "99999")
+ self.segmentEditorNode.SetSelectedSegmentID(segment3Id)
+ self.thresholdEffect.self().onApply() # Threshold Segment_3
+ self.checkSegmentVoxelCount(2, 177) # Segment_3
+
+ # -------------------
+ # Test intensity masking with islands
+ self.segmentEditorNode.SetMaskMode(slicer.vtkMRMLSegmentationNode.EditAllowedEverywhere)
+ self.segmentEditorNode.MasterVolumeIntensityMaskOff()
+ self.segmentEditorNode.SetSelectedSegmentID(segment4Id)
+
+ island1ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ island1ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ island1ModifierLabelmap.SetExtent(2, 5, 2, 5, 2, 5)
+ island1ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ island1ModifierLabelmap.GetPointData().GetScalars().Fill(1)
+ self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd)
+
+ island2ModifierLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ island2ModifierLabelmap.SetImageToWorldMatrix(self.ijkToRas)
+ island2ModifierLabelmap.SetExtent(7, 9, 7, 9, 7, 9)
+ island2ModifierLabelmap.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ island2ModifierLabelmap.GetPointData().GetScalars().Fill(1)
+ self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd)
+ self.checkSegmentVoxelCount(3, 91) # Segment_4
+
+ # Test that no masking works as expected
+ minimumSize = 3
+ self.islandEffect.setParameter('MinimumSize', minimumSize)
+ self.islandEffect.setParameter('Operation', 'KEEP_LARGEST_ISLAND')
+ self.islandEffect.self().onApply()
+ self.checkSegmentVoxelCount(3, 64) # Segment_4
+
+ # Reset Segment_4 islands
+ self.paintEffect.modifySelectedSegmentByLabelmap(island1ModifierLabelmap, self.paintEffect.ModificationModeAdd)
+ self.paintEffect.modifySelectedSegmentByLabelmap(island2ModifierLabelmap, self.paintEffect.ModificationModeAdd)
+
+ # Test intensity masking
+ self.segmentEditorNode.MasterVolumeIntensityMaskOn()
+ self.segmentEditorNode.SetMasterVolumeIntensityMaskRange(-17, 848)
+ self.islandEffect.self().onApply()
+ self.checkSegmentVoxelCount(3, 87) # Segment_4
+
+ # Restore old overwrite setting
+ self.segmentEditorNode.SetOverwriteMode(oldOverwriteMode)
+ self.segmentEditorNode.MasterVolumeIntensityMaskOff()
diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py
index 947b0eaed6e..f190cf4d49e 100644
--- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py
+++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyCorePluginsSelfTest.py
@@ -9,32 +9,32 @@
#
class SubjectHierarchyCorePluginsSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "SubjectHierarchyCorePluginsSelfTest"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = ["SubjectHierarchy"]
- parent.contributors = ["Csaba Pinter (Queen's)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "SubjectHierarchyCorePluginsSelfTest"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = ["SubjectHierarchy"]
+ parent.contributors = ["Csaba Pinter (Queen's)"]
+ parent.helpText = """
This is a self test for the Subject hierarchy core plugins.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Csaba Pinter, PerkLab, Queen's University and was supported through the Applied Cancer
Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
- self.parent = parent
+ self.parent = parent
- # Add this test to the SelfTest module's list for discovery when the module
- # is created. Since this module may be discovered before SelfTests itself,
- # create the list if it doesn't already exist.
- try:
- slicer.selfTests
- except AttributeError:
- slicer.selfTests = {}
- slicer.selfTests['SubjectHierarchyCorePluginsSelfTest'] = self.runTest
+ # Add this test to the SelfTest module's list for discovery when the module
+ # is created. Since this module may be discovered before SelfTests itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.selfTests
+ except AttributeError:
+ slicer.selfTests = {}
+ slicer.selfTests['SubjectHierarchyCorePluginsSelfTest'] = self.runTest
- def runTest(self, msec=100, **kwargs):
- tester = SubjectHierarchyCorePluginsSelfTestTest()
- tester.runTest()
+ def runTest(self, msec=100, **kwargs):
+ tester = SubjectHierarchyCorePluginsSelfTestTest()
+ tester.runTest()
#
@@ -42,8 +42,8 @@ def runTest(self, msec=100, **kwargs):
#
class SubjectHierarchyCorePluginsSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
#
@@ -51,180 +51,180 @@ def setup(self):
#
class SubjectHierarchyCorePluginsSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
+ """
- def __init__(self):
- pass
+ def __init__(self):
+ pass
class SubjectHierarchyCorePluginsSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- self.delayMs = 700
-
- # TODO: Comment out (sample code for debugging)
- # logFile = open('d:/pyTestLog.txt', 'w')
- # logFile.write(repr(slicer.modules.SubjectHierarchyCorePluginsSelfTest) + '\n')
- # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n')
- # logFile.close()
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_SubjectHierarchyCorePluginsSelfTest_FullTest1()
-
- # ------------------------------------------------------------------------------
- def test_SubjectHierarchyCorePluginsSelfTest_FullTest1(self):
- # Check for SubjectHierarchy module
- self.assertTrue(slicer.modules.subjecthierarchy)
-
- # Switch to subject hierarchy module so that the changes can be seen as the test goes
- slicer.util.selectModule('SubjectHierarchy')
-
- self.section_SetupPathsAndNames()
- self.section_MarkupRole()
- self.section_CloneNode()
- self.section_SegmentEditor()
-
- # ------------------------------------------------------------------------------
- def section_SetupPathsAndNames(self):
- # Set constants
- self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID()
- self.sampleMarkupName = 'SampleMarkup'
- self.studyItemID = self.invalidItemID
- self.cloneNodeNamePostfix = slicer.qSlicerSubjectHierarchyCloneNodePlugin().getCloneNodeNamePostfix()
-
- # Test printing of all context menu actions and their section numbers
- pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance();
- print(pluginHandler.dumpContextMenuActions())
-
- # ------------------------------------------------------------------------------
- def section_MarkupRole(self):
- self.delayDisplay("Markup role", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Create sample markups node
- markupsNode = slicer.vtkMRMLMarkupsFiducialNode()
- slicer.mrmlScene.AddNode(markupsNode)
- markupsNode.SetName(self.sampleMarkupName)
- fiducialPosition = [100.0, 0.0, 0.0]
- markupsNode.AddControlPoint(fiducialPosition)
- markupsShItemID = shNode.GetItemByDataNode(markupsNode)
- self.assertIsNotNone(markupsShItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups')
-
- # Create patient and study
- patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), 'Patient')
- self.studyItemID = shNode.CreateStudyItem(patientItemID, 'Study')
-
- # Add markups under study
- markupsShItemID2 = shNode.CreateItem(self.studyItemID, markupsNode)
- self.assertEqual(markupsShItemID, markupsShItemID2)
- self.assertEqual(shNode.GetItemParent(markupsShItemID), self.studyItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups')
-
- # ------------------------------------------------------------------------------
- def section_CloneNode(self):
- self.delayDisplay("Clone node", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- markupsNode = slicer.util.getNode(self.sampleMarkupName)
- markupsShItemID = shNode.GetItemByDataNode(markupsNode)
-
- self.assertIsNotNone(markupsShItemID)
- self.assertIsNotNone(shNode.GetItemDataNode(markupsShItemID))
-
- # Add storage node for markups node to test cloning those
- markupsStorageNode = slicer.vtkMRMLMarkupsFiducialStorageNode()
- slicer.mrmlScene.AddNode(markupsStorageNode)
- markupsNode.SetAndObserveStorageNodeID(markupsStorageNode.GetID())
-
- # Get clone node plugin
- pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
- self.assertIsNotNone(pluginHandler)
-
- cloneNodePlugin = pluginHandler.pluginByName('CloneNode')
- self.assertIsNotNone(cloneNodePlugin)
-
- # Set markup node as current (i.e. selected in the tree) for clone
- pluginHandler.setCurrentItem(markupsShItemID)
-
- # Get clone node context menu action and trigger
- cloneNodePlugin.itemContextMenuActions()[0].activate(qt.QAction.Trigger)
-
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode'), 2)
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsDisplayNode'), 2)
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialStorageNode'), 2)
-
- clonedMarkupsName = self.sampleMarkupName + self.cloneNodeNamePostfix
- clonedMarkupsNode = slicer.util.getNode(clonedMarkupsName)
- self.assertIsNotNone(clonedMarkupsNode)
- clonedMarkupsShItemID = shNode.GetItemChildWithName(self.studyItemID, clonedMarkupsName)
- self.assertIsNotNone(clonedMarkupsShItemID)
- self.assertIsNotNone(clonedMarkupsNode.GetDisplayNode())
- self.assertIsNotNone(clonedMarkupsNode.GetStorageNode())
-
- inSameStudy = slicer.vtkSlicerSubjectHierarchyModuleLogic.AreItemsInSameBranch(
- shNode, markupsShItemID, clonedMarkupsShItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy())
- self.assertTrue(inSameStudy)
-
- # ------------------------------------------------------------------------------
- def section_SegmentEditor(self):
- self.delayDisplay("Segment Editor", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- import SampleData
- mrHeadNode = SampleData.SampleDataLogic().downloadMRHead()
-
- # Make sure Data module is initialized because the use case tested below
- # (https://github.com/Slicer/Slicer/issues/4877) needs an initialized SH
- # tree view so that applyReferenceHighlightForItems is run
- slicer.util.selectModule('Data')
-
- folderItem = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'TestFolder')
-
- mrHeadItem = shNode.GetItemByDataNode(mrHeadNode)
- shNode.SetItemParent(mrHeadItem, folderItem)
-
- dataModuleWidget = slicer.modules.data.widgetRepresentation()
- treeView = slicer.util.findChildren(dataModuleWidget, className='qMRMLSubjectHierarchyTreeView')[0]
- treeView.setCurrentItem(mrHeadItem)
-
- pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- segmentEditorPlugin = pluginHandler.pluginByName('SegmentEditor').self()
- segmentEditorPlugin.segmentEditorAction.trigger()
-
- # Get segmentation node automatically created by "Segment this..." action
- segmentationNode = None
- segmentationNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSegmentationNode')
- segmentationNodes.UnRegister(None)
- for i in range(segmentationNodes.GetNumberOfItems()):
- currentSegNode = segmentationNodes.GetItemAsObject(i)
- if currentSegNode.GetNodeReferenceID(currentSegNode.GetReferenceImageGeometryReferenceRole()) == mrHeadNode.GetID():
- segmentationNode = currentSegNode
- break
-
- self.assertIsNotNone(segmentationNode)
-
- segmentationItem = shNode.GetItemByDataNode(segmentationNode)
- self.assertEqual(shNode.GetItemParent(segmentationItem), shNode.GetItemParent(mrHeadItem))
- self.assertEqual(segmentationNode.GetName()[:len(mrHeadNode.GetName())], mrHeadNode.GetName())
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ self.delayMs = 700
+
+ # TODO: Comment out (sample code for debugging)
+ # logFile = open('d:/pyTestLog.txt', 'w')
+ # logFile.write(repr(slicer.modules.SubjectHierarchyCorePluginsSelfTest) + '\n')
+ # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n')
+ # logFile.close()
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SubjectHierarchyCorePluginsSelfTest_FullTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_SubjectHierarchyCorePluginsSelfTest_FullTest1(self):
+ # Check for SubjectHierarchy module
+ self.assertTrue(slicer.modules.subjecthierarchy)
+
+ # Switch to subject hierarchy module so that the changes can be seen as the test goes
+ slicer.util.selectModule('SubjectHierarchy')
+
+ self.section_SetupPathsAndNames()
+ self.section_MarkupRole()
+ self.section_CloneNode()
+ self.section_SegmentEditor()
+
+ # ------------------------------------------------------------------------------
+ def section_SetupPathsAndNames(self):
+ # Set constants
+ self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID()
+ self.sampleMarkupName = 'SampleMarkup'
+ self.studyItemID = self.invalidItemID
+ self.cloneNodeNamePostfix = slicer.qSlicerSubjectHierarchyCloneNodePlugin().getCloneNodeNamePostfix()
+
+ # Test printing of all context menu actions and their section numbers
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance();
+ print(pluginHandler.dumpContextMenuActions())
+
+ # ------------------------------------------------------------------------------
+ def section_MarkupRole(self):
+ self.delayDisplay("Markup role", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Create sample markups node
+ markupsNode = slicer.vtkMRMLMarkupsFiducialNode()
+ slicer.mrmlScene.AddNode(markupsNode)
+ markupsNode.SetName(self.sampleMarkupName)
+ fiducialPosition = [100.0, 0.0, 0.0]
+ markupsNode.AddControlPoint(fiducialPosition)
+ markupsShItemID = shNode.GetItemByDataNode(markupsNode)
+ self.assertIsNotNone(markupsShItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups')
+
+ # Create patient and study
+ patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), 'Patient')
+ self.studyItemID = shNode.CreateStudyItem(patientItemID, 'Study')
+
+ # Add markups under study
+ markupsShItemID2 = shNode.CreateItem(self.studyItemID, markupsNode)
+ self.assertEqual(markupsShItemID, markupsShItemID2)
+ self.assertEqual(shNode.GetItemParent(markupsShItemID), self.studyItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(markupsShItemID), 'Markups')
+
+ # ------------------------------------------------------------------------------
+ def section_CloneNode(self):
+ self.delayDisplay("Clone node", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ markupsNode = slicer.util.getNode(self.sampleMarkupName)
+ markupsShItemID = shNode.GetItemByDataNode(markupsNode)
+
+ self.assertIsNotNone(markupsShItemID)
+ self.assertIsNotNone(shNode.GetItemDataNode(markupsShItemID))
+
+ # Add storage node for markups node to test cloning those
+ markupsStorageNode = slicer.vtkMRMLMarkupsFiducialStorageNode()
+ slicer.mrmlScene.AddNode(markupsStorageNode)
+ markupsNode.SetAndObserveStorageNodeID(markupsStorageNode.GetID())
+
+ # Get clone node plugin
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
+ self.assertIsNotNone(pluginHandler)
+
+ cloneNodePlugin = pluginHandler.pluginByName('CloneNode')
+ self.assertIsNotNone(cloneNodePlugin)
+
+ # Set markup node as current (i.e. selected in the tree) for clone
+ pluginHandler.setCurrentItem(markupsShItemID)
+
+ # Get clone node context menu action and trigger
+ cloneNodePlugin.itemContextMenuActions()[0].activate(qt.QAction.Trigger)
+
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialNode'), 2)
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsDisplayNode'), 2)
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsFiducialStorageNode'), 2)
+
+ clonedMarkupsName = self.sampleMarkupName + self.cloneNodeNamePostfix
+ clonedMarkupsNode = slicer.util.getNode(clonedMarkupsName)
+ self.assertIsNotNone(clonedMarkupsNode)
+ clonedMarkupsShItemID = shNode.GetItemChildWithName(self.studyItemID, clonedMarkupsName)
+ self.assertIsNotNone(clonedMarkupsShItemID)
+ self.assertIsNotNone(clonedMarkupsNode.GetDisplayNode())
+ self.assertIsNotNone(clonedMarkupsNode.GetStorageNode())
+
+ inSameStudy = slicer.vtkSlicerSubjectHierarchyModuleLogic.AreItemsInSameBranch(
+ shNode, markupsShItemID, clonedMarkupsShItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy())
+ self.assertTrue(inSameStudy)
+
+ # ------------------------------------------------------------------------------
+ def section_SegmentEditor(self):
+ self.delayDisplay("Segment Editor", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ import SampleData
+ mrHeadNode = SampleData.SampleDataLogic().downloadMRHead()
+
+ # Make sure Data module is initialized because the use case tested below
+ # (https://github.com/Slicer/Slicer/issues/4877) needs an initialized SH
+ # tree view so that applyReferenceHighlightForItems is run
+ slicer.util.selectModule('Data')
+
+ folderItem = shNode.CreateFolderItem(shNode.GetSceneItemID(), 'TestFolder')
+
+ mrHeadItem = shNode.GetItemByDataNode(mrHeadNode)
+ shNode.SetItemParent(mrHeadItem, folderItem)
+
+ dataModuleWidget = slicer.modules.data.widgetRepresentation()
+ treeView = slicer.util.findChildren(dataModuleWidget, className='qMRMLSubjectHierarchyTreeView')[0]
+ treeView.setCurrentItem(mrHeadItem)
+
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ segmentEditorPlugin = pluginHandler.pluginByName('SegmentEditor').self()
+ segmentEditorPlugin.segmentEditorAction.trigger()
+
+ # Get segmentation node automatically created by "Segment this..." action
+ segmentationNode = None
+ segmentationNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSegmentationNode')
+ segmentationNodes.UnRegister(None)
+ for i in range(segmentationNodes.GetNumberOfItems()):
+ currentSegNode = segmentationNodes.GetItemAsObject(i)
+ if currentSegNode.GetNodeReferenceID(currentSegNode.GetReferenceImageGeometryReferenceRole()) == mrHeadNode.GetID():
+ segmentationNode = currentSegNode
+ break
+
+ self.assertIsNotNone(segmentationNode)
+
+ segmentationItem = shNode.GetItemByDataNode(segmentationNode)
+ self.assertEqual(shNode.GetItemParent(segmentationItem), shNode.GetItemParent(mrHeadItem))
+ self.assertEqual(segmentationNode.GetName()[:len(mrHeadNode.GetName())], mrHeadNode.GetName())
diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py
index 38946356955..eb3661971fe 100644
--- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py
+++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyFoldersTest1.py
@@ -10,274 +10,274 @@
class SubjectHierarchyFoldersTest1(unittest.TestCase):
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_SubjectHierarchyFoldersTest1()
-
- # ------------------------------------------------------------------------------
- def test_SubjectHierarchyFoldersTest1(self):
- # Check for modules
- self.assertIsNotNone(slicer.modules.subjecthierarchy)
-
- self.TestSection_InitializeTest()
- self.TestSection_LoadTestData()
- self.TestSection_FolderVisibility()
- self.TestSection_ApplyDisplayPropertiesOnBranch()
- self.TestSection_FolderDisplayOverrideAllowed()
-
- logging.info('Test finished')
-
- # ------------------------------------------------------------------------------
- def TestSection_InitializeTest(self):
- #
- # Define variables
- #
-
- # A certain model to test that is both in the brain and the midbrain folders
- self.testModelNodeName = 'Model_79_left_red_nucleus'
- self.testModelNode = None
- self.overrideColor = [255, 255, 0]
-
- # Get subject hierarchy node
- self.shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- self.assertIsNotNone(self.shNode)
- # Get folder plugin
- pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
- self.assertIsNotNone(pluginHandler)
- self.folderPlugin = pluginHandler.pluginByName('Folder')
- self.assertIsNotNone(self.folderPlugin)
-
- #
- # Initialize test
- #
-
- # Create 3D view
- self.layoutName = "Test3DView"
- # ownerNode manages this view instead of the layout manager (it can be any node in the scene)
- self.viewOwnerNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScriptedModuleNode")
- self.viewNode = slicer.vtkMRMLViewNode()
- self.viewNode.SetName(self.layoutName)
- self.viewNode.SetLayoutName(self.layoutName)
- self.viewNode.SetLayoutColor(1, 1, 0)
- self.viewNode.SetAndObserveParentLayoutNodeID(self.viewOwnerNode.GetID())
- self.viewNode = slicer.mrmlScene.AddNode(self.viewNode)
- self.viewWidget = slicer.qMRMLThreeDWidget()
- self.viewWidget.setMRMLScene(slicer.mrmlScene)
- self.viewWidget.setMRMLViewNode(self.viewNode)
- self.viewWidget.show()
-
- # Get model displayable manager for the 3D view
- self.modelDisplayableManager = self.getModelDisplayableManager()
- self.assertIsNotNone(self.modelDisplayableManager)
-
- # ------------------------------------------------------------------------------
- def TestSection_LoadTestData(self):
- # Load NAC Brain Atlas 2015 (https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f)
- logging.info('Test section: Load NAC Brain Atlas 2015')
-
- import SampleData
- sceneFile = SampleData.downloadFromURL(
- fileNames='NACBrainAtlas2015.mrb',
- # Note: this data set is from SlicerDataStore (not from SlicerTestingData) repository
- uris=DATA_STORE_URL + 'SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f',
- checksums='SHA256:d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f')[0]
-
- ioManager = slicer.app.ioManager()
- ioManager.loadFile(sceneFile)
-
- # Check number of models to see if atlas was fully loaded
- self.assertEqual(298, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode')) # 301 with main window due to the slice views
-
- # Check number of model hierarchy nodes to make sure all of them were converted
- self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelHierarchyNode'))
- # Check number of folder display nodes, which is zero until branch display related functions are used
- self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode'))
-
- # Check number of folder items
- numberOfFolderItems = 0
- allItems = vtk.vtkIdList()
- self.shNode.GetItemChildren(self.shNode.GetSceneItemID(), allItems, True)
- for index in range(allItems.GetNumberOfIds()):
- currentItem = allItems.GetId(index)
- if self.shNode.IsItemLevel(currentItem, slicer.vtkMRMLSubjectHierarchyConstants.GetSubjectHierarchyLevelFolder()):
- numberOfFolderItems += 1
- self.assertEqual(80, numberOfFolderItems)
-
- # ------------------------------------------------------------------------------
- def TestSection_FolderVisibility(self):
- # Test apply display properties on branch feature
- logging.info('Test section: Folder visibility')
-
- # Get folder that contains the whole brain
- brainFolderItem = self.shNode.GetItemByName('Brain')
- self.assertNotEqual(brainFolderItem, 0)
-
- # Check number of visible models
- modelNodes = slicer.util.getNodes('vtkMRMLModelNode*')
- modelNodesList = list(modelNodes.values())
- numberOfVisibleModels = 0
- for modelNode in modelNodesList:
- displayNode = modelNode.GetDisplayNode()
- actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
- if actor.GetVisibility() > 0:
- numberOfVisibleModels += 1
- self.assertEqual(225, numberOfVisibleModels)
-
- # Check model node hierarchy visibility
- self.testModelNode = slicer.util.getNode(self.testModelNodeName)
- self.assertIsNotNone(self.testModelNode)
-
- testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
- self.assertTrue(testModelHierarchyVisibility)
-
- # Hide branch using the folder plugin
- self.startTiming()
- self.folderPlugin.setDisplayVisibility(brainFolderItem, 0)
- logging.info('Time of hiding whole brain: ' + str(self.stopTiming() / 1000) + ' s')
-
- # Check if a folder display node was indeed created when changing display property on the folder
- self.assertEqual(1, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode'))
- brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem)
- self.assertIsNotNone(brainFolderDisplayNode)
-
- # Check model node hierarchy visibility
- testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
- self.assertFalse(testModelHierarchyVisibility)
-
- # Check if brain models were indeed hidden
- numberOfVisibleModels = 0
- for modelNode in modelNodesList:
- displayNode = modelNode.GetDisplayNode()
- actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
- if actor.GetVisibility() > 0:
- numberOfVisibleModels += 1
- self.assertEqual(3, numberOfVisibleModels)
-
- # Show folder again
- self.startTiming()
- self.folderPlugin.setDisplayVisibility(brainFolderItem, 1)
- logging.info('Time of showing whole brain: ' + str(self.stopTiming() / 1000) + ' s')
-
- # Check number of visible models
- numberOfVisibleModels = 0
- for modelNode in modelNodesList:
- displayNode = modelNode.GetDisplayNode()
- actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
- if actor.GetVisibility() > 0:
- numberOfVisibleModels += 1
- self.assertEqual(225, numberOfVisibleModels)
-
- # Check model node hierarchy visibility
- testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
- self.assertTrue(testModelHierarchyVisibility)
-
- # ------------------------------------------------------------------------------
- def TestSection_ApplyDisplayPropertiesOnBranch(self):
- # Test apply display properties on branch feature
- logging.info('Test section: Apply display properties on branch')
-
- # Get folder that contains the midbrain
- midbrainFolderItem = self.shNode.GetItemByName('midbrain')
- self.assertNotEqual(midbrainFolderItem, 0)
-
- # Test simple color override, check actor color
- overrideColorQt = qt.QColor(self.overrideColor[0], self.overrideColor[1], self.overrideColor[2])
- self.startTiming()
- self.folderPlugin.setDisplayColor(midbrainFolderItem, overrideColorQt, {})
- logging.info('Time of setting override color on midbrain branch: ' + str(self.stopTiming() / 1000) + ' s')
-
- # Check number of models with overridden color
- numberOfOverriddenMidbrainModels = 0
- testModelNodeOverridden = False
- midbrainModelItems = vtk.vtkIdList()
- self.shNode.GetItemChildren(midbrainFolderItem, midbrainModelItems, True)
- for index in range(midbrainModelItems.GetNumberOfIds()):
- currentMidbrainModelItem = midbrainModelItems.GetId(index)
- currentMidbrainModelNode = self.shNode.GetItemDataNode(currentMidbrainModelItem)
- if currentMidbrainModelNode: # The child item can be a folder as well, in which case there is no model node
- displayNode = currentMidbrainModelNode.GetDisplayNode()
- actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
- currentColor = actor.GetProperty().GetColor()
- if (currentColor[0] == self.overrideColor[0] / 255 and
- currentColor[1] == self.overrideColor[1] / 255 and
- currentColor[2] == self.overrideColor[2] / 255):
- if currentMidbrainModelNode is self.testModelNode:
- testModelNodeOverridden = True
- numberOfOverriddenMidbrainModels += 1
- self.assertEqual(6, numberOfOverriddenMidbrainModels)
- self.assertTrue(testModelNodeOverridden)
-
- # Test hierarchy opacity
- testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
- self.assertEqual(testModelHierarchyOpacity, 1.0)
-
- midbrainFolderDisplayNode = self.shNode.GetItemDataNode(midbrainFolderItem)
- self.assertIsNotNone(midbrainFolderDisplayNode)
- midbrainFolderDisplayNode.SetOpacity(0.5)
-
- testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
- self.assertEqual(testModelHierarchyOpacity, 0.5)
-
- brainFolderItem = self.shNode.GetItemByName('Brain')
- self.assertNotEqual(brainFolderItem, 0)
- brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem)
- self.assertIsNotNone(brainFolderDisplayNode)
- brainFolderDisplayNode.SetOpacity(0.5)
-
- testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
- self.assertEqual(testModelHierarchyOpacity, 0.25)
-
- # ------------------------------------------------------------------------------
- def TestSection_FolderDisplayOverrideAllowed(self):
- # Test exclusion of a node from the apply display properties feature
- logging.info('Test section: Disable apply display properties using FolderDisplayOverrideAllowed')
-
- testModelDisplayNode = self.testModelNode.GetDisplayNode()
- self.assertTrue(testModelDisplayNode.GetFolderDisplayOverrideAllowed())
-
- # Turn of override allowed
- testModelDisplayNode.SetFolderDisplayOverrideAllowed(False)
-
- # Check that color and opacity are not overridden
- testModelActor = self.modelDisplayableManager.GetActorByID(testModelDisplayNode.GetID())
-
- testModelCurrentColor = testModelActor.GetProperty().GetColor()
- colorOverridden = False
- if (testModelCurrentColor[0] == self.overrideColor[0] / 255 and
- testModelCurrentColor[1] == self.overrideColor[1] / 255 and
- testModelCurrentColor[2] == self.overrideColor[2] / 255):
- colorOverridden = True
- self.assertFalse(colorOverridden)
-
- testModelCurrentOpacity = testModelActor.GetProperty().GetOpacity()
- self.assertEqual(testModelCurrentOpacity, 1.0)
-
- # ------------------------------------------------------------------------------
- def startTiming(self):
- self.timer = qt.QTime()
- self.timer.start()
-
- # ------------------------------------------------------------------------------
- def stopTiming(self):
- return self.timer.elapsed()
-
- # ------------------------------------------------------------------------------
- def getModelDisplayableManager(self):
- if self.viewWidget is None:
- logging.error('View widget is not created')
- return None
- managers = vtk.vtkCollection()
- self.viewWidget.getDisplayableManagers(managers)
- for i in range(managers.GetNumberOfItems()):
- obj = managers.GetItemAsObject(i)
- if obj.IsA('vtkMRMLModelDisplayableManager'):
- return obj
- logging.error('Failed to find the model displayable manager')
- return None
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SubjectHierarchyFoldersTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_SubjectHierarchyFoldersTest1(self):
+ # Check for modules
+ self.assertIsNotNone(slicer.modules.subjecthierarchy)
+
+ self.TestSection_InitializeTest()
+ self.TestSection_LoadTestData()
+ self.TestSection_FolderVisibility()
+ self.TestSection_ApplyDisplayPropertiesOnBranch()
+ self.TestSection_FolderDisplayOverrideAllowed()
+
+ logging.info('Test finished')
+
+ # ------------------------------------------------------------------------------
+ def TestSection_InitializeTest(self):
+ #
+ # Define variables
+ #
+
+ # A certain model to test that is both in the brain and the midbrain folders
+ self.testModelNodeName = 'Model_79_left_red_nucleus'
+ self.testModelNode = None
+ self.overrideColor = [255, 255, 0]
+
+ # Get subject hierarchy node
+ self.shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ self.assertIsNotNone(self.shNode)
+ # Get folder plugin
+ pluginHandler = slicer.qSlicerSubjectHierarchyPluginHandler().instance()
+ self.assertIsNotNone(pluginHandler)
+ self.folderPlugin = pluginHandler.pluginByName('Folder')
+ self.assertIsNotNone(self.folderPlugin)
+
+ #
+ # Initialize test
+ #
+
+ # Create 3D view
+ self.layoutName = "Test3DView"
+ # ownerNode manages this view instead of the layout manager (it can be any node in the scene)
+ self.viewOwnerNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScriptedModuleNode")
+ self.viewNode = slicer.vtkMRMLViewNode()
+ self.viewNode.SetName(self.layoutName)
+ self.viewNode.SetLayoutName(self.layoutName)
+ self.viewNode.SetLayoutColor(1, 1, 0)
+ self.viewNode.SetAndObserveParentLayoutNodeID(self.viewOwnerNode.GetID())
+ self.viewNode = slicer.mrmlScene.AddNode(self.viewNode)
+ self.viewWidget = slicer.qMRMLThreeDWidget()
+ self.viewWidget.setMRMLScene(slicer.mrmlScene)
+ self.viewWidget.setMRMLViewNode(self.viewNode)
+ self.viewWidget.show()
+
+ # Get model displayable manager for the 3D view
+ self.modelDisplayableManager = self.getModelDisplayableManager()
+ self.assertIsNotNone(self.modelDisplayableManager)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_LoadTestData(self):
+ # Load NAC Brain Atlas 2015 (https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f)
+ logging.info('Test section: Load NAC Brain Atlas 2015')
+
+ import SampleData
+ sceneFile = SampleData.downloadFromURL(
+ fileNames='NACBrainAtlas2015.mrb',
+ # Note: this data set is from SlicerDataStore (not from SlicerTestingData) repository
+ uris=DATA_STORE_URL + 'SHA256/d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f',
+ checksums='SHA256:d69d0331d4fd2574be1459b7734921f64f5872d3cb9589ec01b2f53dadc7112f')[0]
+
+ ioManager = slicer.app.ioManager()
+ ioManager.loadFile(sceneFile)
+
+ # Check number of models to see if atlas was fully loaded
+ self.assertEqual(298, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode')) # 301 with main window due to the slice views
+
+ # Check number of model hierarchy nodes to make sure all of them were converted
+ self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelHierarchyNode'))
+ # Check number of folder display nodes, which is zero until branch display related functions are used
+ self.assertEqual(0, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode'))
+
+ # Check number of folder items
+ numberOfFolderItems = 0
+ allItems = vtk.vtkIdList()
+ self.shNode.GetItemChildren(self.shNode.GetSceneItemID(), allItems, True)
+ for index in range(allItems.GetNumberOfIds()):
+ currentItem = allItems.GetId(index)
+ if self.shNode.IsItemLevel(currentItem, slicer.vtkMRMLSubjectHierarchyConstants.GetSubjectHierarchyLevelFolder()):
+ numberOfFolderItems += 1
+ self.assertEqual(80, numberOfFolderItems)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_FolderVisibility(self):
+ # Test apply display properties on branch feature
+ logging.info('Test section: Folder visibility')
+
+ # Get folder that contains the whole brain
+ brainFolderItem = self.shNode.GetItemByName('Brain')
+ self.assertNotEqual(brainFolderItem, 0)
+
+ # Check number of visible models
+ modelNodes = slicer.util.getNodes('vtkMRMLModelNode*')
+ modelNodesList = list(modelNodes.values())
+ numberOfVisibleModels = 0
+ for modelNode in modelNodesList:
+ displayNode = modelNode.GetDisplayNode()
+ actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
+ if actor.GetVisibility() > 0:
+ numberOfVisibleModels += 1
+ self.assertEqual(225, numberOfVisibleModels)
+
+ # Check model node hierarchy visibility
+ self.testModelNode = slicer.util.getNode(self.testModelNodeName)
+ self.assertIsNotNone(self.testModelNode)
+
+ testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
+ self.assertTrue(testModelHierarchyVisibility)
+
+ # Hide branch using the folder plugin
+ self.startTiming()
+ self.folderPlugin.setDisplayVisibility(brainFolderItem, 0)
+ logging.info('Time of hiding whole brain: ' + str(self.stopTiming() / 1000) + ' s')
+
+ # Check if a folder display node was indeed created when changing display property on the folder
+ self.assertEqual(1, slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLFolderDisplayNode'))
+ brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem)
+ self.assertIsNotNone(brainFolderDisplayNode)
+
+ # Check model node hierarchy visibility
+ testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
+ self.assertFalse(testModelHierarchyVisibility)
+
+ # Check if brain models were indeed hidden
+ numberOfVisibleModels = 0
+ for modelNode in modelNodesList:
+ displayNode = modelNode.GetDisplayNode()
+ actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
+ if actor.GetVisibility() > 0:
+ numberOfVisibleModels += 1
+ self.assertEqual(3, numberOfVisibleModels)
+
+ # Show folder again
+ self.startTiming()
+ self.folderPlugin.setDisplayVisibility(brainFolderItem, 1)
+ logging.info('Time of showing whole brain: ' + str(self.stopTiming() / 1000) + ' s')
+
+ # Check number of visible models
+ numberOfVisibleModels = 0
+ for modelNode in modelNodesList:
+ displayNode = modelNode.GetDisplayNode()
+ actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
+ if actor.GetVisibility() > 0:
+ numberOfVisibleModels += 1
+ self.assertEqual(225, numberOfVisibleModels)
+
+ # Check model node hierarchy visibility
+ testModelHierarchyVisibility = slicer.vtkMRMLFolderDisplayNode.GetHierarchyVisibility(self.testModelNode)
+ self.assertTrue(testModelHierarchyVisibility)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_ApplyDisplayPropertiesOnBranch(self):
+ # Test apply display properties on branch feature
+ logging.info('Test section: Apply display properties on branch')
+
+ # Get folder that contains the midbrain
+ midbrainFolderItem = self.shNode.GetItemByName('midbrain')
+ self.assertNotEqual(midbrainFolderItem, 0)
+
+ # Test simple color override, check actor color
+ overrideColorQt = qt.QColor(self.overrideColor[0], self.overrideColor[1], self.overrideColor[2])
+ self.startTiming()
+ self.folderPlugin.setDisplayColor(midbrainFolderItem, overrideColorQt, {})
+ logging.info('Time of setting override color on midbrain branch: ' + str(self.stopTiming() / 1000) + ' s')
+
+ # Check number of models with overridden color
+ numberOfOverriddenMidbrainModels = 0
+ testModelNodeOverridden = False
+ midbrainModelItems = vtk.vtkIdList()
+ self.shNode.GetItemChildren(midbrainFolderItem, midbrainModelItems, True)
+ for index in range(midbrainModelItems.GetNumberOfIds()):
+ currentMidbrainModelItem = midbrainModelItems.GetId(index)
+ currentMidbrainModelNode = self.shNode.GetItemDataNode(currentMidbrainModelItem)
+ if currentMidbrainModelNode: # The child item can be a folder as well, in which case there is no model node
+ displayNode = currentMidbrainModelNode.GetDisplayNode()
+ actor = self.modelDisplayableManager.GetActorByID(displayNode.GetID())
+ currentColor = actor.GetProperty().GetColor()
+ if (currentColor[0] == self.overrideColor[0] / 255 and
+ currentColor[1] == self.overrideColor[1] / 255 and
+ currentColor[2] == self.overrideColor[2] / 255):
+ if currentMidbrainModelNode is self.testModelNode:
+ testModelNodeOverridden = True
+ numberOfOverriddenMidbrainModels += 1
+ self.assertEqual(6, numberOfOverriddenMidbrainModels)
+ self.assertTrue(testModelNodeOverridden)
+
+ # Test hierarchy opacity
+ testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
+ self.assertEqual(testModelHierarchyOpacity, 1.0)
+
+ midbrainFolderDisplayNode = self.shNode.GetItemDataNode(midbrainFolderItem)
+ self.assertIsNotNone(midbrainFolderDisplayNode)
+ midbrainFolderDisplayNode.SetOpacity(0.5)
+
+ testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
+ self.assertEqual(testModelHierarchyOpacity, 0.5)
+
+ brainFolderItem = self.shNode.GetItemByName('Brain')
+ self.assertNotEqual(brainFolderItem, 0)
+ brainFolderDisplayNode = self.shNode.GetItemDataNode(brainFolderItem)
+ self.assertIsNotNone(brainFolderDisplayNode)
+ brainFolderDisplayNode.SetOpacity(0.5)
+
+ testModelHierarchyOpacity = slicer.vtkMRMLFolderDisplayNode.GetHierarchyOpacity(self.testModelNode)
+ self.assertEqual(testModelHierarchyOpacity, 0.25)
+
+ # ------------------------------------------------------------------------------
+ def TestSection_FolderDisplayOverrideAllowed(self):
+ # Test exclusion of a node from the apply display properties feature
+ logging.info('Test section: Disable apply display properties using FolderDisplayOverrideAllowed')
+
+ testModelDisplayNode = self.testModelNode.GetDisplayNode()
+ self.assertTrue(testModelDisplayNode.GetFolderDisplayOverrideAllowed())
+
+ # Turn of override allowed
+ testModelDisplayNode.SetFolderDisplayOverrideAllowed(False)
+
+ # Check that color and opacity are not overridden
+ testModelActor = self.modelDisplayableManager.GetActorByID(testModelDisplayNode.GetID())
+
+ testModelCurrentColor = testModelActor.GetProperty().GetColor()
+ colorOverridden = False
+ if (testModelCurrentColor[0] == self.overrideColor[0] / 255 and
+ testModelCurrentColor[1] == self.overrideColor[1] / 255 and
+ testModelCurrentColor[2] == self.overrideColor[2] / 255):
+ colorOverridden = True
+ self.assertFalse(colorOverridden)
+
+ testModelCurrentOpacity = testModelActor.GetProperty().GetOpacity()
+ self.assertEqual(testModelCurrentOpacity, 1.0)
+
+ # ------------------------------------------------------------------------------
+ def startTiming(self):
+ self.timer = qt.QTime()
+ self.timer.start()
+
+ # ------------------------------------------------------------------------------
+ def stopTiming(self):
+ return self.timer.elapsed()
+
+ # ------------------------------------------------------------------------------
+ def getModelDisplayableManager(self):
+ if self.viewWidget is None:
+ logging.error('View widget is not created')
+ return None
+ managers = vtk.vtkCollection()
+ self.viewWidget.getDisplayableManagers(managers)
+ for i in range(managers.GetNumberOfItems()):
+ obj = managers.GetItemAsObject(i)
+ if obj.IsA('vtkMRMLModelDisplayableManager'):
+ return obj
+ logging.error('Failed to find the model displayable manager')
+ return None
diff --git a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py
index 7a855ceacda..b566e53e016 100644
--- a/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py
+++ b/Modules/Loadable/SubjectHierarchy/Testing/Python/SubjectHierarchyGenericSelfTest.py
@@ -15,32 +15,32 @@
#
class SubjectHierarchyGenericSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "SubjectHierarchyGenericSelfTest"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = ["SubjectHierarchy", "DICOM"]
- parent.contributors = ["Csaba Pinter (Queen's)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "SubjectHierarchyGenericSelfTest"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = ["SubjectHierarchy", "DICOM"]
+ parent.contributors = ["Csaba Pinter (Queen's)"]
+ parent.helpText = """
This is a self test for the Subject hierarchy module generic features.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was originally developed by Csaba Pinter, PerkLab, Queen's University and was supported through the Applied Cancer
Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
- self.parent = parent
+ self.parent = parent
- # Add this test to the SelfTest module's list for discovery when the module
- # is created. Since this module may be discovered before SelfTests itself,
- # create the list if it doesn't already exist.
- try:
- slicer.selfTests
- except AttributeError:
- slicer.selfTests = {}
- slicer.selfTests['SubjectHierarchyGenericSelfTest'] = self.runTest
+ # Add this test to the SelfTest module's list for discovery when the module
+ # is created. Since this module may be discovered before SelfTests itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.selfTests
+ except AttributeError:
+ slicer.selfTests = {}
+ slicer.selfTests['SubjectHierarchyGenericSelfTest'] = self.runTest
- def runTest(self, msec=100, **kwargs):
- tester = SubjectHierarchyGenericSelfTestTest()
- tester.runTest()
+ def runTest(self, msec=100, **kwargs):
+ tester = SubjectHierarchyGenericSelfTestTest()
+ tester.runTest()
#
@@ -48,8 +48,8 @@ def runTest(self, msec=100, **kwargs):
#
class SubjectHierarchyGenericSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
#
@@ -57,623 +57,623 @@ def setup(self):
#
class SubjectHierarchyGenericSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
+ """
- def __init__(self):
- pass
+ def __init__(self):
+ pass
class SubjectHierarchyGenericSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear()
-
- self.delayMs = 700
-
- # TODO: Comment out (sample code for debugging)
- # logFile = open('d:/pyTestLog.txt', 'w')
- # logFile.write(repr(slicer.modules.subjecthierarchygenericselftest) + '\n')
- # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n')
- # logFile.close()
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_SubjectHierarchyGenericSelfTest_FullTest1()
-
- # ------------------------------------------------------------------------------
- def test_SubjectHierarchyGenericSelfTest_FullTest1(self):
- # Check for SubjectHierarchy module
- self.assertIsNotNone(slicer.modules.subjecthierarchy)
-
- # Switch to subject hierarchy module so that the changes can be seen as the test goes
- slicer.util.selectModule('SubjectHierarchy')
-
- self.section_SetupPathsAndNames()
- self.section_ClearScene()
- self.section_LoadDicomDataWitchBatchProcessing()
- self.section_SaveScene()
- self.section_AddNodeToSubjectHierarchy()
- self.section_CLI()
- self.section_CreateSecondBranch()
- self.section_ReparentNodeInSubjectHierarchy()
- self.section_LoadScene()
- self.section_TestCircularParenthood()
- self.section_AttributeFilters()
- self.section_ComboboxFeatures()
-
- logging.info('Test finished')
-
- # ------------------------------------------------------------------------------
- def section_SetupPathsAndNames(self):
- # Set constants
- subjectHierarchyGenericSelfTestDir = slicer.app.temporaryPath + '/SubjectHierarchyGenericSelfTest'
- print('Test directory: ' + subjectHierarchyGenericSelfTestDir)
- if not os.access(subjectHierarchyGenericSelfTestDir, os.F_OK):
- os.mkdir(subjectHierarchyGenericSelfTestDir)
-
- self.dicomDataDir = subjectHierarchyGenericSelfTestDir + '/DicomData'
- if not os.access(self.dicomDataDir, os.F_OK):
- os.mkdir(self.dicomDataDir)
-
- self.dicomDatabaseDir = subjectHierarchyGenericSelfTestDir + '/CtkDicomDatabase'
- self.dicomZipFileUrl = TESTING_DATA_URL + 'SHA256/1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863'
- self.dicomZipChecksum = 'SHA256:1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863'
- self.dicomZipFilePath = subjectHierarchyGenericSelfTestDir + '/TestDicomCT.zip'
- self.expectedNumOfFilesInDicomDataDir = 10
- self.tempDir = subjectHierarchyGenericSelfTestDir + '/Temp'
- self.genericTestSceneFileName = self.tempDir + '/SubjectHierarchyGenericSelfTestScene.mrml'
-
- self.attributeFilterTestSceneFileUrl = TESTING_DATA_URL + 'SHA256/83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984'
- self.attributeFilterTestSceneChecksum = 'SHA256:83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984'
- self.attributeFilterTestSceneFileName = 'SubjectHierarchyAttributeFilterTestScene.mrb'
-
- self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID()
-
- self.loadedDicomStudyName = 'No study description (20110101)'
- self.loadedDicomVolumeName = '303: Unnamed Series'
- self.patientItemID = self.invalidItemID # To be filled in after adding
- self.patientOriginalName = ''
- self.patientNewName = 'TestPatient_1'
- self.studyItemID = self.invalidItemID
- self.studyOriginalName = ''
- self.studyNewName = 'No study description (20170107)'
- self.ctVolumeShItemID = self.invalidItemID
- self.ctVolumeOriginalName = ''
- self.ctVolumeNewName = '404: Unnamed Series'
- self.sampleLabelmapName = 'SampleLabelmap'
- self.sampleLabelmapNode = None
- self.sampleLabelmapShItemID = self.invalidItemID
- self.sampleModelName = 'SampleModel'
- self.sampleModelNode = None
- self.sampleModelShItemID = self.invalidItemID
- self.patient2Name = 'Patient2'
- self.patient2ItemID = self.invalidItemID
- self.study2Name = 'Study2'
- self.study2ItemID = self.invalidItemID
- self.folderName = 'Folder'
- self.folderItemID = self.invalidItemID
-
- # ------------------------------------------------------------------------------
- def section_ClearScene(self):
- self.delayDisplay("Clear scene", self.delayMs)
-
- # Clear the scene to make sure there is no crash (closing scene is a sensitive operation)
- slicer.mrmlScene.Clear()
-
- # Make sure there is only one subject hierarchy node after closing the scene
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # ------------------------------------------------------------------------------
- def section_LoadDicomDataWitchBatchProcessing(self):
- try:
- # Open Data module so that a subject hierarchy scene model is active
- # (which caused problems with batch processing)
- slicer.util.selectModule('Data')
-
- # Open test database and empty it
- with DICOMUtils.TemporaryDICOMDatabase(self.dicomDatabaseDir) as db:
- self.assertTrue(db.isOpen)
- self.assertEqual(slicer.dicomDatabase, db)
-
- slicer.mrmlScene.StartState(slicer.vtkMRMLScene.BatchProcessState)
-
- # Download, unzip, import, and load data. Verify loaded nodes.
- loadedNodes = {'vtkMRMLScalarVolumeNode': 1}
- with DICOMUtils.LoadDICOMFilesToDatabase( \
- self.dicomZipFileUrl, self.dicomZipFilePath, \
- self.dicomDataDir, self.expectedNumOfFilesInDicomDataDir, \
- {}, loadedNodes, checksum=self.dicomZipChecksum) as success:
- self.assertTrue(success)
-
- slicer.mrmlScene.EndState(slicer.vtkMRMLScene.BatchProcessState)
-
- self.assertEqual(len(slicer.util.getNodes('vtkMRMLSubjectHierarchyNode*')), 1)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
- loadedDicomVolumeItemID = shNode.GetItemByName(self.loadedDicomVolumeName)
- loadedDicomStudyItemID = shNode.GetItemByName(self.loadedDicomStudyName)
- self.assertEqual(shNode.GetItemParent(loadedDicomVolumeItemID), loadedDicomStudyItemID)
-
- except Exception as e:
- import traceback
- traceback.print_exc()
- self.delayDisplay('Test caused exception!\n' + str(e), self.delayMs * 2)
-
- # ------------------------------------------------------------------------------
- def section_SaveScene(self):
- self.delayDisplay("Save scene", self.delayMs)
-
- if not os.access(self.tempDir, os.F_OK):
- os.mkdir(self.tempDir)
-
- if os.access(self.genericTestSceneFileName, os.F_OK):
- os.remove(self.genericTestSceneFileName)
-
- # Save MRML scene into file
- slicer.mrmlScene.Commit(self.genericTestSceneFileName)
- logging.info('Scene saved into ' + self.genericTestSceneFileName)
-
- readable = os.access(self.genericTestSceneFileName, os.R_OK)
- self.assertTrue(readable)
-
- # ------------------------------------------------------------------------------
- def section_AddNodeToSubjectHierarchy(self):
- self.delayDisplay("Add node to subject hierarchy", self.delayMs)
-
- # Get volume previously loaded from DICOM
- volumeNodes = list(slicer.util.getNodes('vtkMRMLScalarVolumeNode*').values())
- ctVolumeNode = volumeNodes[len(volumeNodes) - 1]
- self.assertIsNotNone(ctVolumeNode)
-
- # Create sample labelmap and model and add them in subject hierarchy
- self.sampleLabelmapNode = self.createSampleLabelmapVolumeNode(ctVolumeNode, self.sampleLabelmapName, 2)
- sampleModelColor = [0.0, 1.0, 0.0]
- self.sampleModelNode = self.createSampleModelNode(self.sampleModelName, sampleModelColor, ctVolumeNode)
-
- # Get subject hierarchy scene model and node
- dataWidget = slicer.modules.data.widgetRepresentation()
- self.assertIsNotNone(dataWidget)
- shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView')
- self.assertIsNotNone(shTreeView)
- shModel = shTreeView.model()
- self.assertIsNotNone(shModel)
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Get and check subject hierarchy items for the data nodes
- self.ctVolumeShItemID = shNode.GetItemByDataNode(ctVolumeNode)
- self.ctVolumeOriginalName = shNode.GetItemName(self.ctVolumeShItemID)
- self.assertIsNotNone(self.ctVolumeShItemID)
-
- self.sampleLabelmapShItemID = shNode.GetItemByDataNode(self.sampleLabelmapNode)
- self.assertIsNotNone(self.sampleLabelmapShItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps')
-
- self.sampleModelShItemID = shNode.GetItemByDataNode(self.sampleModelNode)
- self.assertIsNotNone(self.sampleModelShItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models')
-
- # Save item IDs for scene load testing
- self.studyItemID = shNode.GetItemParent(self.ctVolumeShItemID)
- self.studyOriginalName = shNode.GetItemName(self.studyItemID)
- self.assertIsNotNone(self.studyItemID)
-
- self.patientItemID = shNode.GetItemParent(self.studyItemID)
- self.patientOriginalName = shNode.GetItemName(self.patientItemID)
- self.assertIsNotNone(self.patientItemID)
-
- # Verify DICOM levels
- self.assertEqual(shNode.GetItemLevel(self.patientItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelPatient())
- self.assertEqual(shNode.GetItemLevel(self.studyItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy())
- self.assertEqual(shNode.GetItemLevel(self.ctVolumeShItemID), "")
-
- # Add model and labelmap to the created study
- retVal1 = shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID)
- self.assertTrue(retVal1)
- retVal2 = shModel.reparent(self.sampleModelShItemID, self.studyItemID)
- self.assertTrue(retVal2)
- qt.QApplication.processEvents()
-
- # ------------------------------------------------------------------------------
- def section_CLI(self):
- self.delayDisplay("Test command-line interface", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Get CT volume
- ctVolumeNode = shNode.GetItemDataNode(self.ctVolumeShItemID)
- self.assertIsNotNone(ctVolumeNode)
-
- # Create output volume
- resampledVolumeNode = slicer.vtkMRMLScalarVolumeNode()
- resampledVolumeNode.SetName(ctVolumeNode.GetName() + '_Resampled_10x10x10mm')
- slicer.mrmlScene.AddNode(resampledVolumeNode)
-
- # Resample
- resampleParameters = {'outputPixelSpacing': '24.5,24.5,11.5', 'interpolationType': 'lanczos',
- 'InputVolume': ctVolumeNode.GetID(), 'OutputVolume': resampledVolumeNode.GetID()}
- slicer.cli.run(slicer.modules.resamplescalarvolume, None, resampleParameters, wait_for_completion=True)
- self.delayDisplay("Wait for CLI logic to add result to same branch", self.delayMs)
-
- # Check if output is also under the same study node
- resampledVolumeItemID = shNode.GetItemByDataNode(resampledVolumeNode)
- self.assertIsNotNone(resampledVolumeItemID)
- self.assertEqual(shNode.GetItemParent(resampledVolumeItemID), self.studyItemID)
-
- # ------------------------------------------------------------------------------
- def section_CreateSecondBranch(self):
- self.delayDisplay("Create second branch in subject hierarchy", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Create second patient, study, and a folder
- self.patient2ItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), self.patient2Name)
- self.study2ItemID = shNode.CreateStudyItem(self.patient2ItemID, self.study2Name)
- self.folderItemID = shNode.CreateFolderItem(self.study2ItemID, self.folderName)
-
- # Check if the items have the right parents
- self.assertEqual(shNode.GetItemParent(self.patient2ItemID), shNode.GetSceneItemID())
- self.assertEqual(shNode.GetItemParent(self.study2ItemID), self.patient2ItemID)
- self.assertEqual(shNode.GetItemParent(self.folderItemID), self.study2ItemID)
-
- # ------------------------------------------------------------------------------
- def section_ReparentNodeInSubjectHierarchy(self):
- self.delayDisplay("Reparent node in subject hierarchy", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Get subject hierarchy scene model
- dataWidget = slicer.modules.data.widgetRepresentation()
- self.assertIsNotNone(dataWidget)
- shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView')
- self.assertIsNotNone(shTreeView)
- shModel = shTreeView.model()
- self.assertIsNotNone(shModel)
-
- # Reparent using the item model
- shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID)
- self.assertEqual(shNode.GetItemParent(self.sampleLabelmapShItemID), self.studyItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps')
-
- # Reparent using the node's set parent function
- shNode.SetItemParent(self.ctVolumeShItemID, self.study2ItemID)
- self.assertEqual(shNode.GetItemParent(self.ctVolumeShItemID), self.study2ItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(self.ctVolumeShItemID), 'Volumes')
-
- # Reparent using the node's create item function
- shNode.CreateItem(self.folderItemID, self.sampleModelNode)
- self.assertEqual(shNode.GetItemParent(self.sampleModelShItemID), self.folderItemID)
- self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models')
-
- # ------------------------------------------------------------------------------
- def section_LoadScene(self):
- self.delayDisplay("Load scene", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- # Rename existing items so that when the scene is loaded again they are different
- shNode.SetItemName(self.patientItemID, self.patientNewName)
- shNode.SetItemName(self.studyItemID, self.studyNewName)
- shNode.SetItemName(self.ctVolumeShItemID, self.ctVolumeNewName)
-
- # Load the saved scene
- slicer.util.loadScene(self.genericTestSceneFileName)
-
- # Check number of nodes in the scene
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLScalarVolumeNode'), 4)
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode'), 4) # Including the three slice view models
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1)
-
- # Check if the items are in the right hierarchy with the right names
- self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientNewName), self.patientItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.patientItemID, self.studyNewName), self.studyItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.studyItemID, self.sampleLabelmapName), self.sampleLabelmapShItemID)
-
- self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patient2Name), self.patient2ItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.patient2ItemID, self.study2Name), self.study2ItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.folderName), self.folderItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.folderItemID, self.sampleModelName), self.sampleModelShItemID)
- self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.ctVolumeNewName), self.ctVolumeShItemID)
-
- loadedPatientItemID = shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientOriginalName)
- self.assertIsNotNone(loadedPatientItemID)
- loadedStudyItemID = shNode.GetItemChildWithName(loadedPatientItemID, self.studyOriginalName)
- self.assertIsNotNone(loadedStudyItemID)
- loadedCtVolumeShItemID = shNode.GetItemChildWithName(loadedStudyItemID, self.ctVolumeOriginalName)
- self.assertIsNotNone(loadedCtVolumeShItemID)
-
- # Print subject hierarchy after the test
- logging.info(shNode)
-
- # ------------------------------------------------------------------------------
- def section_TestCircularParenthood(self):
- # Test case for https://issues.slicer.org/view.php?id=4713
- self.delayDisplay("Test circular parenthood", self.delayMs)
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertIsNotNone(shNode)
-
- sceneItemID = shNode.GetSceneItemID()
- mainfolder_ID = shNode.CreateFolderItem(sceneItemID, "Main Folder")
- subfolder_ID = shNode.CreateFolderItem(sceneItemID, "Sub Folder")
- shNode.SetItemParent(subfolder_ID, mainfolder_ID) # Regular hierarchy setting
- shNode.SetItemParent(mainfolder_ID, subfolder_ID) # Makes slicer crash instead of returning an error
-
- # ------------------------------------------------------------------------------
- def section_AttributeFilters(self):
- self.delayDisplay("Attribute filters", self.delayMs)
-
- import SampleData
- sceneFile = SampleData.downloadFromURL(
- fileNames=self.attributeFilterTestSceneFileName,
- uris=self.attributeFilterTestSceneFileUrl,
- # loadFiles=True,
- checksums=self.attributeFilterTestSceneChecksum)[0]
- if not os.path.exists(sceneFile):
- logging.error('Failed to download attribute filter test scene to path ' + str(sceneFile))
- self.assertTrue(os.path.exists(sceneFile))
-
- slicer.mrmlScene.Clear()
- ioManager = slicer.app.ioManager()
- ioManager.loadFile(sceneFile)
-
- # The loaded scene contains the following items and data nodes
- #
- # Scene
- # +----- NewFolder
- # | +----------- MarkupsAngle (DataNode:vtkMRMLMarkupsAngleNode1)
- # | | (ItemAttributes: ItemAttribute1:'1')
- # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1')
- # | +----------- MarkupsAngle_1 (DataNode:vtkMRMLMarkupsAngleNode2)
- # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1')
- # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3)
- # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ParentAttribute:'')
- # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3)
- # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ChildAttribute:'')
- # +----- NewFolder_1
- # | (ItemAttributes: FolderAttribute1:'1')
- # | +----------- MarkupsCurve_1 (DataNode:vtkMRMLMarkupsCurveNode2)
- # | | (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green')
- # | +----------- MarkupsCurve (DataNode:vtkMRMLMarkupsCurveNode1)
- # | (ItemAttributes: ItemAttribute2:'2')
- # | (NodeAttributes: Markups.MovingMarkupIndex:'2', Sajt:'Green')
- # +----- MarkupsCurve_2 (DataNode:vtkMRMLMarkupsCurveNode1)
- # (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green')
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
-
- # Check scene validity
- self.assertEqual(9, shNode.GetNumberOfItems())
- self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsNode'), 7)
-
- # Create test SH tree view
- shTreeView = slicer.qMRMLSubjectHierarchyTreeView()
- shTreeView.setMRMLScene(slicer.mrmlScene)
- shTreeView.show()
-
- shProxyModel = shTreeView.sortFilterProxyModel()
-
- def testAttributeFilters(filteredObject, proxyModel):
- # Check include node attribute name filter
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
- filteredObject.addNodeAttributeFilter('Sajt')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- filteredObject.includeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- # Check attribute value filter
- filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3)
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
- filteredObject.includeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '3')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
- filteredObject.includeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Check exclude node attribute name filter (overrides include node attribute name filter)
- filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
- filteredObject.excludeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- # Check if exclude indeed overrides include node attribute name filter
- filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingMarkupIndex']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- filteredObject.includeNodeAttributeNamesFilter = []
- filteredObject.excludeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Check include item attribute name filter
- filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
- filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'FolderAttribute1']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
- filteredObject.addItemAttributeFilter('ItemAttribute2')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'ItemAttribute2']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- filteredObject.includeItemAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Check legacy (item) attribute value filter
- filteredObject.attributeNameFilter = 'ItemAttribute1'
- filteredObject.attributeValueFilter = '1'
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
- filteredObject.attributeNameFilter = ''
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Check exclude item attribute name filter (overrides include item attribute filter)
- filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8)
- filteredObject.excludeItemAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- # Check if exclude indeed overrides include item attribute filter
- filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
- filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1']
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 0)
- filteredObject.includeItemAttributeNamesFilter = []
- filteredObject.excludeItemAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- filteredObject.excludeItemAttributeNamesFilter = ['FolderAttribute1']
- # Note: Shown only 6 because accepted children of rejected parents are not shown
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8)
- filteredObject.excludeItemAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Check attribute filtering with class name and attribute value
- filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3, True, 'vtkMRMLMarkupsCurveNode')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
- filteredObject.addNodeAttributeFilter('ParentAttribute', '', True, 'vtkMRMLMarkupsAngleNode')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
- filteredObject.addNodeAttributeFilter('ChildAttribute', '', True, 'vtkMRMLMarkupsAngleNode')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 6)
- filteredObject.includeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
- # Check with empty attribute value
- filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '', True, 'vtkMRMLMarkupsCurveNode')
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
- filteredObject.includeNodeAttributeNamesFilter = []
- self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- logging.info('Test attribute filters on proxy model directly')
- testAttributeFilters(shProxyModel, shProxyModel)
- logging.info('Test attribute filters on tree view')
- testAttributeFilters(shTreeView, shProxyModel)
-
- # ------------------------------------------------------------------------------
- def section_ComboboxFeatures(self):
- self.delayDisplay("Combobox features", self.delayMs)
-
- comboBox = slicer.qMRMLSubjectHierarchyComboBox()
- comboBox.setMRMLScene(slicer.mrmlScene)
- comboBox.show()
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Enable None item, number of accepted SH items is the same (None does not have a corresponding accepted SH item)
- comboBox.noneEnabled = True
- self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9)
-
- # Default text
- self.assertEqual(comboBox.defaultText, 'Select subject hierarchy item')
-
- # Select node, include parent names in current item text (when collapsed)
- markupsCurve1ItemID = shNode.GetItemByName('MarkupsCurve_1')
- comboBox.setCurrentItem(markupsCurve1ItemID)
- self.assertEqual(comboBox.defaultText, 'NewFolder_1 / MarkupsCurve_1')
-
- # Select None item
- comboBox.setCurrentItem(0)
- self.assertEqual(comboBox.defaultText, comboBox.noneDisplay)
-
- # Do not show parent names in current item text
- comboBox.showCurrentItemParents = False
- comboBox.setCurrentItem(markupsCurve1ItemID)
- self.assertEqual(comboBox.defaultText, 'MarkupsCurve_1')
-
- # Change None item name
- comboBox.noneDisplay = 'No selection'
- comboBox.setCurrentItem(0)
- self.assertEqual(comboBox.defaultText, comboBox.noneDisplay)
-
- # ------------------------------------------------------------------------------
- # Utility functions
-
- # ------------------------------------------------------------------------------
- # Create sample labelmap with same geometry as input volume
- def createSampleLabelmapVolumeNode(self, volumeNode, name, label, colorNode=None):
- self.assertIsNotNone(volumeNode)
- self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode'))
- self.assertTrue(label > 0)
-
- sampleLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
- sampleLabelmapNode.SetName(name)
- sampleLabelmapNode = slicer.mrmlScene.AddNode(sampleLabelmapNode)
- sampleLabelmapNode.Copy(volumeNode)
- imageData = sampleLabelmapNode.GetImageData()
- extent = imageData.GetExtent()
- for x in range(extent[0], extent[1] + 1):
- for y in range(extent[2], extent[3] + 1):
- for z in range(extent[4], extent[5] + 1):
- if ((x >= (extent[1] / 4) and x <= (extent[1] / 4) * 3) and
- (y >= (extent[3] / 4) and y <= (extent[3] / 4) * 3) and
- (z >= (extent[5] / 4) and z <= (extent[5] / 4) * 3)):
- imageData.SetScalarComponentFromDouble(x, y, z, 0, label)
- else:
- imageData.SetScalarComponentFromDouble(x, y, z, 0, 0)
-
- # Display labelmap
- labelmapVolumeDisplayNode = slicer.vtkMRMLLabelMapVolumeDisplayNode()
- slicer.mrmlScene.AddNode(labelmapVolumeDisplayNode)
- if colorNode is None:
- colorNode = slicer.util.getNode('GenericAnatomyColors')
- self.assertIsNotNone(colorNode)
- labelmapVolumeDisplayNode.SetAndObserveColorNodeID(colorNode.GetID())
- labelmapVolumeDisplayNode.VisibilityOn()
- sampleLabelmapName = slicer.mrmlScene.GenerateUniqueName(name)
- sampleLabelmapNode.SetName(sampleLabelmapName)
- sampleLabelmapNode.SetAndObserveDisplayNodeID(labelmapVolumeDisplayNode.GetID())
-
- return sampleLabelmapNode
-
- # ------------------------------------------------------------------------------
- # Create sphere model at the centre of an input volume
- def createSampleModelNode(self, name, color, volumeNode=None):
- if volumeNode:
- self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode'))
- bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
- volumeNode.GetRASBounds(bounds)
- x = (bounds[0] + bounds[1]) / 2
- y = (bounds[2] + bounds[3]) / 2
- z = (bounds[4] + bounds[5]) / 2
- radius = min(bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]) / 3.0
- else:
- radius = 50
- x = y = z = 0
-
- # Taken from: https://mantisarchive.slicer.org/view.php?id=1536
- sphere = vtk.vtkSphereSource()
- sphere.SetCenter(x, y, z)
- sphere.SetRadius(radius)
-
- modelNode = slicer.vtkMRMLModelNode()
- modelNode.SetName(name)
- modelNode = slicer.mrmlScene.AddNode(modelNode)
- modelNode.SetPolyDataConnection(sphere.GetOutputPort())
- modelNode.SetHideFromEditors(0)
-
- displayNode = slicer.vtkMRMLModelDisplayNode()
- slicer.mrmlScene.AddNode(displayNode)
- displayNode.Visibility2DOn()
- displayNode.VisibilityOn()
- displayNode.SetColor(color[0], color[1], color[2])
- modelNode.SetAndObserveDisplayNodeID(displayNode.GetID())
-
- return modelNode
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear()
+
+ self.delayMs = 700
+
+ # TODO: Comment out (sample code for debugging)
+ # logFile = open('d:/pyTestLog.txt', 'w')
+ # logFile.write(repr(slicer.modules.subjecthierarchygenericselftest) + '\n')
+ # logFile.write(repr(slicer.modules.subjecthierarchy) + '\n')
+ # logFile.close()
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SubjectHierarchyGenericSelfTest_FullTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_SubjectHierarchyGenericSelfTest_FullTest1(self):
+ # Check for SubjectHierarchy module
+ self.assertIsNotNone(slicer.modules.subjecthierarchy)
+
+ # Switch to subject hierarchy module so that the changes can be seen as the test goes
+ slicer.util.selectModule('SubjectHierarchy')
+
+ self.section_SetupPathsAndNames()
+ self.section_ClearScene()
+ self.section_LoadDicomDataWitchBatchProcessing()
+ self.section_SaveScene()
+ self.section_AddNodeToSubjectHierarchy()
+ self.section_CLI()
+ self.section_CreateSecondBranch()
+ self.section_ReparentNodeInSubjectHierarchy()
+ self.section_LoadScene()
+ self.section_TestCircularParenthood()
+ self.section_AttributeFilters()
+ self.section_ComboboxFeatures()
+
+ logging.info('Test finished')
+
+ # ------------------------------------------------------------------------------
+ def section_SetupPathsAndNames(self):
+ # Set constants
+ subjectHierarchyGenericSelfTestDir = slicer.app.temporaryPath + '/SubjectHierarchyGenericSelfTest'
+ print('Test directory: ' + subjectHierarchyGenericSelfTestDir)
+ if not os.access(subjectHierarchyGenericSelfTestDir, os.F_OK):
+ os.mkdir(subjectHierarchyGenericSelfTestDir)
+
+ self.dicomDataDir = subjectHierarchyGenericSelfTestDir + '/DicomData'
+ if not os.access(self.dicomDataDir, os.F_OK):
+ os.mkdir(self.dicomDataDir)
+
+ self.dicomDatabaseDir = subjectHierarchyGenericSelfTestDir + '/CtkDicomDatabase'
+ self.dicomZipFileUrl = TESTING_DATA_URL + 'SHA256/1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863'
+ self.dicomZipChecksum = 'SHA256:1aa0bb177bbf6471ca5f2192340a6cecdedb81b33506b03ff316c6b5f624e863'
+ self.dicomZipFilePath = subjectHierarchyGenericSelfTestDir + '/TestDicomCT.zip'
+ self.expectedNumOfFilesInDicomDataDir = 10
+ self.tempDir = subjectHierarchyGenericSelfTestDir + '/Temp'
+ self.genericTestSceneFileName = self.tempDir + '/SubjectHierarchyGenericSelfTestScene.mrml'
+
+ self.attributeFilterTestSceneFileUrl = TESTING_DATA_URL + 'SHA256/83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984'
+ self.attributeFilterTestSceneChecksum = 'SHA256:83e0df42d178405dccaf5a87d0661dd4bad71b535c6f15457344a71c4c0b7984'
+ self.attributeFilterTestSceneFileName = 'SubjectHierarchyAttributeFilterTestScene.mrb'
+
+ self.invalidItemID = slicer.vtkMRMLSubjectHierarchyNode.GetInvalidItemID()
+
+ self.loadedDicomStudyName = 'No study description (20110101)'
+ self.loadedDicomVolumeName = '303: Unnamed Series'
+ self.patientItemID = self.invalidItemID # To be filled in after adding
+ self.patientOriginalName = ''
+ self.patientNewName = 'TestPatient_1'
+ self.studyItemID = self.invalidItemID
+ self.studyOriginalName = ''
+ self.studyNewName = 'No study description (20170107)'
+ self.ctVolumeShItemID = self.invalidItemID
+ self.ctVolumeOriginalName = ''
+ self.ctVolumeNewName = '404: Unnamed Series'
+ self.sampleLabelmapName = 'SampleLabelmap'
+ self.sampleLabelmapNode = None
+ self.sampleLabelmapShItemID = self.invalidItemID
+ self.sampleModelName = 'SampleModel'
+ self.sampleModelNode = None
+ self.sampleModelShItemID = self.invalidItemID
+ self.patient2Name = 'Patient2'
+ self.patient2ItemID = self.invalidItemID
+ self.study2Name = 'Study2'
+ self.study2ItemID = self.invalidItemID
+ self.folderName = 'Folder'
+ self.folderItemID = self.invalidItemID
+
+ # ------------------------------------------------------------------------------
+ def section_ClearScene(self):
+ self.delayDisplay("Clear scene", self.delayMs)
+
+ # Clear the scene to make sure there is no crash (closing scene is a sensitive operation)
+ slicer.mrmlScene.Clear()
+
+ # Make sure there is only one subject hierarchy node after closing the scene
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # ------------------------------------------------------------------------------
+ def section_LoadDicomDataWitchBatchProcessing(self):
+ try:
+ # Open Data module so that a subject hierarchy scene model is active
+ # (which caused problems with batch processing)
+ slicer.util.selectModule('Data')
+
+ # Open test database and empty it
+ with DICOMUtils.TemporaryDICOMDatabase(self.dicomDatabaseDir) as db:
+ self.assertTrue(db.isOpen)
+ self.assertEqual(slicer.dicomDatabase, db)
+
+ slicer.mrmlScene.StartState(slicer.vtkMRMLScene.BatchProcessState)
+
+ # Download, unzip, import, and load data. Verify loaded nodes.
+ loadedNodes = {'vtkMRMLScalarVolumeNode': 1}
+ with DICOMUtils.LoadDICOMFilesToDatabase( \
+ self.dicomZipFileUrl, self.dicomZipFilePath, \
+ self.dicomDataDir, self.expectedNumOfFilesInDicomDataDir, \
+ {}, loadedNodes, checksum=self.dicomZipChecksum) as success:
+ self.assertTrue(success)
+
+ slicer.mrmlScene.EndState(slicer.vtkMRMLScene.BatchProcessState)
+
+ self.assertEqual(len(slicer.util.getNodes('vtkMRMLSubjectHierarchyNode*')), 1)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+ loadedDicomVolumeItemID = shNode.GetItemByName(self.loadedDicomVolumeName)
+ loadedDicomStudyItemID = shNode.GetItemByName(self.loadedDicomStudyName)
+ self.assertEqual(shNode.GetItemParent(loadedDicomVolumeItemID), loadedDicomStudyItemID)
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ self.delayDisplay('Test caused exception!\n' + str(e), self.delayMs * 2)
+
+ # ------------------------------------------------------------------------------
+ def section_SaveScene(self):
+ self.delayDisplay("Save scene", self.delayMs)
+
+ if not os.access(self.tempDir, os.F_OK):
+ os.mkdir(self.tempDir)
+
+ if os.access(self.genericTestSceneFileName, os.F_OK):
+ os.remove(self.genericTestSceneFileName)
+
+ # Save MRML scene into file
+ slicer.mrmlScene.Commit(self.genericTestSceneFileName)
+ logging.info('Scene saved into ' + self.genericTestSceneFileName)
+
+ readable = os.access(self.genericTestSceneFileName, os.R_OK)
+ self.assertTrue(readable)
+
+ # ------------------------------------------------------------------------------
+ def section_AddNodeToSubjectHierarchy(self):
+ self.delayDisplay("Add node to subject hierarchy", self.delayMs)
+
+ # Get volume previously loaded from DICOM
+ volumeNodes = list(slicer.util.getNodes('vtkMRMLScalarVolumeNode*').values())
+ ctVolumeNode = volumeNodes[len(volumeNodes) - 1]
+ self.assertIsNotNone(ctVolumeNode)
+
+ # Create sample labelmap and model and add them in subject hierarchy
+ self.sampleLabelmapNode = self.createSampleLabelmapVolumeNode(ctVolumeNode, self.sampleLabelmapName, 2)
+ sampleModelColor = [0.0, 1.0, 0.0]
+ self.sampleModelNode = self.createSampleModelNode(self.sampleModelName, sampleModelColor, ctVolumeNode)
+
+ # Get subject hierarchy scene model and node
+ dataWidget = slicer.modules.data.widgetRepresentation()
+ self.assertIsNotNone(dataWidget)
+ shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView')
+ self.assertIsNotNone(shTreeView)
+ shModel = shTreeView.model()
+ self.assertIsNotNone(shModel)
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Get and check subject hierarchy items for the data nodes
+ self.ctVolumeShItemID = shNode.GetItemByDataNode(ctVolumeNode)
+ self.ctVolumeOriginalName = shNode.GetItemName(self.ctVolumeShItemID)
+ self.assertIsNotNone(self.ctVolumeShItemID)
+
+ self.sampleLabelmapShItemID = shNode.GetItemByDataNode(self.sampleLabelmapNode)
+ self.assertIsNotNone(self.sampleLabelmapShItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps')
+
+ self.sampleModelShItemID = shNode.GetItemByDataNode(self.sampleModelNode)
+ self.assertIsNotNone(self.sampleModelShItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models')
+
+ # Save item IDs for scene load testing
+ self.studyItemID = shNode.GetItemParent(self.ctVolumeShItemID)
+ self.studyOriginalName = shNode.GetItemName(self.studyItemID)
+ self.assertIsNotNone(self.studyItemID)
+
+ self.patientItemID = shNode.GetItemParent(self.studyItemID)
+ self.patientOriginalName = shNode.GetItemName(self.patientItemID)
+ self.assertIsNotNone(self.patientItemID)
+
+ # Verify DICOM levels
+ self.assertEqual(shNode.GetItemLevel(self.patientItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelPatient())
+ self.assertEqual(shNode.GetItemLevel(self.studyItemID), slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy())
+ self.assertEqual(shNode.GetItemLevel(self.ctVolumeShItemID), "")
+
+ # Add model and labelmap to the created study
+ retVal1 = shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID)
+ self.assertTrue(retVal1)
+ retVal2 = shModel.reparent(self.sampleModelShItemID, self.studyItemID)
+ self.assertTrue(retVal2)
+ qt.QApplication.processEvents()
+
+ # ------------------------------------------------------------------------------
+ def section_CLI(self):
+ self.delayDisplay("Test command-line interface", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Get CT volume
+ ctVolumeNode = shNode.GetItemDataNode(self.ctVolumeShItemID)
+ self.assertIsNotNone(ctVolumeNode)
+
+ # Create output volume
+ resampledVolumeNode = slicer.vtkMRMLScalarVolumeNode()
+ resampledVolumeNode.SetName(ctVolumeNode.GetName() + '_Resampled_10x10x10mm')
+ slicer.mrmlScene.AddNode(resampledVolumeNode)
+
+ # Resample
+ resampleParameters = {'outputPixelSpacing': '24.5,24.5,11.5', 'interpolationType': 'lanczos',
+ 'InputVolume': ctVolumeNode.GetID(), 'OutputVolume': resampledVolumeNode.GetID()}
+ slicer.cli.run(slicer.modules.resamplescalarvolume, None, resampleParameters, wait_for_completion=True)
+ self.delayDisplay("Wait for CLI logic to add result to same branch", self.delayMs)
+
+ # Check if output is also under the same study node
+ resampledVolumeItemID = shNode.GetItemByDataNode(resampledVolumeNode)
+ self.assertIsNotNone(resampledVolumeItemID)
+ self.assertEqual(shNode.GetItemParent(resampledVolumeItemID), self.studyItemID)
+
+ # ------------------------------------------------------------------------------
+ def section_CreateSecondBranch(self):
+ self.delayDisplay("Create second branch in subject hierarchy", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Create second patient, study, and a folder
+ self.patient2ItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), self.patient2Name)
+ self.study2ItemID = shNode.CreateStudyItem(self.patient2ItemID, self.study2Name)
+ self.folderItemID = shNode.CreateFolderItem(self.study2ItemID, self.folderName)
+
+ # Check if the items have the right parents
+ self.assertEqual(shNode.GetItemParent(self.patient2ItemID), shNode.GetSceneItemID())
+ self.assertEqual(shNode.GetItemParent(self.study2ItemID), self.patient2ItemID)
+ self.assertEqual(shNode.GetItemParent(self.folderItemID), self.study2ItemID)
+
+ # ------------------------------------------------------------------------------
+ def section_ReparentNodeInSubjectHierarchy(self):
+ self.delayDisplay("Reparent node in subject hierarchy", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Get subject hierarchy scene model
+ dataWidget = slicer.modules.data.widgetRepresentation()
+ self.assertIsNotNone(dataWidget)
+ shTreeView = slicer.util.findChild(dataWidget, name='SubjectHierarchyTreeView')
+ self.assertIsNotNone(shTreeView)
+ shModel = shTreeView.model()
+ self.assertIsNotNone(shModel)
+
+ # Reparent using the item model
+ shModel.reparent(self.sampleLabelmapShItemID, self.studyItemID)
+ self.assertEqual(shNode.GetItemParent(self.sampleLabelmapShItemID), self.studyItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleLabelmapShItemID), 'LabelMaps')
+
+ # Reparent using the node's set parent function
+ shNode.SetItemParent(self.ctVolumeShItemID, self.study2ItemID)
+ self.assertEqual(shNode.GetItemParent(self.ctVolumeShItemID), self.study2ItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(self.ctVolumeShItemID), 'Volumes')
+
+ # Reparent using the node's create item function
+ shNode.CreateItem(self.folderItemID, self.sampleModelNode)
+ self.assertEqual(shNode.GetItemParent(self.sampleModelShItemID), self.folderItemID)
+ self.assertEqual(shNode.GetItemOwnerPluginName(self.sampleModelShItemID), 'Models')
+
+ # ------------------------------------------------------------------------------
+ def section_LoadScene(self):
+ self.delayDisplay("Load scene", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ # Rename existing items so that when the scene is loaded again they are different
+ shNode.SetItemName(self.patientItemID, self.patientNewName)
+ shNode.SetItemName(self.studyItemID, self.studyNewName)
+ shNode.SetItemName(self.ctVolumeShItemID, self.ctVolumeNewName)
+
+ # Load the saved scene
+ slicer.util.loadScene(self.genericTestSceneFileName)
+
+ # Check number of nodes in the scene
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLScalarVolumeNode'), 4)
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLModelNode'), 4) # Including the three slice view models
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLSubjectHierarchyNode'), 1)
+
+ # Check if the items are in the right hierarchy with the right names
+ self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientNewName), self.patientItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.patientItemID, self.studyNewName), self.studyItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.studyItemID, self.sampleLabelmapName), self.sampleLabelmapShItemID)
+
+ self.assertEqual(shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patient2Name), self.patient2ItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.patient2ItemID, self.study2Name), self.study2ItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.folderName), self.folderItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.folderItemID, self.sampleModelName), self.sampleModelShItemID)
+ self.assertEqual(shNode.GetItemChildWithName(self.study2ItemID, self.ctVolumeNewName), self.ctVolumeShItemID)
+
+ loadedPatientItemID = shNode.GetItemChildWithName(shNode.GetSceneItemID(), self.patientOriginalName)
+ self.assertIsNotNone(loadedPatientItemID)
+ loadedStudyItemID = shNode.GetItemChildWithName(loadedPatientItemID, self.studyOriginalName)
+ self.assertIsNotNone(loadedStudyItemID)
+ loadedCtVolumeShItemID = shNode.GetItemChildWithName(loadedStudyItemID, self.ctVolumeOriginalName)
+ self.assertIsNotNone(loadedCtVolumeShItemID)
+
+ # Print subject hierarchy after the test
+ logging.info(shNode)
+
+ # ------------------------------------------------------------------------------
+ def section_TestCircularParenthood(self):
+ # Test case for https://issues.slicer.org/view.php?id=4713
+ self.delayDisplay("Test circular parenthood", self.delayMs)
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertIsNotNone(shNode)
+
+ sceneItemID = shNode.GetSceneItemID()
+ mainfolder_ID = shNode.CreateFolderItem(sceneItemID, "Main Folder")
+ subfolder_ID = shNode.CreateFolderItem(sceneItemID, "Sub Folder")
+ shNode.SetItemParent(subfolder_ID, mainfolder_ID) # Regular hierarchy setting
+ shNode.SetItemParent(mainfolder_ID, subfolder_ID) # Makes slicer crash instead of returning an error
+
+ # ------------------------------------------------------------------------------
+ def section_AttributeFilters(self):
+ self.delayDisplay("Attribute filters", self.delayMs)
+
+ import SampleData
+ sceneFile = SampleData.downloadFromURL(
+ fileNames=self.attributeFilterTestSceneFileName,
+ uris=self.attributeFilterTestSceneFileUrl,
+ # loadFiles=True,
+ checksums=self.attributeFilterTestSceneChecksum)[0]
+ if not os.path.exists(sceneFile):
+ logging.error('Failed to download attribute filter test scene to path ' + str(sceneFile))
+ self.assertTrue(os.path.exists(sceneFile))
+
+ slicer.mrmlScene.Clear()
+ ioManager = slicer.app.ioManager()
+ ioManager.loadFile(sceneFile)
+
+ # The loaded scene contains the following items and data nodes
+ #
+ # Scene
+ # +----- NewFolder
+ # | +----------- MarkupsAngle (DataNode:vtkMRMLMarkupsAngleNode1)
+ # | | (ItemAttributes: ItemAttribute1:'1')
+ # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1')
+ # | +----------- MarkupsAngle_1 (DataNode:vtkMRMLMarkupsAngleNode2)
+ # | | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1')
+ # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3)
+ # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ParentAttribute:'')
+ # | +----------- MarkupsAngle_2 (DataNode:vtkMRMLMarkupsAngleNode3)
+ # | (NodeAttributes: Markups.MovingInSliceView:'Red', Markups.MovingMarkupIndex:'1', ChildAttribute:'')
+ # +----- NewFolder_1
+ # | (ItemAttributes: FolderAttribute1:'1')
+ # | +----------- MarkupsCurve_1 (DataNode:vtkMRMLMarkupsCurveNode2)
+ # | | (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green')
+ # | +----------- MarkupsCurve (DataNode:vtkMRMLMarkupsCurveNode1)
+ # | (ItemAttributes: ItemAttribute2:'2')
+ # | (NodeAttributes: Markups.MovingMarkupIndex:'2', Sajt:'Green')
+ # +----- MarkupsCurve_2 (DataNode:vtkMRMLMarkupsCurveNode1)
+ # (NodeAttributes: Markups.MovingMarkupIndex:'3', Sajt:'Green')
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+
+ # Check scene validity
+ self.assertEqual(9, shNode.GetNumberOfItems())
+ self.assertEqual(slicer.mrmlScene.GetNumberOfNodesByClass('vtkMRMLMarkupsNode'), 7)
+
+ # Create test SH tree view
+ shTreeView = slicer.qMRMLSubjectHierarchyTreeView()
+ shTreeView.setMRMLScene(slicer.mrmlScene)
+ shTreeView.show()
+
+ shProxyModel = shTreeView.sortFilterProxyModel()
+
+ def testAttributeFilters(filteredObject, proxyModel):
+ # Check include node attribute name filter
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
+ filteredObject.addNodeAttributeFilter('Sajt')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ # Check attribute value filter
+ filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3)
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '3')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Check exclude node attribute name filter (overrides include node attribute name filter)
+ filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
+ filteredObject.excludeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ # Check if exclude indeed overrides include node attribute name filter
+ filteredObject.includeNodeAttributeNamesFilter = ['Markups.MovingMarkupIndex']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ filteredObject.excludeNodeAttributeNamesFilter = ['Markups.MovingInSliceView']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ filteredObject.excludeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Check include item attribute name filter
+ filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
+ filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'FolderAttribute1']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
+ filteredObject.addItemAttributeFilter('ItemAttribute2')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1', 'ItemAttribute2']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ filteredObject.includeItemAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Check legacy (item) attribute value filter
+ filteredObject.attributeNameFilter = 'ItemAttribute1'
+ filteredObject.attributeValueFilter = '1'
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
+ filteredObject.attributeNameFilter = ''
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Check exclude item attribute name filter (overrides include item attribute filter)
+ filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8)
+ filteredObject.excludeItemAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ # Check if exclude indeed overrides include item attribute filter
+ filteredObject.includeItemAttributeNamesFilter = ['ItemAttribute1']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 2)
+ filteredObject.excludeItemAttributeNamesFilter = ['ItemAttribute1']
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 0)
+ filteredObject.includeItemAttributeNamesFilter = []
+ filteredObject.excludeItemAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ filteredObject.excludeItemAttributeNamesFilter = ['FolderAttribute1']
+ # Note: Shown only 6 because accepted children of rejected parents are not shown
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 8)
+ filteredObject.excludeItemAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Check attribute filtering with class name and attribute value
+ filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', 3, True, 'vtkMRMLMarkupsCurveNode')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 3)
+ filteredObject.addNodeAttributeFilter('ParentAttribute', '', True, 'vtkMRMLMarkupsAngleNode')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 5)
+ filteredObject.addNodeAttributeFilter('ChildAttribute', '', True, 'vtkMRMLMarkupsAngleNode')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 6)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+ # Check with empty attribute value
+ filteredObject.addNodeAttributeFilter('Markups.MovingMarkupIndex', '', True, 'vtkMRMLMarkupsCurveNode')
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 4)
+ filteredObject.includeNodeAttributeNamesFilter = []
+ self.assertEqual(shProxyModel.acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ logging.info('Test attribute filters on proxy model directly')
+ testAttributeFilters(shProxyModel, shProxyModel)
+ logging.info('Test attribute filters on tree view')
+ testAttributeFilters(shTreeView, shProxyModel)
+
+ # ------------------------------------------------------------------------------
+ def section_ComboboxFeatures(self):
+ self.delayDisplay("Combobox features", self.delayMs)
+
+ comboBox = slicer.qMRMLSubjectHierarchyComboBox()
+ comboBox.setMRMLScene(slicer.mrmlScene)
+ comboBox.show()
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Enable None item, number of accepted SH items is the same (None does not have a corresponding accepted SH item)
+ comboBox.noneEnabled = True
+ self.assertEqual(comboBox.sortFilterProxyModel().acceptedItemCount(shNode.GetSceneItemID()), 9)
+
+ # Default text
+ self.assertEqual(comboBox.defaultText, 'Select subject hierarchy item')
+
+ # Select node, include parent names in current item text (when collapsed)
+ markupsCurve1ItemID = shNode.GetItemByName('MarkupsCurve_1')
+ comboBox.setCurrentItem(markupsCurve1ItemID)
+ self.assertEqual(comboBox.defaultText, 'NewFolder_1 / MarkupsCurve_1')
+
+ # Select None item
+ comboBox.setCurrentItem(0)
+ self.assertEqual(comboBox.defaultText, comboBox.noneDisplay)
+
+ # Do not show parent names in current item text
+ comboBox.showCurrentItemParents = False
+ comboBox.setCurrentItem(markupsCurve1ItemID)
+ self.assertEqual(comboBox.defaultText, 'MarkupsCurve_1')
+
+ # Change None item name
+ comboBox.noneDisplay = 'No selection'
+ comboBox.setCurrentItem(0)
+ self.assertEqual(comboBox.defaultText, comboBox.noneDisplay)
+
+ # ------------------------------------------------------------------------------
+ # Utility functions
+
+ # ------------------------------------------------------------------------------
+ # Create sample labelmap with same geometry as input volume
+ def createSampleLabelmapVolumeNode(self, volumeNode, name, label, colorNode=None):
+ self.assertIsNotNone(volumeNode)
+ self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode'))
+ self.assertTrue(label > 0)
+
+ sampleLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
+ sampleLabelmapNode.SetName(name)
+ sampleLabelmapNode = slicer.mrmlScene.AddNode(sampleLabelmapNode)
+ sampleLabelmapNode.Copy(volumeNode)
+ imageData = sampleLabelmapNode.GetImageData()
+ extent = imageData.GetExtent()
+ for x in range(extent[0], extent[1] + 1):
+ for y in range(extent[2], extent[3] + 1):
+ for z in range(extent[4], extent[5] + 1):
+ if ((x >= (extent[1] / 4) and x <= (extent[1] / 4) * 3) and
+ (y >= (extent[3] / 4) and y <= (extent[3] / 4) * 3) and
+ (z >= (extent[5] / 4) and z <= (extent[5] / 4) * 3)):
+ imageData.SetScalarComponentFromDouble(x, y, z, 0, label)
+ else:
+ imageData.SetScalarComponentFromDouble(x, y, z, 0, 0)
+
+ # Display labelmap
+ labelmapVolumeDisplayNode = slicer.vtkMRMLLabelMapVolumeDisplayNode()
+ slicer.mrmlScene.AddNode(labelmapVolumeDisplayNode)
+ if colorNode is None:
+ colorNode = slicer.util.getNode('GenericAnatomyColors')
+ self.assertIsNotNone(colorNode)
+ labelmapVolumeDisplayNode.SetAndObserveColorNodeID(colorNode.GetID())
+ labelmapVolumeDisplayNode.VisibilityOn()
+ sampleLabelmapName = slicer.mrmlScene.GenerateUniqueName(name)
+ sampleLabelmapNode.SetName(sampleLabelmapName)
+ sampleLabelmapNode.SetAndObserveDisplayNodeID(labelmapVolumeDisplayNode.GetID())
+
+ return sampleLabelmapNode
+
+ # ------------------------------------------------------------------------------
+ # Create sphere model at the centre of an input volume
+ def createSampleModelNode(self, name, color, volumeNode=None):
+ if volumeNode:
+ self.assertTrue(volumeNode.IsA('vtkMRMLScalarVolumeNode'))
+ bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
+ volumeNode.GetRASBounds(bounds)
+ x = (bounds[0] + bounds[1]) / 2
+ y = (bounds[2] + bounds[3]) / 2
+ z = (bounds[4] + bounds[5]) / 2
+ radius = min(bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]) / 3.0
+ else:
+ radius = 50
+ x = y = z = 0
+
+ # Taken from: https://mantisarchive.slicer.org/view.php?id=1536
+ sphere = vtk.vtkSphereSource()
+ sphere.SetCenter(x, y, z)
+ sphere.SetRadius(radius)
+
+ modelNode = slicer.vtkMRMLModelNode()
+ modelNode.SetName(name)
+ modelNode = slicer.mrmlScene.AddNode(modelNode)
+ modelNode.SetPolyDataConnection(sphere.GetOutputPort())
+ modelNode.SetHideFromEditors(0)
+
+ displayNode = slicer.vtkMRMLModelDisplayNode()
+ slicer.mrmlScene.AddNode(displayNode)
+ displayNode.Visibility2DOn()
+ displayNode.VisibilityOn()
+ displayNode.SetColor(color[0], color[1], color[2])
+ modelNode.SetAndObserveDisplayNodeID(displayNode.GetID())
+
+ return modelNode
diff --git a/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py b/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py
index fda608b6752..f0b696bd196 100644
--- a/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py
+++ b/Modules/Loadable/SubjectHierarchy/Widgets/Python/AbstractScriptedSubjectHierarchyPlugin.py
@@ -8,31 +8,31 @@
#
class AbstractScriptedSubjectHierarchyPlugin:
- """ Abstract scripted subject hierarchy plugin for python scripted plugins
+ """ Abstract scripted subject hierarchy plugin for python scripted plugins
- USAGE: Instantiate scripted subject hierarchy plugin adaptor class from
- module (e.g. from setup function), and set python source:
+ USAGE: Instantiate scripted subject hierarchy plugin adaptor class from
+ module (e.g. from setup function), and set python source:
- from SubjectHierarchyPlugins import *
- ...
- class [Module]Widget(ScriptedLoadableModuleWidget):
+ from SubjectHierarchyPlugins import *
...
- def setup(self):
- ...
- scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
- scriptedPlugin.setPythonSource(VolumeClipSubjectHierarchyPlugin.filePath)
+ class [Module]Widget(ScriptedLoadableModuleWidget):
...
+ def setup(self):
+ ...
+ scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
+ scriptedPlugin.setPythonSource(VolumeClipSubjectHierarchyPlugin.filePath)
+ ...
- Example can be found here: https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#subject-hierarchy-plugin-offering-view-context-menu-action
- """
+ Example can be found here: https://slicer.readthedocs.io/en/latest/developer_guide/script_repository.html#subject-hierarchy-plugin-offering-view-context-menu-action
+ """
- def __init__(self, scriptedPlugin):
- self.scriptedPlugin = scriptedPlugin
+ def __init__(self, scriptedPlugin):
+ self.scriptedPlugin = scriptedPlugin
- # Register plugin on initialization
- self.register()
+ # Register plugin on initialization
+ self.register()
- def register(self):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.registerPlugin(self.scriptedPlugin)
- logging.debug('Scripted subject hierarchy plugin registered: ' + self.scriptedPlugin.name)
+ def register(self):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.registerPlugin(self.scriptedPlugin)
+ logging.debug('Scripted subject hierarchy plugin registered: ' + self.scriptedPlugin.name)
diff --git a/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py b/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py
index 8f917e6b0a7..e9b1ef41755 100644
--- a/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py
+++ b/Modules/Loadable/SubjectHierarchy/Widgets/Python/__init__.py
@@ -6,11 +6,11 @@
currentDir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(currentDir)
for fileName in os.listdir(currentDir):
- fileNameNoExtension = os.path.splitext(fileName)[0]
- fileExtension = os.path.splitext(fileName)[1]
- if fileExtension == '.py' and fileNameNoExtension != '__init__':
- importStr = 'from ' + fileNameNoExtension + ' import *'
- try:
- exec(importStr)
- except Exception as e:
- logging.error('Failed to import ' + fileNameNoExtension + ': ' + traceback.format_exc())
+ fileNameNoExtension = os.path.splitext(fileName)[0]
+ fileExtension = os.path.splitext(fileName)[1]
+ if fileExtension == '.py' and fileNameNoExtension != '__init__':
+ importStr = 'from ' + fileNameNoExtension + ' import *'
+ try:
+ exec(importStr)
+ except Exception as e:
+ logging.error('Failed to import ' + fileNameNoExtension + ': ' + traceback.format_exc())
diff --git a/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py b/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py
index 0cb71cb0578..76df6772ac4 100644
--- a/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py
+++ b/Modules/Loadable/Tables/Testing/Python/TablesSelfTest.py
@@ -10,14 +10,14 @@
#
class TablesSelfTest(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "TablesSelfTest"
- self.parent.categories = ["Testing.TestCases"]
- self.parent.dependencies = ["Tables"]
- self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
- self.parent.helpText = """This is a self test for Table node and widgets."""
- parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "TablesSelfTest"
+ self.parent.categories = ["Testing.TestCases"]
+ self.parent.dependencies = ["Tables"]
+ self.parent.contributors = ["Andras Lasso (PerkLab, Queen's)"]
+ self.parent.helpText = """This is a self test for Table node and widgets."""
+ parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab, Queen's University and was supported through the Applied Cancer Research Unit program of Cancer Care Ontario with funds provided by the Ontario Ministry of Health and Long-Term Care"""
#
@@ -25,8 +25,8 @@ def __init__(self, parent):
#
class TablesSelfTestWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
#
@@ -34,254 +34,254 @@ def setup(self):
#
class TablesSelfTestLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget
- """
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget
+ """
- def __init__(self):
- pass
+ def __init__(self):
+ pass
class TablesSelfTestTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_TablesSelfTest_FullTest1()
-
- # ------------------------------------------------------------------------------
- def test_TablesSelfTest_FullTest1(self):
- # Check for Tables module
- self.assertTrue(slicer.modules.tables)
-
- self.section_SetupPathsAndNames()
- self.section_CreateTable()
- self.section_TableProperties()
- self.section_TableWidgetButtons()
- self.section_CliTableInputOutput()
- self.delayDisplay("Test passed")
-
- # ------------------------------------------------------------------------------
- def section_SetupPathsAndNames(self):
- # Set constants
- self.sampleTableName = 'SampleTable'
-
- # ------------------------------------------------------------------------------
- def section_CreateTable(self):
- self.delayDisplay("Create table")
-
- # Create sample table node
- tableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(tableNode)
- tableNode.SetName(self.sampleTableName)
- # Add a new column
- column = tableNode.AddColumn()
- self.assertTrue(column is not None)
- column.InsertNextValue("some")
- column.InsertNextValue("data")
- column.InsertNextValue("in this")
- column.InsertNextValue("column")
- tableNode.Modified()
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_TablesSelfTest_FullTest1()
+
+ # ------------------------------------------------------------------------------
+ def test_TablesSelfTest_FullTest1(self):
+ # Check for Tables module
+ self.assertTrue(slicer.modules.tables)
+
+ self.section_SetupPathsAndNames()
+ self.section_CreateTable()
+ self.section_TableProperties()
+ self.section_TableWidgetButtons()
+ self.section_CliTableInputOutput()
+ self.delayDisplay("Test passed")
+
+ # ------------------------------------------------------------------------------
+ def section_SetupPathsAndNames(self):
+ # Set constants
+ self.sampleTableName = 'SampleTable'
+
+ # ------------------------------------------------------------------------------
+ def section_CreateTable(self):
+ self.delayDisplay("Create table")
+
+ # Create sample table node
+ tableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(tableNode)
+ tableNode.SetName(self.sampleTableName)
- # Check table
- table = tableNode.GetTable()
- self.assertTrue(table is not None)
- self.assertTrue(table.GetNumberOfRows() == 4)
- self.assertTrue(table.GetNumberOfColumns() == 1)
+ # Add a new column
+ column = tableNode.AddColumn()
+ self.assertTrue(column is not None)
+ column.InsertNextValue("some")
+ column.InsertNextValue("data")
+ column.InsertNextValue("in this")
+ column.InsertNextValue("column")
+ tableNode.Modified()
- # ------------------------------------------------------------------------------
- def section_TableProperties(self):
- self.delayDisplay("Table properties")
+ # Check table
+ table = tableNode.GetTable()
+ self.assertTrue(table is not None)
+ self.assertTrue(table.GetNumberOfRows() == 4)
+ self.assertTrue(table.GetNumberOfColumns() == 1)
- tableNode = slicer.util.getNode(self.sampleTableName)
+ # ------------------------------------------------------------------------------
+ def section_TableProperties(self):
+ self.delayDisplay("Table properties")
- tableNode.SetColumnLongName("Column 1", "First column")
- tableNode.SetColumnUnitLabel("Column 1", "mm")
- tableNode.SetColumnDescription("Column 1", "This a long description of the first column")
+ tableNode = slicer.util.getNode(self.sampleTableName)
- tableNode.SetColumnUnitLabel("Column 2", "{SUVbw}g/ml")
- tableNode.SetColumnDescription("Column 2", "Second column")
+ tableNode.SetColumnLongName("Column 1", "First column")
+ tableNode.SetColumnUnitLabel("Column 1", "mm")
+ tableNode.SetColumnDescription("Column 1", "This a long description of the first column")
- # ------------------------------------------------------------------------------
- def section_TableWidgetButtons(self):
- self.delayDisplay("Test widget buttons")
+ tableNode.SetColumnUnitLabel("Column 2", "{SUVbw}g/ml")
+ tableNode.SetColumnDescription("Column 2", "Second column")
- slicer.util.selectModule('Tables')
+ # ------------------------------------------------------------------------------
+ def section_TableWidgetButtons(self):
+ self.delayDisplay("Test widget buttons")
- # Make sure subject hierarchy auto-creation is on for this test
- tablesWidget = slicer.modules.tables.widgetRepresentation()
- self.assertTrue(tablesWidget is not None)
+ slicer.util.selectModule('Tables')
- tableNode = slicer.util.getNode(self.sampleTableName)
+ # Make sure subject hierarchy auto-creation is on for this test
+ tablesWidget = slicer.modules.tables.widgetRepresentation()
+ self.assertTrue(tablesWidget is not None)
- tablesWidget.setCurrentTableNode(tableNode)
+ tableNode = slicer.util.getNode(self.sampleTableName)
- lockTableButton = slicer.util.findChildren(widget=tablesWidget, name='LockTableButton')[0]
- copyButton = slicer.util.findChildren(widget=tablesWidget, name='CopyButton')[0]
- pasteButton = slicer.util.findChildren(widget=tablesWidget, name='PasteButton')[0]
- addRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowInsertButton')[0]
- deleteRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowDeleteButton')[0]
- lockFirstRowButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstRowButton')[0]
- addColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnInsertButton')[0]
- deleteColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnDeleteButton')[0]
- lockFirstColumnButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstColumnButton')[0]
- tableView = slicer.util.findChildren(widget=tablesWidget, name='TableView')[0]
+ tablesWidget.setCurrentTableNode(tableNode)
- tableModel = tableView.model()
+ lockTableButton = slicer.util.findChildren(widget=tablesWidget, name='LockTableButton')[0]
+ copyButton = slicer.util.findChildren(widget=tablesWidget, name='CopyButton')[0]
+ pasteButton = slicer.util.findChildren(widget=tablesWidget, name='PasteButton')[0]
+ addRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowInsertButton')[0]
+ deleteRowButton = slicer.util.findChildren(widget=tablesWidget, name='RowDeleteButton')[0]
+ lockFirstRowButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstRowButton')[0]
+ addColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnInsertButton')[0]
+ deleteColumnButton = slicer.util.findChildren(widget=tablesWidget, name='ColumnDeleteButton')[0]
+ lockFirstColumnButton = slicer.util.findChildren(widget=tablesWidget, name='LockFirstColumnButton')[0]
+ tableView = slicer.util.findChildren(widget=tablesWidget, name='TableView')[0]
- initialNumberOfColumns = tableNode.GetNumberOfColumns()
- initialNumberOfRows = tableNode.GetNumberOfRows()
+ tableModel = tableView.model()
- #############
- self.delayDisplay("Test add rows/columns")
+ initialNumberOfColumns = tableNode.GetNumberOfColumns()
+ initialNumberOfRows = tableNode.GetNumberOfRows()
- addRowButton.click()
- self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows + 1)
+ #############
+ self.delayDisplay("Test add rows/columns")
- addColumnButton.click()
- self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns + 1)
+ addRowButton.click()
+ self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows + 1)
+
+ addColumnButton.click()
+ self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns + 1)
- #############
- self.delayDisplay("Test lock first row/column")
+ #############
+ self.delayDisplay("Test lock first row/column")
- self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
- lockFirstRowButton.click()
- self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'some')
- lockFirstColumnButton.click()
- self.assertTrue(tableModel.data(tableModel.index(0, 0)) == '')
- lockFirstRowButton.click()
- lockFirstColumnButton.click()
-
- #############
- self.delayDisplay("Test delete row/column")
-
- tableView.selectionModel().select(tableModel.index(1, 1), qt.QItemSelectionModel.Select) # Select second item in second column
- deleteColumnButton.click()
- self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
-
- tableView.selectionModel().select(tableModel.index(4, 0), qt.QItemSelectionModel.Select) # Select 5th item in first column
- deleteRowButton.click()
- self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
-
- #############
- self.delayDisplay("Test if buttons are disabled")
-
- lockTableButton.click()
-
- addRowButton.click()
- self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
-
- addColumnButton.click()
- self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
-
- tableView.selectionModel().select(tableView.model().index(0, 0), qt.QItemSelectionModel.Select)
-
- deleteColumnButton.click()
- self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
-
- deleteRowButton.click()
- self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
-
- lockFirstRowButton.click()
- self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
-
- lockFirstColumnButton.click()
- self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
-
- lockTableButton.click()
-
- #############
- self.delayDisplay("Test copy/paste")
-
- tableView.selectColumn(0)
- copyButton.click()
- tableView.clearSelection()
-
- # Paste first column into a newly created second column
- addColumnButton.click()
-
- tableView.setCurrentIndex(tableModel.index(0, 1))
- pasteButton.click()
-
- # Check if first and second column content is the same
- for rowIndex in range(5):
- self.assertEqual(tableModel.data(tableModel.index(rowIndex, 0)), tableModel.data(tableModel.index(rowIndex, 1)))
-
- # ------------------------------------------------------------------------------
- def section_CliTableInputOutput(self):
- self.delayDisplay("Test table writing and reading by CLI module")
-
- # Create input and output nodes
-
- inputTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(inputTableNode)
- inputTableNode.AddColumn()
- inputTableNode.AddColumn()
- inputTableNode.AddColumn()
- inputTableNode.AddEmptyRow()
- inputTableNode.AddEmptyRow()
- inputTableNode.AddEmptyRow()
- for row in range(3):
- for col in range(3):
- inputTableNode.SetCellText(row, col, str((row + 1) * (col + 1)))
- inputTableNode.SetCellText(0, 0, "source")
-
- outputTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(outputTableNode)
-
- # Run CLI module
-
- self.delayDisplay("Run CLI module")
- parameters = {}
- parameters["arg0"] = self.createDummyVolume().GetID()
- parameters["arg1"] = self.createDummyVolume().GetID()
- parameters["transform1"] = self.createDummyTransform().GetID()
- parameters["transform2"] = self.createDummyTransform().GetID()
- parameters["inputDT"] = inputTableNode.GetID()
- parameters["outputDT"] = outputTableNode.GetID()
- slicer.cli.run(slicer.modules.executionmodeltour, None, parameters, wait_for_completion=True)
-
- # Verify the output table content
-
- self.delayDisplay("Verify results")
- # the ExecutionModelTour module copies the input table to the output exxcept the first two rows
- # of the first column, which is set to "Computed first" and "Computed second" strings
- for row in range(3):
- for col in range(3):
- if row == 0 and col == 0:
- self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed first")
- elif row == 1 and col == 0:
- self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed second")
- else:
- self.assertTrue(outputTableNode.GetCellText(row, col) == inputTableNode.GetCellText(row, col))
-
- def createDummyTransform(self):
- transformNode = slicer.vtkMRMLLinearTransformNode()
- slicer.mrmlScene.AddNode(transformNode)
- return transformNode
-
- def createDummyVolume(self):
- imageData = vtk.vtkImageData()
- imageData.SetDimensions(10, 10, 10)
- imageData.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- volumeNode = slicer.vtkMRMLScalarVolumeNode()
- volumeNode.SetAndObserveImageData(imageData)
- displayNode = slicer.vtkMRMLScalarVolumeDisplayNode()
- slicer.mrmlScene.AddNode(volumeNode)
- slicer.mrmlScene.AddNode(displayNode)
- volumeNode.SetAndObserveDisplayNodeID(displayNode.GetID())
- displayNode.SetAndObserveColorNodeID('vtkMRMLColorTableNodeGrey')
- return volumeNode
+ self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
+ lockFirstRowButton.click()
+ self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'some')
+ lockFirstColumnButton.click()
+ self.assertTrue(tableModel.data(tableModel.index(0, 0)) == '')
+ lockFirstRowButton.click()
+ lockFirstColumnButton.click()
+
+ #############
+ self.delayDisplay("Test delete row/column")
+
+ tableView.selectionModel().select(tableModel.index(1, 1), qt.QItemSelectionModel.Select) # Select second item in second column
+ deleteColumnButton.click()
+ self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
+
+ tableView.selectionModel().select(tableModel.index(4, 0), qt.QItemSelectionModel.Select) # Select 5th item in first column
+ deleteRowButton.click()
+ self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
+
+ #############
+ self.delayDisplay("Test if buttons are disabled")
+
+ lockTableButton.click()
+
+ addRowButton.click()
+ self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
+
+ addColumnButton.click()
+ self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
+
+ tableView.selectionModel().select(tableView.model().index(0, 0), qt.QItemSelectionModel.Select)
+
+ deleteColumnButton.click()
+ self.assertTrue(tableNode.GetNumberOfColumns() == initialNumberOfColumns)
+
+ deleteRowButton.click()
+ self.assertTrue(tableNode.GetNumberOfRows() == initialNumberOfRows)
+
+ lockFirstRowButton.click()
+ self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
+
+ lockFirstColumnButton.click()
+ self.assertTrue(tableModel.data(tableModel.index(0, 0)) == 'Column 1')
+
+ lockTableButton.click()
+
+ #############
+ self.delayDisplay("Test copy/paste")
+
+ tableView.selectColumn(0)
+ copyButton.click()
+ tableView.clearSelection()
+
+ # Paste first column into a newly created second column
+ addColumnButton.click()
+
+ tableView.setCurrentIndex(tableModel.index(0, 1))
+ pasteButton.click()
+
+ # Check if first and second column content is the same
+ for rowIndex in range(5):
+ self.assertEqual(tableModel.data(tableModel.index(rowIndex, 0)), tableModel.data(tableModel.index(rowIndex, 1)))
+
+ # ------------------------------------------------------------------------------
+ def section_CliTableInputOutput(self):
+ self.delayDisplay("Test table writing and reading by CLI module")
+
+ # Create input and output nodes
+
+ inputTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(inputTableNode)
+ inputTableNode.AddColumn()
+ inputTableNode.AddColumn()
+ inputTableNode.AddColumn()
+ inputTableNode.AddEmptyRow()
+ inputTableNode.AddEmptyRow()
+ inputTableNode.AddEmptyRow()
+ for row in range(3):
+ for col in range(3):
+ inputTableNode.SetCellText(row, col, str((row + 1) * (col + 1)))
+ inputTableNode.SetCellText(0, 0, "source")
+
+ outputTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(outputTableNode)
+
+ # Run CLI module
+
+ self.delayDisplay("Run CLI module")
+ parameters = {}
+ parameters["arg0"] = self.createDummyVolume().GetID()
+ parameters["arg1"] = self.createDummyVolume().GetID()
+ parameters["transform1"] = self.createDummyTransform().GetID()
+ parameters["transform2"] = self.createDummyTransform().GetID()
+ parameters["inputDT"] = inputTableNode.GetID()
+ parameters["outputDT"] = outputTableNode.GetID()
+ slicer.cli.run(slicer.modules.executionmodeltour, None, parameters, wait_for_completion=True)
+
+ # Verify the output table content
+
+ self.delayDisplay("Verify results")
+ # the ExecutionModelTour module copies the input table to the output exxcept the first two rows
+ # of the first column, which is set to "Computed first" and "Computed second" strings
+ for row in range(3):
+ for col in range(3):
+ if row == 0 and col == 0:
+ self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed first")
+ elif row == 1 and col == 0:
+ self.assertTrue(outputTableNode.GetCellText(row, col) == "Computed second")
+ else:
+ self.assertTrue(outputTableNode.GetCellText(row, col) == inputTableNode.GetCellText(row, col))
+
+ def createDummyTransform(self):
+ transformNode = slicer.vtkMRMLLinearTransformNode()
+ slicer.mrmlScene.AddNode(transformNode)
+ return transformNode
+
+ def createDummyVolume(self):
+ imageData = vtk.vtkImageData()
+ imageData.SetDimensions(10, 10, 10)
+ imageData.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ volumeNode = slicer.vtkMRMLScalarVolumeNode()
+ volumeNode.SetAndObserveImageData(imageData)
+ displayNode = slicer.vtkMRMLScalarVolumeDisplayNode()
+ slicer.mrmlScene.AddNode(volumeNode)
+ slicer.mrmlScene.AddNode(displayNode)
+ volumeNode.SetAndObserveDisplayNodeID(displayNode.GetID())
+ displayNode.SetAndObserveColorNodeID('vtkMRMLColorTableNodeGrey')
+ return volumeNode
diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py
index c8673fbfc36..ee6008b3194 100644
--- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py
+++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRenderingSceneClose.py
@@ -10,21 +10,21 @@
#
class VolumeRenderingSceneClose(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "VolumeRenderingSceneClose"
- parent.categories = ["Testing.TestCases"]
- parent.dependencies = []
- parent.contributors = ["Nicole Aucoin (BWH)"]
- parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "VolumeRenderingSceneClose"
+ parent.categories = ["Testing.TestCases"]
+ parent.dependencies = []
+ parent.contributors = ["Nicole Aucoin (BWH)"]
+ parent.helpText = """
This is a scripted self test to check that scene close
works while in the volume rendering module.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was contributed by Nicole Aucoin, BWH, and was partially funded by NIH grant 3P41RR013218-12S1.
"""
@@ -34,45 +34,45 @@ def __init__(self, parent):
#
class VolumeRenderingSceneCloseWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+ # Instantiate and connect widgets ...
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
- #
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = True
- parametersFormLayout.addRow(self.applyButton)
+ #
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = True
+ parametersFormLayout.addRow(self.applyButton)
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
- # Add vertical spacer
- self.layout.addStretch(1)
+ # Add vertical spacer
+ self.layout.addStretch(1)
- def cleanup(self):
- pass
+ def cleanup(self):
+ pass
- def onApplyButton(self):
- logic = VolumeRenderingSceneCloseLogic()
- print("Run the algorithm")
- logic.run()
+ def onApplyButton(self):
+ logic = VolumeRenderingSceneCloseLogic()
+ print("Run the algorithm")
+ logic.run()
#
@@ -80,74 +80,74 @@ def onApplyButton(self):
#
class VolumeRenderingSceneCloseLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def run(self):
- """
- Run the actual algorithm
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- layoutManager = slicer.app.layoutManager()
- layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
+ def run(self):
+ """
+ Run the actual algorithm
+ """
- slicer.util.delayDisplay('Running the aglorithm')
+ layoutManager = slicer.app.layoutManager()
+ layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutConventionalView)
- import SampleData
- ctVolume = SampleData.downloadSample('CTChest')
- slicer.util.delayDisplay('Downloaded CT sample data')
+ slicer.util.delayDisplay('Running the aglorithm')
- # go to the volume rendering module
- slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering')
- slicer.util.delayDisplay('Volume Rendering module')
+ import SampleData
+ ctVolume = SampleData.downloadSample('CTChest')
+ slicer.util.delayDisplay('Downloaded CT sample data')
- # turn it on
- volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation()
- volumeRenderingWidgetRep.setMRMLVolumeNode(ctVolume)
- volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering')
- volumeRenderingNode.SetVisibility(1)
- slicer.util.delayDisplay('Volume Rendering')
+ # go to the volume rendering module
+ slicer.util.mainWindow().moduleSelector().selectModule('VolumeRendering')
+ slicer.util.delayDisplay('Volume Rendering module')
- # set up a cropping ROI
- volumeRenderingNode.SetCroppingEnabled(1)
- markupsROI = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLMarkupsROINode')
- slicer.util.delayDisplay('Cropping')
+ # turn it on
+ volumeRenderingWidgetRep = slicer.modules.volumerendering.widgetRepresentation()
+ volumeRenderingWidgetRep.setMRMLVolumeNode(ctVolume)
+ volumeRenderingNode = slicer.mrmlScene.GetFirstNodeByName('VolumeRendering')
+ volumeRenderingNode.SetVisibility(1)
+ slicer.util.delayDisplay('Volume Rendering')
- # close the scene
- slicer.mrmlScene.Clear(0)
+ # set up a cropping ROI
+ volumeRenderingNode.SetCroppingEnabled(1)
+ markupsROI = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLMarkupsROINode')
+ slicer.util.delayDisplay('Cropping')
- return True
+ # close the scene
+ slicer.mrmlScene.Clear(0)
+
+ return True
class VolumeRenderingSceneCloseTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.setUp()
- self.test_VolumeRenderingSceneClose1()
- def test_VolumeRenderingSceneClose1(self):
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_VolumeRenderingSceneClose1()
+
+ def test_VolumeRenderingSceneClose1(self):
- self.delayDisplay("Starting the test")
+ self.delayDisplay("Starting the test")
- logic = VolumeRenderingSceneCloseLogic()
- logic.run()
+ logic = VolumeRenderingSceneCloseLogic()
+ logic.run()
- self.delayDisplay('Test passed!')
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py
index faa9446bd6c..49968a9f769 100644
--- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py
+++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomenTutorial.py
@@ -5,4 +5,4 @@
testUtility = slicer.app.testingUtility()
success = testUtility.playTests(filepath)
if not success:
- raise Exception('Failed to finished properly the play back !')
+ raise Exception('Failed to finished properly the play back !')
diff --git a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py
index 9890be05902..0141aedd046 100644
--- a/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py
+++ b/Modules/Loadable/VolumeRendering/Testing/Python/VolumeRendering_CTAbdomen_AppleTutorial.py
@@ -5,4 +5,4 @@
testUtility = slicer.app.testingUtility()
success = testUtility.playTests(filepath)
if not success:
- raise Exception('Failed to finished properly the play back !')
+ raise Exception('Failed to finished properly the play back !')
diff --git a/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py b/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py
index 6f9beb06d1a..e9e5ec91c0f 100644
--- a/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py
+++ b/Modules/Loadable/Volumes/Testing/Python/LoadVolumeDisplaybleSceneModelClose.py
@@ -4,31 +4,31 @@
class VolumesLoadSceneCloseTesting(ScriptedLoadableModuleTest):
- def setUp(self):
- pass
-
- def test_LoadVolumeCloseScene(self):
- """
- Load a volume, go to a module that has a displayable scene model set for the tree view, then close the scene.
- Tests the case of closing a scene with a displayable node in it while a GUI is up that is showing a tree view with a displayable scene model (display nodes are set to null during scene closing and can trigger events).
- """
- self.delayDisplay("Starting the test")
-
- #
- # first, get some sample data
- #
- import SampleData
- SampleData.downloadSample("MRHead")
-
- #
- # enter the models module
- #
- mainWindow = slicer.util.mainWindow()
- mainWindow.moduleSelector().selectModule('Models')
-
- #
- # close the scene
- #
- slicer.mrmlScene.Clear(0)
-
- self.delayDisplay('Test passed')
+ def setUp(self):
+ pass
+
+ def test_LoadVolumeCloseScene(self):
+ """
+ Load a volume, go to a module that has a displayable scene model set for the tree view, then close the scene.
+ Tests the case of closing a scene with a displayable node in it while a GUI is up that is showing a tree view with a displayable scene model (display nodes are set to null during scene closing and can trigger events).
+ """
+ self.delayDisplay("Starting the test")
+
+ #
+ # first, get some sample data
+ #
+ import SampleData
+ SampleData.downloadSample("MRHead")
+
+ #
+ # enter the models module
+ #
+ mainWindow = slicer.util.mainWindow()
+ mainWindow.moduleSelector().selectModule('Models')
+
+ #
+ # close the scene
+ #
+ slicer.mrmlScene.Clear(0)
+
+ self.delayDisplay('Test passed')
diff --git a/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py b/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py
index 2ba77240819..70a1d995bee 100644
--- a/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py
+++ b/Modules/Loadable/Volumes/Testing/Python/VolumesLogicCompareVolumeGeometry.py
@@ -6,118 +6,118 @@
class VolumesLogicCompareVolumeGeometryTesting(ScriptedLoadableModuleTest):
- def setUp(self):
- pass
-
- def test_VolumesLogicCompareVolumeGeometry(self):
- """
- Load a volume, then call the compare volume geometry test with
- different values of epsilon and precision.
- """
- self.delayDisplay("Starting the test")
-
- #
- # first, get some sample data
- #
- import SampleData
- head = SampleData.downloadSample("MRHead")
-
- #
- # get the volumes logic and print out default epsilon and precision
- #
- volumesLogic = slicer.modules.volumes.logic()
- print('Compare volume geometry epsilon: ', volumesLogic.GetCompareVolumeGeometryEpsilon())
- print('Compare volume geometry precision: ', volumesLogic.GetCompareVolumeGeometryPrecision())
- self.assertAlmostEqual(volumesLogic.GetCompareVolumeGeometryEpsilon(), 1e-6)
- self.assertEqual(volumesLogic.GetCompareVolumeGeometryPrecision(), 6)
-
- #
- # compare the head against itself, this shouldn't produce any warning
- # string
- #
- warningString = volumesLogic.CompareVolumeGeometry(head, head)
- if len(warningString) != 0:
- print('Error in checking MRHead geometry against itself')
- print(warningString)
- return False
- else:
- print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
-
- #
- # see if you can get it to fail with a tighter epsilon
- #
- volumesLogic.SetCompareVolumeGeometryEpsilon(1e-10)
- precision = volumesLogic.GetCompareVolumeGeometryPrecision()
- if precision != 10:
- print('Error in calculating precision from epsilon of ', volumesLogic.GetCompareVolumeGeometryEpsilon(), ', expected 10, got ', precision)
- return False
- warningString = volumesLogic.CompareVolumeGeometry(head, head)
- if len(warningString) != 0:
- print('Error in checking MRHead geometry against itself with strict epsilon')
- print(warningString)
- return False
- else:
- print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
-
- #
- # clone the volume so can test for mismatches in geometry with
- # that operation
- #
- head2 = volumesLogic.CloneVolume(head, 'head2')
-
- warningString = volumesLogic.CompareVolumeGeometry(head, head2)
- if len(warningString) != 0:
- print('Error in checking MRHead geometry against itself with epsilon ', volumesLogic.GetCompareVolumeGeometryEpsilon())
- print(warningString)
- return False
- else:
- print('Success in comparing MRHead vs clone with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
-
- #
- # now try with a label map volume
- #
- headLabel = volumesLogic.CreateAndAddLabelVolume(head, "label vol")
- warningString = volumesLogic.CompareVolumeGeometry(head, headLabel)
- if len(warningString) != 0:
- print('Error in comparing MRHead geometry against a label map of itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
- print(warningString)
- return False
- else:
- print('Success in comparing MRHead vs label map with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
-
- #
- # adjust the geometry and make it fail
- #
- head2Matrix = vtk.vtkMatrix4x4()
- head2.GetRASToIJKMatrix(head2Matrix)
- val = head2Matrix.GetElement(2, 0)
- head2Matrix.SetElement(2, 0, val + 0.25)
- head2.SetRASToIJKMatrix(head2Matrix)
- head2.SetSpacing(0.12345678901234567890, 2.0, 3.4)
-
- warningString = volumesLogic.CompareVolumeGeometry(head, head2)
- if len(warningString) == 0:
- print('Error in comparing MRHead geometry against an updated clone, with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
- return False
- else:
- print('Success in making the comparison fail, with with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
- print(warningString)
-
- #
- # reset the epsilon with an invalid negative number
- #
- volumesLogic.SetCompareVolumeGeometryEpsilon(-0.01)
- epsilon = volumesLogic.GetCompareVolumeGeometryEpsilon()
- if epsilon != 0.01:
- print('Failed to use the absolute value for an epsilon of -0.01: ', epsilon)
- return False
- precision = volumesLogic.GetCompareVolumeGeometryPrecision()
- if precision != 2:
- print('Failed to set the precision to 2: ', precision)
- return False
- warningString = volumesLogic.CompareVolumeGeometry(head, head2)
- print(warningString)
-
- self.delayDisplay('Test passed')
-
- return True
+ def setUp(self):
+ pass
+
+ def test_VolumesLogicCompareVolumeGeometry(self):
+ """
+ Load a volume, then call the compare volume geometry test with
+ different values of epsilon and precision.
+ """
+ self.delayDisplay("Starting the test")
+
+ #
+ # first, get some sample data
+ #
+ import SampleData
+ head = SampleData.downloadSample("MRHead")
+
+ #
+ # get the volumes logic and print out default epsilon and precision
+ #
+ volumesLogic = slicer.modules.volumes.logic()
+ print('Compare volume geometry epsilon: ', volumesLogic.GetCompareVolumeGeometryEpsilon())
+ print('Compare volume geometry precision: ', volumesLogic.GetCompareVolumeGeometryPrecision())
+ self.assertAlmostEqual(volumesLogic.GetCompareVolumeGeometryEpsilon(), 1e-6)
+ self.assertEqual(volumesLogic.GetCompareVolumeGeometryPrecision(), 6)
+
+ #
+ # compare the head against itself, this shouldn't produce any warning
+ # string
+ #
+ warningString = volumesLogic.CompareVolumeGeometry(head, head)
+ if len(warningString) != 0:
+ print('Error in checking MRHead geometry against itself')
+ print(warningString)
+ return False
+ else:
+ print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+
+ #
+ # see if you can get it to fail with a tighter epsilon
+ #
+ volumesLogic.SetCompareVolumeGeometryEpsilon(1e-10)
+ precision = volumesLogic.GetCompareVolumeGeometryPrecision()
+ if precision != 10:
+ print('Error in calculating precision from epsilon of ', volumesLogic.GetCompareVolumeGeometryEpsilon(), ', expected 10, got ', precision)
+ return False
+ warningString = volumesLogic.CompareVolumeGeometry(head, head)
+ if len(warningString) != 0:
+ print('Error in checking MRHead geometry against itself with strict epsilon')
+ print(warningString)
+ return False
+ else:
+ print('Success in comparing MRHead vs itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+
+ #
+ # clone the volume so can test for mismatches in geometry with
+ # that operation
+ #
+ head2 = volumesLogic.CloneVolume(head, 'head2')
+
+ warningString = volumesLogic.CompareVolumeGeometry(head, head2)
+ if len(warningString) != 0:
+ print('Error in checking MRHead geometry against itself with epsilon ', volumesLogic.GetCompareVolumeGeometryEpsilon())
+ print(warningString)
+ return False
+ else:
+ print('Success in comparing MRHead vs clone with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+
+ #
+ # now try with a label map volume
+ #
+ headLabel = volumesLogic.CreateAndAddLabelVolume(head, "label vol")
+ warningString = volumesLogic.CompareVolumeGeometry(head, headLabel)
+ if len(warningString) != 0:
+ print('Error in comparing MRHead geometry against a label map of itself with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+ print(warningString)
+ return False
+ else:
+ print('Success in comparing MRHead vs label map with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+
+ #
+ # adjust the geometry and make it fail
+ #
+ head2Matrix = vtk.vtkMatrix4x4()
+ head2.GetRASToIJKMatrix(head2Matrix)
+ val = head2Matrix.GetElement(2, 0)
+ head2Matrix.SetElement(2, 0, val + 0.25)
+ head2.SetRASToIJKMatrix(head2Matrix)
+ head2.SetSpacing(0.12345678901234567890, 2.0, 3.4)
+
+ warningString = volumesLogic.CompareVolumeGeometry(head, head2)
+ if len(warningString) == 0:
+ print('Error in comparing MRHead geometry against an updated clone, with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+ return False
+ else:
+ print('Success in making the comparison fail, with with epsilon', volumesLogic.GetCompareVolumeGeometryEpsilon())
+ print(warningString)
+
+ #
+ # reset the epsilon with an invalid negative number
+ #
+ volumesLogic.SetCompareVolumeGeometryEpsilon(-0.01)
+ epsilon = volumesLogic.GetCompareVolumeGeometryEpsilon()
+ if epsilon != 0.01:
+ print('Failed to use the absolute value for an epsilon of -0.01: ', epsilon)
+ return False
+ precision = volumesLogic.GetCompareVolumeGeometryPrecision()
+ if precision != 2:
+ print('Failed to set the precision to 2: ', precision)
+ return False
+ warningString = volumesLogic.CompareVolumeGeometry(head, head2)
+ print(warningString)
+
+ self.delayDisplay('Test passed')
+
+ return True
diff --git a/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py b/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py
index 8e0e4e50700..6482567396e 100644
--- a/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py
+++ b/Modules/Scripted/CropVolumeSequence/CropVolumeSequence.py
@@ -13,19 +13,19 @@
#
class CropVolumeSequence(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Crop volume sequence"
- self.parent.categories = ["Sequences"]
- self.parent.dependencies = []
- self.parent.contributors = ["Andras Lasso (PerkLab, Queen's University)"]
- self.parent.helpText = """This module can crop and resample a volume sequence to reduce its size for faster rendering and processing."""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Crop volume sequence"
+ self.parent.categories = ["Sequences"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Andras Lasso (PerkLab, Queen's University)"]
+ self.parent.helpText = """This module can crop and resample a volume sequence to reduce its size for faster rendering and processing."""
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This file was originally developed by Andras Lasso
"""
@@ -35,121 +35,121 @@ def __init__(self, parent):
#
class CropVolumeSequenceWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Instantiate and connect widgets ...
-
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
-
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- #
- # input volume selector
- #
- self.inputSelector = slicer.qMRMLNodeComboBox()
- self.inputSelector.nodeTypes = ["vtkMRMLSequenceNode"]
- self.inputSelector.addEnabled = False
- self.inputSelector.removeEnabled = False
- self.inputSelector.noneEnabled = False
- self.inputSelector.showHidden = False
- self.inputSelector.showChildNodeTypes = False
- self.inputSelector.setMRMLScene(slicer.mrmlScene)
- self.inputSelector.setToolTip("Pick a sequence node of volumes that will be cropped and resampled.")
- parametersFormLayout.addRow("Input volume sequence: ", self.inputSelector)
-
- #
- # output volume selector
- #
- self.outputSelector = slicer.qMRMLNodeComboBox()
- self.outputSelector.nodeTypes = ["vtkMRMLSequenceNode"]
- self.outputSelector.selectNodeUponCreation = True
- self.outputSelector.addEnabled = True
- self.outputSelector.removeEnabled = True
- self.outputSelector.noneEnabled = True
- self.outputSelector.noneDisplay = "(Overwrite input)"
- self.outputSelector.showHidden = False
- self.outputSelector.showChildNodeTypes = False
- self.outputSelector.setMRMLScene(slicer.mrmlScene)
- self.outputSelector.setToolTip("Pick a sequence node where the cropped and resampled volumes will be stored.")
- parametersFormLayout.addRow("Output volume sequence: ", self.outputSelector)
-
- #
- # Crop parameters selector
- #
- self.cropParametersSelector = slicer.qMRMLNodeComboBox()
- self.cropParametersSelector.nodeTypes = ["vtkMRMLCropVolumeParametersNode"]
- self.cropParametersSelector.selectNodeUponCreation = True
- self.cropParametersSelector.addEnabled = True
- self.cropParametersSelector.removeEnabled = True
- self.cropParametersSelector.renameEnabled = True
- self.cropParametersSelector.noneEnabled = False
- self.cropParametersSelector.showHidden = True
- self.cropParametersSelector.showChildNodeTypes = False
- self.cropParametersSelector.setMRMLScene(slicer.mrmlScene)
- self.cropParametersSelector.setToolTip("Select a crop volumes parameters.")
-
- self.editCropParametersButton = qt.QPushButton()
- self.editCropParametersButton.setIcon(qt.QIcon(':Icons/Go.png'))
- # self.editCropParametersButton.setMaximumWidth(60)
- self.editCropParametersButton.enabled = True
- self.editCropParametersButton.toolTip = "Go to Crop Volume module to edit cropping parameters."
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.cropParametersSelector)
- hbox.addWidget(self.editCropParametersButton)
- parametersFormLayout.addRow("Crop volume settings: ", hbox)
-
- #
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run the algorithm."
- self.applyButton.enabled = False
- parametersFormLayout.addRow(self.applyButton)
-
- # connections
- self.applyButton.connect('clicked(bool)', self.onApplyButton)
- self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
- self.cropParametersSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
- self.editCropParametersButton.connect("clicked()", self.onEditCropParameters)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- # Refresh Apply button state
- self.onSelect()
-
- def cleanup(self):
- pass
-
- def onSelect(self):
- self.applyButton.enabled = (self.inputSelector.currentNode() and self.cropParametersSelector.currentNode())
-
- def onEditCropParameters(self):
- if not self.cropParametersSelector.currentNode():
- cropParametersNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode")
- self.cropParametersSelector.setCurrentNode(cropParametersNode)
- if self.inputSelector.currentNode():
- inputVolSeq = self.inputSelector.currentNode()
- seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
- inputVolume = seqBrowser.GetProxyNode(inputVolSeq)
- if inputVolume:
- self.cropParametersSelector.currentNode().SetInputVolumeNodeID(inputVolume.GetID())
- slicer.app.openNodeModule(self.cropParametersSelector.currentNode())
-
- def onApplyButton(self):
- logic = CropVolumeSequenceLogic()
- logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), self.cropParametersSelector.currentNode())
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Instantiate and connect widgets ...
+
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
+
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ #
+ # input volume selector
+ #
+ self.inputSelector = slicer.qMRMLNodeComboBox()
+ self.inputSelector.nodeTypes = ["vtkMRMLSequenceNode"]
+ self.inputSelector.addEnabled = False
+ self.inputSelector.removeEnabled = False
+ self.inputSelector.noneEnabled = False
+ self.inputSelector.showHidden = False
+ self.inputSelector.showChildNodeTypes = False
+ self.inputSelector.setMRMLScene(slicer.mrmlScene)
+ self.inputSelector.setToolTip("Pick a sequence node of volumes that will be cropped and resampled.")
+ parametersFormLayout.addRow("Input volume sequence: ", self.inputSelector)
+
+ #
+ # output volume selector
+ #
+ self.outputSelector = slicer.qMRMLNodeComboBox()
+ self.outputSelector.nodeTypes = ["vtkMRMLSequenceNode"]
+ self.outputSelector.selectNodeUponCreation = True
+ self.outputSelector.addEnabled = True
+ self.outputSelector.removeEnabled = True
+ self.outputSelector.noneEnabled = True
+ self.outputSelector.noneDisplay = "(Overwrite input)"
+ self.outputSelector.showHidden = False
+ self.outputSelector.showChildNodeTypes = False
+ self.outputSelector.setMRMLScene(slicer.mrmlScene)
+ self.outputSelector.setToolTip("Pick a sequence node where the cropped and resampled volumes will be stored.")
+ parametersFormLayout.addRow("Output volume sequence: ", self.outputSelector)
+
+ #
+ # Crop parameters selector
+ #
+ self.cropParametersSelector = slicer.qMRMLNodeComboBox()
+ self.cropParametersSelector.nodeTypes = ["vtkMRMLCropVolumeParametersNode"]
+ self.cropParametersSelector.selectNodeUponCreation = True
+ self.cropParametersSelector.addEnabled = True
+ self.cropParametersSelector.removeEnabled = True
+ self.cropParametersSelector.renameEnabled = True
+ self.cropParametersSelector.noneEnabled = False
+ self.cropParametersSelector.showHidden = True
+ self.cropParametersSelector.showChildNodeTypes = False
+ self.cropParametersSelector.setMRMLScene(slicer.mrmlScene)
+ self.cropParametersSelector.setToolTip("Select a crop volumes parameters.")
+
+ self.editCropParametersButton = qt.QPushButton()
+ self.editCropParametersButton.setIcon(qt.QIcon(':Icons/Go.png'))
+ # self.editCropParametersButton.setMaximumWidth(60)
+ self.editCropParametersButton.enabled = True
+ self.editCropParametersButton.toolTip = "Go to Crop Volume module to edit cropping parameters."
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.cropParametersSelector)
+ hbox.addWidget(self.editCropParametersButton)
+ parametersFormLayout.addRow("Crop volume settings: ", hbox)
+
+ #
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run the algorithm."
+ self.applyButton.enabled = False
+ parametersFormLayout.addRow(self.applyButton)
+
+ # connections
+ self.applyButton.connect('clicked(bool)', self.onApplyButton)
+ self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
+ self.cropParametersSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)
+ self.editCropParametersButton.connect("clicked()", self.onEditCropParameters)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ # Refresh Apply button state
+ self.onSelect()
+
+ def cleanup(self):
+ pass
+
+ def onSelect(self):
+ self.applyButton.enabled = (self.inputSelector.currentNode() and self.cropParametersSelector.currentNode())
+
+ def onEditCropParameters(self):
+ if not self.cropParametersSelector.currentNode():
+ cropParametersNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLCropVolumeParametersNode")
+ self.cropParametersSelector.setCurrentNode(cropParametersNode)
+ if self.inputSelector.currentNode():
+ inputVolSeq = self.inputSelector.currentNode()
+ seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
+ inputVolume = seqBrowser.GetProxyNode(inputVolSeq)
+ if inputVolume:
+ self.cropParametersSelector.currentNode().SetInputVolumeNodeID(inputVolume.GetID())
+ slicer.app.openNodeModule(self.cropParametersSelector.currentNode())
+
+ def onApplyButton(self):
+ logic = CropVolumeSequenceLogic()
+ logic.run(self.inputSelector.currentNode(), self.outputSelector.currentNode(), self.cropParametersSelector.currentNode())
#
@@ -157,188 +157,188 @@ def onApplyButton(self):
#
class CropVolumeSequenceLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def transformForSequence(self, volumeSeq):
- seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(volumeSeq)
- if not seqBrowser:
- return None
- proxyVolume = seqBrowser.GetProxyNode(volumeSeq)
- if not proxyVolume:
- return None
- return proxyVolume.GetTransformNodeID()
-
- def run(self, inputVolSeq, outputVolSeq, cropParameters):
- """
- Run the actual algorithm
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- logging.info('Processing started')
-
- # Get original parent transform, if any (before creating the new sequence browser)
- inputVolTransformNodeID = self.transformForSequence(inputVolSeq)
- outputVolTransformNodeID = None
-
- seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode")
- seqBrowser.SetAndObserveMasterSequenceNodeID(inputVolSeq.GetID())
- seqBrowser.SetSaveChanges(inputVolSeq, True) # allow modifying node in the sequence
-
- seqBrowser.SetSelectedItemNumber(0)
- slicer.modules.sequences.logic().UpdateAllProxyNodes()
- slicer.app.processEvents()
- inputVolume = seqBrowser.GetProxyNode(inputVolSeq)
- inputVolume.SetAndObserveTransformNodeID(inputVolTransformNodeID)
- cropParameters.SetInputVolumeNodeID(inputVolume.GetID())
-
- if outputVolSeq == inputVolSeq:
- outputVolSeq = None
-
- if outputVolSeq:
- # Get original parent transform, if any (before erasing all the proxy nodes)
- outputVolTransformNodeID = self.transformForSequence(outputVolSeq)
-
- # Initialize output sequence
- outputVolSeq.RemoveAllDataNodes()
- outputVolSeq.SetIndexType(inputVolSeq.GetIndexType())
- outputVolSeq.SetIndexName(inputVolSeq.GetIndexName())
- outputVolSeq.SetIndexUnit(inputVolSeq.GetIndexUnit())
- outputVolume = slicer.mrmlScene.AddNewNodeByClass(inputVolume.GetClassName())
- outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID)
- cropParameters.SetOutputVolumeNodeID(outputVolume.GetID())
- else:
- outputVolume = None
- cropParameters.SetOutputVolumeNodeID(inputVolume.GetID())
-
- # Make sure we can record data into the output sequence is not overwritten by any browser nodes
- browserNodesForOutputSequence = vtk.vtkCollection()
- playSuspendedForBrowserNodes = []
- slicer.modules.sequences.logic().GetBrowserNodesForSequenceNode(outputVolSeq, browserNodesForOutputSequence)
- for i in range(browserNodesForOutputSequence.GetNumberOfItems()):
- browserNodeForOutputSequence = browserNodesForOutputSequence.GetItemAsObject(i)
- if browserNodeForOutputSequence == seqBrowser:
- continue
- if browserNodeForOutputSequence.GetPlayback(outputVolSeq):
- browserNodeForOutputSequence.SetPlayback(outputVolSeq, False)
- playSuspendedForBrowserNodes.append(browserNodeForOutputSequence)
-
- try:
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
- numberOfDataNodes = inputVolSeq.GetNumberOfDataNodes()
- for seqItemNumber in range(numberOfDataNodes):
- slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
- seqBrowser.SetSelectedItemNumber(seqItemNumber)
- slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser)
- slicer.modules.cropvolume.logic().Apply(cropParameters)
- if outputVolSeq:
- # Saved cropped result
- outputVolSeq.SetDataNodeAtValue(outputVolume, inputVolSeq.GetNthIndexValue(seqItemNumber))
-
- finally:
- qt.QApplication.restoreOverrideCursor()
-
- # Temporary result node
- if outputVolume:
- slicer.mrmlScene.RemoveNode(outputVolume)
- # Temporary input browser node
- slicer.mrmlScene.RemoveNode(seqBrowser)
- # Temporary input volume proxy node
- slicer.mrmlScene.RemoveNode(inputVolume)
-
- # Move output sequence node in the same browser node as the input volume sequence
- # if not in a sequence browser node already.
- if outputVolSeq:
-
- if slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(outputVolSeq) is None:
- # Add output sequence to a sequence browser
- seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
- if seqBrowser:
- seqBrowser.AddSynchronizedSequenceNode(outputVolSeq)
- else:
- seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode")
- seqBrowser.SetAndObserveMasterSequenceNodeID(outputVolSeq.GetID())
- seqBrowser.SetOverwriteProxyName(outputVolSeq, True)
-
- # Show output in slice views
- slicer.modules.sequences.logic().UpdateAllProxyNodes()
- slicer.app.processEvents()
- outputVolume = seqBrowser.GetProxyNode(outputVolSeq)
- outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID)
- slicer.util.setSliceViewerLayers(background=outputVolume)
+ def transformForSequence(self, volumeSeq):
+ seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(volumeSeq)
+ if not seqBrowser:
+ return None
+ proxyVolume = seqBrowser.GetProxyNode(volumeSeq)
+ if not proxyVolume:
+ return None
+ return proxyVolume.GetTransformNodeID()
- else:
- # Restore play enabled states
- for playSuspendedForBrowserNode in playSuspendedForBrowserNodes:
- playSuspendedForBrowserNode.SetPlayback(outputVolSeq, True)
+ def run(self, inputVolSeq, outputVolSeq, cropParameters):
+ """
+ Run the actual algorithm
+ """
+
+ logging.info('Processing started')
- else:
- # Refresh proxy node
- seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
- slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser)
+ # Get original parent transform, if any (before creating the new sequence browser)
+ inputVolTransformNodeID = self.transformForSequence(inputVolSeq)
+ outputVolTransformNodeID = None
- logging.info('Processing completed')
+ seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode")
+ seqBrowser.SetAndObserveMasterSequenceNodeID(inputVolSeq.GetID())
+ seqBrowser.SetSaveChanges(inputVolSeq, True) # allow modifying node in the sequence
+
+ seqBrowser.SetSelectedItemNumber(0)
+ slicer.modules.sequences.logic().UpdateAllProxyNodes()
+ slicer.app.processEvents()
+ inputVolume = seqBrowser.GetProxyNode(inputVolSeq)
+ inputVolume.SetAndObserveTransformNodeID(inputVolTransformNodeID)
+ cropParameters.SetInputVolumeNodeID(inputVolume.GetID())
+
+ if outputVolSeq == inputVolSeq:
+ outputVolSeq = None
+
+ if outputVolSeq:
+ # Get original parent transform, if any (before erasing all the proxy nodes)
+ outputVolTransformNodeID = self.transformForSequence(outputVolSeq)
+
+ # Initialize output sequence
+ outputVolSeq.RemoveAllDataNodes()
+ outputVolSeq.SetIndexType(inputVolSeq.GetIndexType())
+ outputVolSeq.SetIndexName(inputVolSeq.GetIndexName())
+ outputVolSeq.SetIndexUnit(inputVolSeq.GetIndexUnit())
+ outputVolume = slicer.mrmlScene.AddNewNodeByClass(inputVolume.GetClassName())
+ outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID)
+ cropParameters.SetOutputVolumeNodeID(outputVolume.GetID())
+ else:
+ outputVolume = None
+ cropParameters.SetOutputVolumeNodeID(inputVolume.GetID())
+
+ # Make sure we can record data into the output sequence is not overwritten by any browser nodes
+ browserNodesForOutputSequence = vtk.vtkCollection()
+ playSuspendedForBrowserNodes = []
+ slicer.modules.sequences.logic().GetBrowserNodesForSequenceNode(outputVolSeq, browserNodesForOutputSequence)
+ for i in range(browserNodesForOutputSequence.GetNumberOfItems()):
+ browserNodeForOutputSequence = browserNodesForOutputSequence.GetItemAsObject(i)
+ if browserNodeForOutputSequence == seqBrowser:
+ continue
+ if browserNodeForOutputSequence.GetPlayback(outputVolSeq):
+ browserNodeForOutputSequence.SetPlayback(outputVolSeq, False)
+ playSuspendedForBrowserNodes.append(browserNodeForOutputSequence)
+
+ try:
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+ numberOfDataNodes = inputVolSeq.GetNumberOfDataNodes()
+ for seqItemNumber in range(numberOfDataNodes):
+ slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
+ seqBrowser.SetSelectedItemNumber(seqItemNumber)
+ slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser)
+ slicer.modules.cropvolume.logic().Apply(cropParameters)
+ if outputVolSeq:
+ # Saved cropped result
+ outputVolSeq.SetDataNodeAtValue(outputVolume, inputVolSeq.GetNthIndexValue(seqItemNumber))
+
+ finally:
+ qt.QApplication.restoreOverrideCursor()
+
+ # Temporary result node
+ if outputVolume:
+ slicer.mrmlScene.RemoveNode(outputVolume)
+ # Temporary input browser node
+ slicer.mrmlScene.RemoveNode(seqBrowser)
+ # Temporary input volume proxy node
+ slicer.mrmlScene.RemoveNode(inputVolume)
+
+ # Move output sequence node in the same browser node as the input volume sequence
+ # if not in a sequence browser node already.
+ if outputVolSeq:
+
+ if slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(outputVolSeq) is None:
+ # Add output sequence to a sequence browser
+ seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
+ if seqBrowser:
+ seqBrowser.AddSynchronizedSequenceNode(outputVolSeq)
+ else:
+ seqBrowser = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceBrowserNode")
+ seqBrowser.SetAndObserveMasterSequenceNodeID(outputVolSeq.GetID())
+ seqBrowser.SetOverwriteProxyName(outputVolSeq, True)
+
+ # Show output in slice views
+ slicer.modules.sequences.logic().UpdateAllProxyNodes()
+ slicer.app.processEvents()
+ outputVolume = seqBrowser.GetProxyNode(outputVolSeq)
+ outputVolume.SetAndObserveTransformNodeID(outputVolTransformNodeID)
+ slicer.util.setSliceViewerLayers(background=outputVolume)
+
+ else:
+ # Restore play enabled states
+ for playSuspendedForBrowserNode in playSuspendedForBrowserNodes:
+ playSuspendedForBrowserNode.SetPlayback(outputVolSeq, True)
+
+ else:
+ # Refresh proxy node
+ seqBrowser = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(inputVolSeq)
+ slicer.modules.sequences.logic().UpdateProxyNodesFromSequences(seqBrowser)
+
+ logging.info('Processing completed')
class CropVolumeSequenceTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.setUp()
- self.test_CropVolumeSequence1()
- def test_CropVolumeSequence1(self):
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_CropVolumeSequence1()
+
+ def test_CropVolumeSequence1(self):
- self.delayDisplay("Starting the test")
+ self.delayDisplay("Starting the test")
- # Load volume sequence
- import SampleData
- sequenceNode = SampleData.downloadSample('CTCardioSeq')
- sequenceBrowserNode = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(sequenceNode)
+ # Load volume sequence
+ import SampleData
+ sequenceNode = SampleData.downloadSample('CTCardioSeq')
+ sequenceBrowserNode = slicer.modules.sequences.logic().GetFirstBrowserNodeForSequenceNode(sequenceNode)
- # Set cropping parameters
- croppedSequenceNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSequenceNode')
- cropVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLCropVolumeParametersNode')
- cropVolumeNode.SetIsotropicResampling(True)
- cropVolumeNode.SetSpacingScalingConst(3.0)
- volumeNode = sequenceBrowserNode.GetProxyNode(sequenceNode)
+ # Set cropping parameters
+ croppedSequenceNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSequenceNode')
+ cropVolumeNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLCropVolumeParametersNode')
+ cropVolumeNode.SetIsotropicResampling(True)
+ cropVolumeNode.SetSpacingScalingConst(3.0)
+ volumeNode = sequenceBrowserNode.GetProxyNode(sequenceNode)
- # Set cropping region
- roiNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode')
- cropVolumeNode.SetROINodeID(roiNode.GetID())
- cropVolumeNode.SetInputVolumeNodeID(volumeNode.GetID())
- slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeNode)
+ # Set cropping region
+ roiNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode')
+ cropVolumeNode.SetROINodeID(roiNode.GetID())
+ cropVolumeNode.SetInputVolumeNodeID(volumeNode.GetID())
+ slicer.modules.cropvolume.logic().FitROIToInputVolume(cropVolumeNode)
- # Crop volume sequence
- CropVolumeSequenceLogic().run(sequenceNode, croppedSequenceNode, cropVolumeNode)
+ # Crop volume sequence
+ CropVolumeSequenceLogic().run(sequenceNode, croppedSequenceNode, cropVolumeNode)
- # Verify results
+ # Verify results
- self.assertEqual(croppedSequenceNode.GetNumberOfDataNodes(),
- sequenceNode.GetNumberOfDataNodes())
+ self.assertEqual(croppedSequenceNode.GetNumberOfDataNodes(),
+ sequenceNode.GetNumberOfDataNodes())
- cropVolumeNode = sequenceBrowserNode.GetProxyNode(croppedSequenceNode)
- self.assertIsNotNone(cropVolumeNode)
+ cropVolumeNode = sequenceBrowserNode.GetProxyNode(croppedSequenceNode)
+ self.assertIsNotNone(cropVolumeNode)
- # We downsampled by a factor of 3 therefore the image size must be decreased by about factor of 3
- # (less along z axis due to anisotropic input volume and isotropic output volume)
- self.assertEqual(volumeNode.GetImageData().GetExtent(), (0, 127, 0, 103, 0, 71))
- self.assertEqual(cropVolumeNode.GetImageData().GetExtent(), (0, 41, 0, 33, 0, 40))
+ # We downsampled by a factor of 3 therefore the image size must be decreased by about factor of 3
+ # (less along z axis due to anisotropic input volume and isotropic output volume)
+ self.assertEqual(volumeNode.GetImageData().GetExtent(), (0, 127, 0, 103, 0, 71))
+ self.assertEqual(cropVolumeNode.GetImageData().GetExtent(), (0, 41, 0, 33, 0, 40))
- self.delayDisplay('Test passed!')
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Scripted/DICOM/DICOM.py b/Modules/Scripted/DICOM/DICOM.py
index 0aa6b7dcdd9..a7cf76bdc02 100644
--- a/Modules/Scripted/DICOM/DICOM.py
+++ b/Modules/Scripted/DICOM/DICOM.py
@@ -22,537 +22,537 @@
class DICOM(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "DICOM"
- self.parent.categories = ["", "Informatics"] # top level module
- self.parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab)"]
- self.parent.helpText = """
+ self.parent.title = "DICOM"
+ self.parent.categories = ["", "Informatics"] # top level module
+ self.parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab)"]
+ self.parent.helpText = """
This module allows importing, loading, and exporting DICOM files, and sending receiving data using DICOM networking.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This work is supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community.
"""
- self.parent.icon = qt.QIcon(':Icons/Medium/SlicerLoadDICOM.png')
- self.parent.dependencies = ["SubjectHierarchy"]
-
- self.viewWidget = None # Widget used in the layout manager (contains just label and browser widget)
- self.browserWidget = None # SlicerDICOMBrowser instance (ctkDICOMBrowser with additional section for loading the selected items)
- self.browserSettingsWidget = None
- self.currentViewArrangement = 0
- # This variable is set to true if we temporarily
- # hide the data probe (and so we need to restore its visibility).
- self.dataProbeHasBeenTemporarilyHidden = False
-
- self.postModuleDiscoveryTasksPerformed = False
-
- def setup(self):
- # Tasks to execute after the application has started up
- slicer.app.connect("startupCompleted()", self.performPostModuleDiscoveryTasks)
- slicer.app.connect("urlReceived(QString)", self.onURLReceived)
-
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.registerPlugin(slicer.qSlicerSubjectHierarchyDICOMPlugin())
-
- self.viewFactory = slicer.qSlicerSingletonViewFactory()
- self.viewFactory.setTagName("dicombrowser")
- if slicer.app.layoutManager() is not None:
- slicer.app.layoutManager().registerViewFactory(self.viewFactory)
-
- def performPostModuleDiscoveryTasks(self):
- """Since dicom plugins are discovered while the application
- is initialized, they may be found after the DICOM module
- itself if initialized. This method is tied to a singleShot
- that will be called once the event loop is ready to start.
- """
+ self.parent.icon = qt.QIcon(':Icons/Medium/SlicerLoadDICOM.png')
+ self.parent.dependencies = ["SubjectHierarchy"]
+
+ self.viewWidget = None # Widget used in the layout manager (contains just label and browser widget)
+ self.browserWidget = None # SlicerDICOMBrowser instance (ctkDICOMBrowser with additional section for loading the selected items)
+ self.browserSettingsWidget = None
+ self.currentViewArrangement = 0
+ # This variable is set to true if we temporarily
+ # hide the data probe (and so we need to restore its visibility).
+ self.dataProbeHasBeenTemporarilyHidden = False
- if self.postModuleDiscoveryTasksPerformed:
- return
- self.postModuleDiscoveryTasksPerformed = True
-
- if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
- slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
-
- self.initializeDICOMDatabase()
-
- settings = qt.QSettings()
- if settings.contains('DICOM/RunListenerAtStart') and not slicer.app.commandOptions().testingEnabled:
- if settings.value('DICOM/RunListenerAtStart') == 'true':
- self.startListener()
-
- if not slicer.app.commandOptions().noMainWindow:
- slicer.util.mainWindow().findChild(qt.QAction, "LoadDICOMAction").setVisible(True)
- # add to the main app file menu
- self.addMenu()
- # add the settings options
- self.settingsPanel = DICOMSettingsPanel()
- slicer.app.settingsDialog().addPanel("DICOM", self.settingsPanel)
-
- layoutManager = slicer.app.layoutManager()
- layoutManager.layoutChanged.connect(self.onLayoutChanged)
- layout = (
- ""
- " - "
- " "
- "
"
- ""
- )
- layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode()
- layoutNode.AddLayoutDescription(
- slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView, layout)
- self.currentViewArrangement = layoutNode.GetViewArrangement()
- self.previousViewArrangement = layoutNode.GetViewArrangement()
-
- slicer.app.moduleManager().connect(
- 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded)
-
- def onURLReceived(self, urlString):
- """Process DICOM view requests. Example:
- slicer://viewer/?studyUID=2.16.840.1.113669.632.20.121711.10000158860
- &access_token=k0zR6WAPpNbVguQ8gGUHp6
- &dicomweb_endpoint=http%3A%2F%2Fdemo.kheops.online%2Fapi
- &dicomweb_uri_endpoint=%20http%3A%2F%2Fdemo.kheops.online%2Fapi%2Fwado
- """
+ self.postModuleDiscoveryTasksPerformed = False
+
+ def setup(self):
+ # Tasks to execute after the application has started up
+ slicer.app.connect("startupCompleted()", self.performPostModuleDiscoveryTasks)
+ slicer.app.connect("urlReceived(QString)", self.onURLReceived)
+
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.registerPlugin(slicer.qSlicerSubjectHierarchyDICOMPlugin())
+
+ self.viewFactory = slicer.qSlicerSingletonViewFactory()
+ self.viewFactory.setTagName("dicombrowser")
+ if slicer.app.layoutManager() is not None:
+ slicer.app.layoutManager().registerViewFactory(self.viewFactory)
+
+ def performPostModuleDiscoveryTasks(self):
+ """Since dicom plugins are discovered while the application
+ is initialized, they may be found after the DICOM module
+ itself if initialized. This method is tied to a singleShot
+ that will be called once the event loop is ready to start.
+ """
+
+ if self.postModuleDiscoveryTasksPerformed:
+ return
+ self.postModuleDiscoveryTasksPerformed = True
+
+ if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
+ slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
+
+ self.initializeDICOMDatabase()
+
+ settings = qt.QSettings()
+ if settings.contains('DICOM/RunListenerAtStart') and not slicer.app.commandOptions().testingEnabled:
+ if settings.value('DICOM/RunListenerAtStart') == 'true':
+ self.startListener()
+
+ if not slicer.app.commandOptions().noMainWindow:
+ slicer.util.mainWindow().findChild(qt.QAction, "LoadDICOMAction").setVisible(True)
+ # add to the main app file menu
+ self.addMenu()
+ # add the settings options
+ self.settingsPanel = DICOMSettingsPanel()
+ slicer.app.settingsDialog().addPanel("DICOM", self.settingsPanel)
+
+ layoutManager = slicer.app.layoutManager()
+ layoutManager.layoutChanged.connect(self.onLayoutChanged)
+ layout = (
+ ""
+ " - "
+ " "
+ "
"
+ ""
+ )
+ layoutNode = slicer.app.layoutManager().layoutLogic().GetLayoutNode()
+ layoutNode.AddLayoutDescription(
+ slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView, layout)
+ self.currentViewArrangement = layoutNode.GetViewArrangement()
+ self.previousViewArrangement = layoutNode.GetViewArrangement()
+
+ slicer.app.moduleManager().connect(
+ 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded)
+
+ def onURLReceived(self, urlString):
+ """Process DICOM view requests. Example:
+ slicer://viewer/?studyUID=2.16.840.1.113669.632.20.121711.10000158860
+ &access_token=k0zR6WAPpNbVguQ8gGUHp6
+ &dicomweb_endpoint=http%3A%2F%2Fdemo.kheops.online%2Fapi
+ &dicomweb_uri_endpoint=%20http%3A%2F%2Fdemo.kheops.online%2Fapi%2Fwado
+ """
+
+ url = qt.QUrl(urlString)
+ if (url.authority().lower() != "viewer"):
+ logging.debug("DICOM module ignores non-viewer URL: " + urlString)
+ return
+ query = qt.QUrlQuery(url)
+ queryMap = {}
+ for key, value in query.queryItems(qt.QUrl.FullyDecoded):
+ queryMap[key] = qt.QUrl.fromPercentEncoding(value)
+
+ if not "dicomweb_endpoint" in queryMap:
+ logging.debug("DICOM module ignores URL without dicomweb_endpoint query parameter: " + urlString)
+ return
+ if not "studyUID" in queryMap:
+ logging.debug("DICOM module ignores URL without studyUID query parameter: " + urlString)
+ return
+
+ logging.info("DICOM module received URL: " + urlString)
+
+ accessToken = None
+ if "access_token" in queryMap:
+ accessToken = queryMap["access_token"]
+
+ slicer.util.selectModule("DICOM")
+ slicer.app.processEvents()
+ from DICOMLib import DICOMUtils
+ importedSeriesInstanceUIDs = DICOMUtils.importFromDICOMWeb(
+ dicomWebEndpoint=queryMap["dicomweb_endpoint"],
+ studyInstanceUID=queryMap["studyUID"],
+ accessToken=accessToken)
+
+ # Select newly loaded items to make it easier to load them
+ self.browserWidget.dicomBrowser.setSelectedItems(ctk.ctkDICOMModel.SeriesType, importedSeriesInstanceUIDs)
+
+ def initializeDICOMDatabase(self):
+ # Create alias for convenience
+ slicer.dicomDatabase = slicer.app.dicomDatabase()
+
+ # Set the dicom pre-cache tags once all plugin classes have been initialized.
+ # Pre-caching tags is very important for fast DICOM loading because tags that are
+ # not pre-cached during DICOM import in bulk, will be cached during Examine step one-by-one
+ # (which takes magnitudes more time).
+ tagsToPrecache = list(slicer.dicomDatabase.tagsToPrecache)
+ for pluginClass in slicer.modules.dicomPlugins:
+ plugin = slicer.modules.dicomPlugins[pluginClass]()
+ tagsToPrecache += list(plugin.tags.values())
+ tagsToPrecache = sorted(set(tagsToPrecache)) # remove duplicates
+ slicer.dicomDatabase.tagsToPrecache = tagsToPrecache
+
+ # Try to initialize the database using the location stored in settings
+ if slicer.app.commandOptions().testingEnabled:
+ # For automatic tests (use a separate DICOM database for testing)
+ slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectoryTest_' + ctk.ctkDICOMDatabase().schemaVersion()
+ databaseDirectory = os.path.join(slicer.app.temporaryPath,
+ 'temp' + slicer.app.applicationName + 'DICOMDatabase_' + ctk.ctkDICOMDatabase().schemaVersion())
+ else:
+ # For production
+ slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectory_' + ctk.ctkDICOMDatabase().schemaVersion()
+ settings = qt.QSettings()
+ databaseDirectory = settings.value(slicer.dicomDatabaseDirectorySettingsKey)
+ if not databaseDirectory:
+ documentsLocation = qt.QStandardPaths.DocumentsLocation
+ documents = qt.QStandardPaths.writableLocation(documentsLocation)
+ databaseDirectory = os.path.join(documents, slicer.app.applicationName + "DICOMDatabase")
+ settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, databaseDirectory)
+
+ # Attempt to open the database. If it fails then user will have to configure it using DICOM module.
+ databaseFileName = databaseDirectory + "/ctkDICOM.sql"
+ slicer.dicomDatabase.openDatabase(databaseFileName)
+ if slicer.dicomDatabase.isOpen:
+ # There is an existing database at the current location
+ if slicer.dicomDatabase.schemaVersionLoaded() != slicer.dicomDatabase.schemaVersion():
+ # Schema does not match, do not use it
+ slicer.dicomDatabase.closeDatabase()
+
+ def startListener(self):
+
+ if not slicer.dicomDatabase.isOpen:
+ logging.error("Failed to start DICOM listener. DICOM database is not open.")
+ return False
- url = qt.QUrl(urlString)
- if (url.authority().lower() != "viewer"):
- logging.debug("DICOM module ignores non-viewer URL: " + urlString)
- return
- query = qt.QUrlQuery(url)
- queryMap = {}
- for key, value in query.queryItems(qt.QUrl.FullyDecoded):
- queryMap[key] = qt.QUrl.fromPercentEncoding(value)
-
- if not "dicomweb_endpoint" in queryMap:
- logging.debug("DICOM module ignores URL without dicomweb_endpoint query parameter: " + urlString)
- return
- if not "studyUID" in queryMap:
- logging.debug("DICOM module ignores URL without studyUID query parameter: " + urlString)
- return
-
- logging.info("DICOM module received URL: " + urlString)
-
- accessToken = None
- if "access_token" in queryMap:
- accessToken = queryMap["access_token"]
-
- slicer.util.selectModule("DICOM")
- slicer.app.processEvents()
- from DICOMLib import DICOMUtils
- importedSeriesInstanceUIDs = DICOMUtils.importFromDICOMWeb(
- dicomWebEndpoint=queryMap["dicomweb_endpoint"],
- studyInstanceUID=queryMap["studyUID"],
- accessToken=accessToken)
-
- # Select newly loaded items to make it easier to load them
- self.browserWidget.dicomBrowser.setSelectedItems(ctk.ctkDICOMModel.SeriesType, importedSeriesInstanceUIDs)
-
- def initializeDICOMDatabase(self):
- # Create alias for convenience
- slicer.dicomDatabase = slicer.app.dicomDatabase()
-
- # Set the dicom pre-cache tags once all plugin classes have been initialized.
- # Pre-caching tags is very important for fast DICOM loading because tags that are
- # not pre-cached during DICOM import in bulk, will be cached during Examine step one-by-one
- # (which takes magnitudes more time).
- tagsToPrecache = list(slicer.dicomDatabase.tagsToPrecache)
- for pluginClass in slicer.modules.dicomPlugins:
- plugin = slicer.modules.dicomPlugins[pluginClass]()
- tagsToPrecache += list(plugin.tags.values())
- tagsToPrecache = sorted(set(tagsToPrecache)) # remove duplicates
- slicer.dicomDatabase.tagsToPrecache = tagsToPrecache
-
- # Try to initialize the database using the location stored in settings
- if slicer.app.commandOptions().testingEnabled:
- # For automatic tests (use a separate DICOM database for testing)
- slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectoryTest_' + ctk.ctkDICOMDatabase().schemaVersion()
- databaseDirectory = os.path.join(slicer.app.temporaryPath,
- 'temp' + slicer.app.applicationName + 'DICOMDatabase_' + ctk.ctkDICOMDatabase().schemaVersion())
- else:
- # For production
- slicer.dicomDatabaseDirectorySettingsKey = 'DatabaseDirectory_' + ctk.ctkDICOMDatabase().schemaVersion()
- settings = qt.QSettings()
- databaseDirectory = settings.value(slicer.dicomDatabaseDirectorySettingsKey)
- if not databaseDirectory:
- documentsLocation = qt.QStandardPaths.DocumentsLocation
- documents = qt.QStandardPaths.writableLocation(documentsLocation)
- databaseDirectory = os.path.join(documents, slicer.app.applicationName + "DICOMDatabase")
- settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, databaseDirectory)
-
- # Attempt to open the database. If it fails then user will have to configure it using DICOM module.
- databaseFileName = databaseDirectory + "/ctkDICOM.sql"
- slicer.dicomDatabase.openDatabase(databaseFileName)
- if slicer.dicomDatabase.isOpen:
- # There is an existing database at the current location
- if slicer.dicomDatabase.schemaVersionLoaded() != slicer.dicomDatabase.schemaVersion():
- # Schema does not match, do not use it
- slicer.dicomDatabase.closeDatabase()
-
- def startListener(self):
-
- if not slicer.dicomDatabase.isOpen:
- logging.error("Failed to start DICOM listener. DICOM database is not open.")
- return False
-
- if not hasattr(slicer, 'dicomListener'):
- dicomListener = DICOMLib.DICOMListener(slicer.dicomDatabase)
-
- try:
- dicomListener.start()
- except (UserWarning, OSError) as message:
- logging.error('Problem trying to start DICOM listener:\n %s' % message)
- return False
- if not dicomListener.process:
- logging.error("Failed to start DICOM listener. Process start failed.")
- return False
- slicer.dicomListener = dicomListener
- logging.info("DICOM C-Store SCP service started at port " + str(slicer.dicomListener.port))
-
- def stopListener(self):
- if hasattr(slicer, 'dicomListener'):
- logging.info("DICOM C-Store SCP service stopping")
- slicer.dicomListener.stop()
- del slicer.dicomListener
-
- def addMenu(self):
- """Add an action to the File menu that will go into
- the DICOM module by selecting the module. Note that
- once the module is constructed (below in setup) another
- connection is made that will also cause the instance-created
- DICOM browser to be raised by this menu action"""
- a = self.parent.action()
- a.setText(a.tr("Add DICOM Data"))
- fileMenu = slicer.util.lookupTopLevelWidget('FileMenu')
- if fileMenu:
- for child in fileMenu.children():
- if child.objectName == "RecentlyLoadedMenu":
- fileMenu.insertAction(child.menuAction(), a) # insert action before RecentlyLoadedMenu
-
- def setBrowserWidgetInDICOMLayout(self, browserWidget):
- """Set DICOM browser widget in the custom view layout"""
- if self.browserWidget == browserWidget:
- return
-
- if self.browserWidget is not None:
- self.browserWidget.closed.disconnect(self.onBrowserWidgetClosed)
-
- oldBrowserWidget = self.browserWidget
- self.browserWidget = browserWidget
- self.browserWidget.setAutoFillBackground(True)
- if slicer.util.mainWindow():
- # For some reason, we cannot disconnect this event connection if
- # main window, and not disconnecting would cause crash on application shutdown,
- # so we only connect when main window is present.
- self.browserWidget.closed.connect(self.onBrowserWidgetClosed)
-
- if self.viewWidget is None:
- self.viewWidget = qt.QWidget()
- self.viewWidget.setAutoFillBackground(True)
- self.viewFactory.setWidget(self.viewWidget)
- layout = qt.QVBoxLayout()
- self.viewWidget.setLayout(layout)
-
- label = qt.QLabel("DICOM database")
- label.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- layout.addWidget(label)
- font = qt.QFont()
- font.setBold(True)
- font.setPointSize(12)
- label.setFont(font)
-
- if oldBrowserWidget is not None:
- self.viewWidget.layout().removeWidget(oldBrowserWidget)
-
- if self.browserWidget:
- self.viewWidget.layout().addWidget(self.browserWidget)
-
- def onLayoutChanged(self, viewArrangement):
- if viewArrangement == self.currentViewArrangement:
- return
-
- if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone and
- self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView):
- self.previousViewArrangement = self.currentViewArrangement
- self.currentViewArrangement = viewArrangement
-
- if self.browserWidget is None:
- return
- mw = slicer.util.mainWindow()
- dataProbe = mw.findChild("QWidget", "DataProbeCollapsibleWidget") if mw else None
- if self.currentViewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView:
- # View has been changed to the DICOM browser view
- self.browserWidget.show()
- # If we are in DICOM module, hide the Data Probe to have more space for the module
- try:
- inDicomModule = slicer.modules.dicom.widgetRepresentation().isEntered
- except AttributeError:
- # Slicer is shutting down
- inDicomModule = False
- if inDicomModule and dataProbe and dataProbe.isVisible():
- dataProbe.setVisible(False)
- self.dataProbeHasBeenTemporarilyHidden = True
- else:
- # View has been changed from the DICOM browser view
- if self.dataProbeHasBeenTemporarilyHidden:
- # DataProbe was temporarily hidden, restore its visibility now
- dataProbe.setVisible(True)
- self.dataProbeHasBeenTemporarilyHidden = False
+ if not hasattr(slicer, 'dicomListener'):
+ dicomListener = DICOMLib.DICOMListener(slicer.dicomDatabase)
- def onBrowserWidgetClosed(self):
- if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView and
- self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone):
- # current layout is a valid layout that is not the DICOM browser view, so nothing to do
- return
+ try:
+ dicomListener.start()
+ except (UserWarning, OSError) as message:
+ logging.error('Problem trying to start DICOM listener:\n %s' % message)
+ return False
+ if not dicomListener.process:
+ logging.error("Failed to start DICOM listener. Process start failed.")
+ return False
+ slicer.dicomListener = dicomListener
+ logging.info("DICOM C-Store SCP service started at port " + str(slicer.dicomListener.port))
+
+ def stopListener(self):
+ if hasattr(slicer, 'dicomListener'):
+ logging.info("DICOM C-Store SCP service stopping")
+ slicer.dicomListener.stop()
+ del slicer.dicomListener
+
+ def addMenu(self):
+ """Add an action to the File menu that will go into
+ the DICOM module by selecting the module. Note that
+ once the module is constructed (below in setup) another
+ connection is made that will also cause the instance-created
+ DICOM browser to be raised by this menu action"""
+ a = self.parent.action()
+ a.setText(a.tr("Add DICOM Data"))
+ fileMenu = slicer.util.lookupTopLevelWidget('FileMenu')
+ if fileMenu:
+ for child in fileMenu.children():
+ if child.objectName == "RecentlyLoadedMenu":
+ fileMenu.insertAction(child.menuAction(), a) # insert action before RecentlyLoadedMenu
+
+ def setBrowserWidgetInDICOMLayout(self, browserWidget):
+ """Set DICOM browser widget in the custom view layout"""
+ if self.browserWidget == browserWidget:
+ return
+
+ if self.browserWidget is not None:
+ self.browserWidget.closed.disconnect(self.onBrowserWidgetClosed)
+
+ oldBrowserWidget = self.browserWidget
+ self.browserWidget = browserWidget
+ self.browserWidget.setAutoFillBackground(True)
+ if slicer.util.mainWindow():
+ # For some reason, we cannot disconnect this event connection if
+ # main window, and not disconnecting would cause crash on application shutdown,
+ # so we only connect when main window is present.
+ self.browserWidget.closed.connect(self.onBrowserWidgetClosed)
+
+ if self.viewWidget is None:
+ self.viewWidget = qt.QWidget()
+ self.viewWidget.setAutoFillBackground(True)
+ self.viewFactory.setWidget(self.viewWidget)
+ layout = qt.QVBoxLayout()
+ self.viewWidget.setLayout(layout)
+
+ label = qt.QLabel("DICOM database")
+ label.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ layout.addWidget(label)
+ font = qt.QFont()
+ font.setBold(True)
+ font.setPointSize(12)
+ label.setFont(font)
+
+ if oldBrowserWidget is not None:
+ self.viewWidget.layout().removeWidget(oldBrowserWidget)
+
+ if self.browserWidget:
+ self.viewWidget.layout().addWidget(self.browserWidget)
+
+ def onLayoutChanged(self, viewArrangement):
+ if viewArrangement == self.currentViewArrangement:
+ return
+
+ if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone and
+ self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView):
+ self.previousViewArrangement = self.currentViewArrangement
+ self.currentViewArrangement = viewArrangement
+
+ if self.browserWidget is None:
+ return
+ mw = slicer.util.mainWindow()
+ dataProbe = mw.findChild("QWidget", "DataProbeCollapsibleWidget") if mw else None
+ if self.currentViewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView:
+ # View has been changed to the DICOM browser view
+ self.browserWidget.show()
+ # If we are in DICOM module, hide the Data Probe to have more space for the module
+ try:
+ inDicomModule = slicer.modules.dicom.widgetRepresentation().isEntered
+ except AttributeError:
+ # Slicer is shutting down
+ inDicomModule = False
+ if inDicomModule and dataProbe and dataProbe.isVisible():
+ dataProbe.setVisible(False)
+ self.dataProbeHasBeenTemporarilyHidden = True
+ else:
+ # View has been changed from the DICOM browser view
+ if self.dataProbeHasBeenTemporarilyHidden:
+ # DataProbe was temporarily hidden, restore its visibility now
+ dataProbe.setVisible(True)
+ self.dataProbeHasBeenTemporarilyHidden = False
- layoutId = self.previousViewArrangement
+ def onBrowserWidgetClosed(self):
+ if (self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView and
+ self.currentViewArrangement != slicer.vtkMRMLLayoutNode.SlicerLayoutNone):
+ # current layout is a valid layout that is not the DICOM browser view, so nothing to do
+ return
- # Use a default layout if this layout is not valid
- if (layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutNone
- or layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView):
- layoutId = qt.QSettings().value("MainWindow/layout", slicer.vtkMRMLLayoutNode.SlicerLayoutInitialView)
+ layoutId = self.previousViewArrangement
- slicer.app.layoutManager().setLayout(layoutId)
+ # Use a default layout if this layout is not valid
+ if (layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutNone
+ or layoutId == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView):
+ layoutId = qt.QSettings().value("MainWindow/layout", slicer.vtkMRMLLayoutNode.SlicerLayoutInitialView)
- def _onModuleAboutToBeUnloaded(self, moduleName):
- # Application is shutting down. Stop the listener.
- if moduleName == "DICOM":
- self.stopListener()
- slicer.app.moduleManager().disconnect(
- 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded)
+ slicer.app.layoutManager().setLayout(layoutId)
+
+ def _onModuleAboutToBeUnloaded(self, moduleName):
+ # Application is shutting down. Stop the listener.
+ if moduleName == "DICOM":
+ self.stopListener()
+ slicer.app.moduleManager().disconnect(
+ 'moduleAboutToBeUnloaded(QString)', self._onModuleAboutToBeUnloaded)
class _ui_DICOMSettingsPanel:
- def __init__(self, parent):
- vBoxLayout = qt.QVBoxLayout(parent)
- # Add generic settings
- genericGroupBox = ctk.ctkCollapsibleGroupBox()
- genericGroupBox.title = "Generic DICOM settings"
- genericGroupBoxFormLayout = qt.QFormLayout(genericGroupBox)
-
- directoryButton = ctk.ctkDirectoryButton()
- genericGroupBoxFormLayout.addRow("Database location:", directoryButton)
- parent.registerProperty(slicer.dicomDatabaseDirectorySettingsKey, directoryButton,
- "directory", str(qt.SIGNAL("directoryChanged(QString)")),
- "DICOM general settings", ctk.ctkSettingsPanel.OptionRequireRestart)
- # Restart is forced because no mechanism is implemented that would reopen the DICOM database after
- # folder location is changed. It is easier to restart the application than implementing an update
- # mechanism.
-
- loadReferencesComboBox = ctk.ctkComboBox()
- loadReferencesComboBox.toolTip = "Determines whether referenced DICOM series are " \
- "offered when loading DICOM, or the automatic behavior if interaction is disabled. " \
- "Interactive selection of referenced series is the default selection"
- loadReferencesComboBox.addItem("Ask user", qt.QMessageBox.InvalidRole)
- loadReferencesComboBox.addItem("Always", qt.QMessageBox.Yes)
- loadReferencesComboBox.addItem("Never", qt.QMessageBox.No)
- loadReferencesComboBox.currentIndex = 0
- genericGroupBoxFormLayout.addRow("Load referenced series:", loadReferencesComboBox)
- parent.registerProperty(
- "DICOM/automaticallyLoadReferences", loadReferencesComboBox,
- "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")))
-
- detailedLoggingCheckBox = qt.QCheckBox()
- detailedLoggingCheckBox.toolTip = ("Log more details during DICOM operations."
- " Useful for investigating DICOM loading issues but may impact performance.")
- genericGroupBoxFormLayout.addRow("Detailed logging:", detailedLoggingCheckBox)
- detailedLoggingMapper = ctk.ctkBooleanMapper(detailedLoggingCheckBox, "checked", str(qt.SIGNAL("toggled(bool)")))
- parent.registerProperty(
- "DICOM/detailedLogging", detailedLoggingMapper,
- "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")))
-
- vBoxLayout.addWidget(genericGroupBox)
-
- # Add settings panel for the plugins
- plugins = slicer.modules.dicomPlugins
- for pluginName in plugins.keys():
- if hasattr(plugins[pluginName], 'settingsPanelEntry'):
- pluginGroupBox = ctk.ctkCollapsibleGroupBox()
- pluginGroupBox.title = pluginName
- vBoxLayout.addWidget(pluginGroupBox)
- plugins[pluginName].settingsPanelEntry(parent, pluginGroupBox)
- vBoxLayout.addStretch(1)
+ def __init__(self, parent):
+ vBoxLayout = qt.QVBoxLayout(parent)
+ # Add generic settings
+ genericGroupBox = ctk.ctkCollapsibleGroupBox()
+ genericGroupBox.title = "Generic DICOM settings"
+ genericGroupBoxFormLayout = qt.QFormLayout(genericGroupBox)
+
+ directoryButton = ctk.ctkDirectoryButton()
+ genericGroupBoxFormLayout.addRow("Database location:", directoryButton)
+ parent.registerProperty(slicer.dicomDatabaseDirectorySettingsKey, directoryButton,
+ "directory", str(qt.SIGNAL("directoryChanged(QString)")),
+ "DICOM general settings", ctk.ctkSettingsPanel.OptionRequireRestart)
+ # Restart is forced because no mechanism is implemented that would reopen the DICOM database after
+ # folder location is changed. It is easier to restart the application than implementing an update
+ # mechanism.
+
+ loadReferencesComboBox = ctk.ctkComboBox()
+ loadReferencesComboBox.toolTip = "Determines whether referenced DICOM series are " \
+ "offered when loading DICOM, or the automatic behavior if interaction is disabled. " \
+ "Interactive selection of referenced series is the default selection"
+ loadReferencesComboBox.addItem("Ask user", qt.QMessageBox.InvalidRole)
+ loadReferencesComboBox.addItem("Always", qt.QMessageBox.Yes)
+ loadReferencesComboBox.addItem("Never", qt.QMessageBox.No)
+ loadReferencesComboBox.currentIndex = 0
+ genericGroupBoxFormLayout.addRow("Load referenced series:", loadReferencesComboBox)
+ parent.registerProperty(
+ "DICOM/automaticallyLoadReferences", loadReferencesComboBox,
+ "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")))
+
+ detailedLoggingCheckBox = qt.QCheckBox()
+ detailedLoggingCheckBox.toolTip = ("Log more details during DICOM operations."
+ " Useful for investigating DICOM loading issues but may impact performance.")
+ genericGroupBoxFormLayout.addRow("Detailed logging:", detailedLoggingCheckBox)
+ detailedLoggingMapper = ctk.ctkBooleanMapper(detailedLoggingCheckBox, "checked", str(qt.SIGNAL("toggled(bool)")))
+ parent.registerProperty(
+ "DICOM/detailedLogging", detailedLoggingMapper,
+ "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")))
+
+ vBoxLayout.addWidget(genericGroupBox)
+
+ # Add settings panel for the plugins
+ plugins = slicer.modules.dicomPlugins
+ for pluginName in plugins.keys():
+ if hasattr(plugins[pluginName], 'settingsPanelEntry'):
+ pluginGroupBox = ctk.ctkCollapsibleGroupBox()
+ pluginGroupBox.title = pluginName
+ vBoxLayout.addWidget(pluginGroupBox)
+ plugins[pluginName].settingsPanelEntry(parent, pluginGroupBox)
+ vBoxLayout.addStretch(1)
class DICOMSettingsPanel(ctk.ctkSettingsPanel):
- def __init__(self, *args, **kwargs):
- ctk.ctkSettingsPanel.__init__(self, *args, **kwargs)
- self.ui = _ui_DICOMSettingsPanel(self)
+ def __init__(self, *args, **kwargs):
+ ctk.ctkSettingsPanel.__init__(self, *args, **kwargs)
+ self.ui = _ui_DICOMSettingsPanel(self)
#
# DICOM file dialog
#
class DICOMFileDialog:
- """This specially named class is detected by the scripted loadable
- module and is the target for optional drag and drop operations.
- See: Base/QTGUI/qSlicerScriptedFileDialog.h
- and commit http://svn.slicer.org/Slicer4/trunk@21951 and issue #3081
- """
-
- def __init__(self, qSlicerFileDialog):
- self.qSlicerFileDialog = qSlicerFileDialog
- qSlicerFileDialog.fileType = 'DICOM Directory'
- qSlicerFileDialog.description = 'Load directory into DICOM database'
- qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read
- self.directoriesToAdd = []
-
- def execDialog(self):
- """Not used"""
- logging.debug('execDialog called on %s' % self)
-
- def isMimeDataAccepted(self):
- """Checks the dropped data and returns true if it is one or
- more directories"""
- self.directoriesToAdd, _ = DICOMFileDialog.pathsFromMimeData(self.qSlicerFileDialog.mimeData())
- self.qSlicerFileDialog.acceptMimeData(len(self.directoriesToAdd) != 0)
-
- @staticmethod
- def pathsFromMimeData(mimeData):
- directoriesToAdd = []
- filesToAdd = []
- if mimeData.hasFormat('text/uri-list'):
- urls = mimeData.urls()
- for url in urls:
- localPath = url.toLocalFile() # convert QUrl to local path
- pathInfo = qt.QFileInfo()
- pathInfo.setFile(localPath) # information about the path
- if pathInfo.isDir(): # if it is a directory we add the files to the dialog
- directoriesToAdd.append(localPath)
- else:
- filesToAdd.append(localPath)
- return directoriesToAdd, filesToAdd
-
- @staticmethod
- def isAscii(s):
- """Return True if string only contains ASCII characters.
+ """This specially named class is detected by the scripted loadable
+ module and is the target for optional drag and drop operations.
+ See: Base/QTGUI/qSlicerScriptedFileDialog.h
+ and commit http://svn.slicer.org/Slicer4/trunk@21951 and issue #3081
"""
- if isinstance(s, str):
- try:
- s.encode('ascii')
- except UnicodeEncodeError:
- # encoding as ascii failed, therefore it was not an ascii string
- return False
- else:
- try:
- s.decode('ascii')
- except UnicodeDecodeError:
- # decoding to ascii failed, therefore it was not an ascii string
+
+ def __init__(self, qSlicerFileDialog):
+ self.qSlicerFileDialog = qSlicerFileDialog
+ qSlicerFileDialog.fileType = 'DICOM Directory'
+ qSlicerFileDialog.description = 'Load directory into DICOM database'
+ qSlicerFileDialog.action = slicer.qSlicerFileDialog.Read
+ self.directoriesToAdd = []
+
+ def execDialog(self):
+ """Not used"""
+ logging.debug('execDialog called on %s' % self)
+
+ def isMimeDataAccepted(self):
+ """Checks the dropped data and returns true if it is one or
+ more directories"""
+ self.directoriesToAdd, _ = DICOMFileDialog.pathsFromMimeData(self.qSlicerFileDialog.mimeData())
+ self.qSlicerFileDialog.acceptMimeData(len(self.directoriesToAdd) != 0)
+
+ @staticmethod
+ def pathsFromMimeData(mimeData):
+ directoriesToAdd = []
+ filesToAdd = []
+ if mimeData.hasFormat('text/uri-list'):
+ urls = mimeData.urls()
+ for url in urls:
+ localPath = url.toLocalFile() # convert QUrl to local path
+ pathInfo = qt.QFileInfo()
+ pathInfo.setFile(localPath) # information about the path
+ if pathInfo.isDir(): # if it is a directory we add the files to the dialog
+ directoriesToAdd.append(localPath)
+ else:
+ filesToAdd.append(localPath)
+ return directoriesToAdd, filesToAdd
+
+ @staticmethod
+ def isAscii(s):
+ """Return True if string only contains ASCII characters.
+ """
+ if isinstance(s, str):
+ try:
+ s.encode('ascii')
+ except UnicodeEncodeError:
+ # encoding as ascii failed, therefore it was not an ascii string
+ return False
+ else:
+ try:
+ s.decode('ascii')
+ except UnicodeDecodeError:
+ # decoding to ascii failed, therefore it was not an ascii string
+ return False
+ return True
+
+ @staticmethod
+ def validDirectories(directoriesToAdd):
+ """Return True if the directory names are acceptable for input.
+ If path contains non-ASCII characters then they are rejected because
+ DICOM module cannot reliable read files form folders that contain
+ special characters in the name.
+ """
+ if slicer.app.isCodePageUtf8():
+ return True
+
+ for directoryName in directoriesToAdd:
+ for root, dirs, files in os.walk(directoryName):
+ if not DICOMFileDialog.isAscii(root):
+ return False
+ for name in files:
+ if not DICOMFileDialog.isAscii(name):
+ return False
+ for name in dirs:
+ if not DICOMFileDialog.isAscii(name):
+ return False
+
+ return True
+
+ @staticmethod
+ def createDefaultDatabase():
+ """If DICOM database is invalid then try to create a default one. If fails then show an error message.
+ This method should only be used when user initiates DICOM import on the GUI, because the error message is
+ shown in a popup, which would block execution of auomated processing scripts.
+ Returns True if a valid DICOM database is available (has been created succussfully or it was already available).
+ """
+ if slicer.dicomDatabase and slicer.dicomDatabase.isOpen:
+ # Valid DICOM database already exists
+ return True
+
+ # Try to create a database with default settings
+ if slicer.modules.DICOMInstance.browserWidget is None:
+ slicer.util.selectModule('DICOM')
+ slicer.modules.DICOMInstance.browserWidget.dicomBrowser.createNewDatabaseDirectory()
+ if slicer.dicomDatabase and slicer.dicomDatabase.isOpen:
+ # DICOM database created successfully
+ return True
+
+ # Failed to create database
+ # Make sure the browser is visible then display error message
+ slicer.util.selectModule('DICOM')
+ slicer.modules.dicom.widgetRepresentation().self().onOpenBrowserWidget()
+ slicer.util.warningDisplay("Could not create a DICOM database with default settings. Please create a new database or"
+ " update the existing incompatible database using options shown in DICOM browser.")
return False
- return True
-
- @staticmethod
- def validDirectories(directoriesToAdd):
- """Return True if the directory names are acceptable for input.
- If path contains non-ASCII characters then they are rejected because
- DICOM module cannot reliable read files form folders that contain
- special characters in the name.
- """
- if slicer.app.isCodePageUtf8():
- return True
-
- for directoryName in directoriesToAdd:
- for root, dirs, files in os.walk(directoryName):
- if not DICOMFileDialog.isAscii(root):
- return False
- for name in files:
- if not DICOMFileDialog.isAscii(name):
- return False
- for name in dirs:
- if not DICOMFileDialog.isAscii(name):
- return False
- return True
+ def dropEvent(self):
+ if not DICOMFileDialog.createDefaultDatabase():
+ return
- @staticmethod
- def createDefaultDatabase():
- """If DICOM database is invalid then try to create a default one. If fails then show an error message.
- This method should only be used when user initiates DICOM import on the GUI, because the error message is
- shown in a popup, which would block execution of auomated processing scripts.
- Returns True if a valid DICOM database is available (has been created succussfully or it was already available).
- """
- if slicer.dicomDatabase and slicer.dicomDatabase.isOpen:
- # Valid DICOM database already exists
- return True
-
- # Try to create a database with default settings
- if slicer.modules.DICOMInstance.browserWidget is None:
- slicer.util.selectModule('DICOM')
- slicer.modules.DICOMInstance.browserWidget.dicomBrowser.createNewDatabaseDirectory()
- if slicer.dicomDatabase and slicer.dicomDatabase.isOpen:
- # DICOM database created successfully
- return True
-
- # Failed to create database
- # Make sure the browser is visible then display error message
- slicer.util.selectModule('DICOM')
- slicer.modules.dicom.widgetRepresentation().self().onOpenBrowserWidget()
- slicer.util.warningDisplay("Could not create a DICOM database with default settings. Please create a new database or"
- " update the existing incompatible database using options shown in DICOM browser.")
- return False
-
- def dropEvent(self):
- if not DICOMFileDialog.createDefaultDatabase():
- return
-
- if not DICOMFileDialog.validDirectories(self.directoriesToAdd):
- if not slicer.util.confirmYesNoDisplay("Import of files that have special (non-ASCII) characters in their names is not supported."
- " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"):
- self.directoriesToAdd = []
- return
+ if not DICOMFileDialog.validDirectories(self.directoriesToAdd):
+ if not slicer.util.confirmYesNoDisplay("Import of files that have special (non-ASCII) characters in their names is not supported."
+ " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"):
+ self.directoriesToAdd = []
+ return
- slicer.util.selectModule('DICOM')
- slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd)
- self.directoriesToAdd = []
+ slicer.util.selectModule('DICOM')
+ slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd)
+ self.directoriesToAdd = []
class DICOMLoadingByDragAndDropEventFilter(qt.QWidget):
- """This event filter is used for overriding drag-and-drop behavior while
- the DICOM module is active. To simplify DICOM import, while DICOM module is active
- then files or folders that are drag-and-dropped to the application window
- are always interpreted as DICOM data.
- """
-
- def eventFilter(self, object, event):
+ """This event filter is used for overriding drag-and-drop behavior while
+ the DICOM module is active. To simplify DICOM import, while DICOM module is active
+ then files or folders that are drag-and-dropped to the application window
+ are always interpreted as DICOM data.
"""
- Custom event filter for Slicer Main Window.
- Inputs: Object (QObject), Event (QEvent)
- """
- if event.type() == qt.QEvent.DragEnter:
- self.dragEnterEvent(event)
- return True
- if event.type() == qt.QEvent.Drop:
- self.dropEvent(event)
- return True
- return False
-
- def dragEnterEvent(self, event):
- """
- Actions to do when a drag enter event occurs in the Main Window.
+ def eventFilter(self, object, event):
+ """
+ Custom event filter for Slicer Main Window.
+
+ Inputs: Object (QObject), Event (QEvent)
+ """
+ if event.type() == qt.QEvent.DragEnter:
+ self.dragEnterEvent(event)
+ return True
+ if event.type() == qt.QEvent.Drop:
+ self.dropEvent(event)
+ return True
+ return False
- Read up on https://doc.qt.io/qt-5.12/dnd.html#dropping
- Input: Event (QEvent)
- """
- self.directoriesToAdd, self.filesToAdd = DICOMFileDialog.pathsFromMimeData(event.mimeData())
- if self.directoriesToAdd or self.filesToAdd:
- event.acceptProposedAction() # allows drop event to proceed
- else:
- event.ignore()
-
- def dropEvent(self, event):
- if not DICOMFileDialog.createDefaultDatabase():
- return
-
- if not DICOMFileDialog.validDirectories(self.directoriesToAdd) or not DICOMFileDialog.validDirectories(self.filesToAdd):
- if not slicer.util.confirmYesNoDisplay("Import from folders with special (non-ASCII) characters in the name is not supported."
- " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"):
- self.directoriesToAdd = []
- return
+ def dragEnterEvent(self, event):
+ """
+ Actions to do when a drag enter event occurs in the Main Window.
+
+ Read up on https://doc.qt.io/qt-5.12/dnd.html#dropping
+ Input: Event (QEvent)
+ """
+ self.directoriesToAdd, self.filesToAdd = DICOMFileDialog.pathsFromMimeData(event.mimeData())
+ if self.directoriesToAdd or self.filesToAdd:
+ event.acceptProposedAction() # allows drop event to proceed
+ else:
+ event.ignore()
+
+ def dropEvent(self, event):
+ if not DICOMFileDialog.createDefaultDatabase():
+ return
+
+ if not DICOMFileDialog.validDirectories(self.directoriesToAdd) or not DICOMFileDialog.validDirectories(self.filesToAdd):
+ if not slicer.util.confirmYesNoDisplay("Import from folders with special (non-ASCII) characters in the name is not supported."
+ " It is recommended to move files into a different folder and retry. Try to import from current location anyway?"):
+ self.directoriesToAdd = []
+ return
- slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd)
- slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importFiles(self.filesToAdd)
+ slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importDirectories(self.directoriesToAdd)
+ slicer.modules.DICOMInstance.browserWidget.dicomBrowser.importFiles(self.filesToAdd)
#
@@ -560,337 +560,337 @@ def dropEvent(self, event):
#
class DICOMWidget(ScriptedLoadableModuleWidget):
- """
- Slicer module that creates the Qt GUI for interacting with DICOM
- """
+ """
+ Slicer module that creates the Qt GUI for interacting with DICOM
+ """
+
+ # sets up the widget
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # sets up the widget
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ # If DICOM module is the startup module then this widget will be shown
+ # before startup completes, therefore we need to ensure here that
+ # module discovery happens before proceeding.
+ slicer.modules.DICOMInstance.performPostModuleDiscoveryTasks()
- # If DICOM module is the startup module then this widget will be shown
- # before startup completes, therefore we need to ensure here that
- # module discovery happens before proceeding.
- slicer.modules.DICOMInstance.performPostModuleDiscoveryTasks()
+ # This module is often used in developer mode, therefore
+ # collapse reload & test section by default.
+ if hasattr(self, "reloadCollapsibleButton"):
+ self.reloadCollapsibleButton.collapsed = True
- # This module is often used in developer mode, therefore
- # collapse reload & test section by default.
- if hasattr(self, "reloadCollapsibleButton"):
- self.reloadCollapsibleButton.collapsed = True
+ self.dragAndDropEventFilter = DICOMLoadingByDragAndDropEventFilter()
- self.dragAndDropEventFilter = DICOMLoadingByDragAndDropEventFilter()
+ globals()['d'] = self
- globals()['d'] = self
+ self.testingServer = None
+ self.browserWidget = None
- self.testingServer = None
- self.browserWidget = None
+ # Load widget from .ui file (created by Qt Designer)
+ uiWidget = slicer.util.loadUI(self.resourcePath('UI/DICOM.ui'))
+ self.layout.addWidget(uiWidget)
+ self.ui = slicer.util.childWidgetVariables(uiWidget)
- # Load widget from .ui file (created by Qt Designer)
- uiWidget = slicer.util.loadUI(self.resourcePath('UI/DICOM.ui'))
- self.layout.addWidget(uiWidget)
- self.ui = slicer.util.childWidgetVariables(uiWidget)
+ self.browserWidget = DICOMLib.SlicerDICOMBrowser()
+ self.browserWidget.objectName = 'SlicerDICOMBrowser'
- self.browserWidget = DICOMLib.SlicerDICOMBrowser()
- self.browserWidget.objectName = 'SlicerDICOMBrowser'
+ slicer.modules.DICOMInstance.setBrowserWidgetInDICOMLayout(self.browserWidget)
- slicer.modules.DICOMInstance.setBrowserWidgetInDICOMLayout(self.browserWidget)
+ layoutManager = slicer.app.layoutManager()
+ if layoutManager is not None:
+ layoutManager.layoutChanged.connect(self.onLayoutChanged)
+ viewArrangement = slicer.app.layoutManager().layoutLogic().GetLayoutNode().GetViewArrangement()
+ self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
- layoutManager = slicer.app.layoutManager()
- if layoutManager is not None:
- layoutManager.layoutChanged.connect(self.onLayoutChanged)
- viewArrangement = slicer.app.layoutManager().layoutLogic().GetLayoutNode().GetViewArrangement()
- self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
-
- # connect to the 'Show DICOM Browser' button
- self.ui.showBrowserButton.connect('clicked()', self.toggleBrowserWidget)
-
- self.ui.importButton.connect('clicked()', self.importFolder)
-
- self.ui.subjectHierarchyTree.setMRMLScene(slicer.mrmlScene)
- self.ui.subjectHierarchyTree.currentItemChanged.connect(self.onCurrentItemChanged)
- self.ui.subjectHierarchyTree.currentItemModified.connect(self.onCurrentItemModified)
- self.subjectHierarchyCurrentVisibility = False
- self.ui.subjectHierarchyTree.setColumnHidden(self.ui.subjectHierarchyTree.model().idColumn, True)
-
- #
- # DICOM networking
- #
-
- self.ui.networkingFrame.collapsed = True
- self.ui.queryServerButton.connect('clicked()', self.browserWidget.dicomBrowser, "openQueryDialog()")
-
- self.ui.toggleListener.connect('toggled(bool)', self.onToggleListener)
-
- settings = qt.QSettings()
- self.ui.runListenerAtStart.checked = settingsValue('DICOM/RunListenerAtStart', False, converter=toBool)
- self.ui.runListenerAtStart.connect('toggled(bool)', self.onRunListenerAtStart)
-
- # Testing server - not exposed (used for development)
-
- self.toggleServer = qt.QPushButton("Start Testing Server")
- self.ui.networkingFrame.layout().addWidget(self.toggleServer)
- self.toggleServer.connect('clicked()', self.onToggleServer)
-
- self.verboseServer = qt.QCheckBox("Verbose")
- self.ui.networkingFrame.layout().addWidget(self.verboseServer)
-
- # advanced options - not exposed to end users
- # developers can uncomment these lines to access testing server
- self.toggleServer.hide()
- self.verboseServer.hide()
-
- #
- # Browser settings
- #
-
- self.ui.browserSettingsFrame.collapsed = True
-
- self.updateDatabaseDirectoryFromBrowser(self.browserWidget.dicomBrowser.databaseDirectory)
- # Synchronize database selection between browser and this widget
- self.ui.directoryButton.directoryChanged.connect(self.updateDatabaseDirectoryFromWidget)
- self.ui.directoryButton.sizePolicy = qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Fixed)
- self.browserWidget.dicomBrowser.databaseDirectoryChanged.connect(self.updateDatabaseDirectoryFromBrowser)
-
- self.ui.browserAutoHideCheckBox.checked = not settingsValue('DICOM/BrowserPersistent', False, converter=toBool)
- self.ui.browserAutoHideCheckBox.stateChanged.connect(self.onBrowserAutoHideStateChanged)
-
- self.ui.repairDatabaseButton.connect('clicked()', self.browserWidget.dicomBrowser, "onRepairAction()")
- self.ui.clearDatabaseButton.connect('clicked()', self.onClearDatabase)
-
- # connect to the main window's dicom button
- mw = slicer.util.mainWindow()
- if mw:
- try:
- action = slicer.util.findChildren(mw, name='LoadDICOMAction')[0]
- action.connect('triggered()', self.onOpenBrowserWidget)
- except IndexError:
- logging.error('Could not connect to the main window DICOM button')
-
- self.databaseRefreshRequestTimer = qt.QTimer()
- self.databaseRefreshRequestTimer.setSingleShot(True)
- # If not receiving new file for 2 seconds then a database update is triggered.
- self.databaseRefreshRequestTimer.setInterval(2000)
- self.databaseRefreshRequestTimer.connect('timeout()', self.requestDatabaseRefresh)
-
- #
- # DICOM Plugins selection widget
- #
- self.ui.dicomPluginsFrame.collapsed = True
- self.pluginSelector = DICOMLib.DICOMPluginSelector(self.ui.dicomPluginsFrame)
- self.ui.dicomPluginsFrame.layout().addWidget(self.pluginSelector)
- self.checkBoxByPlugins = []
-
- for pluginClass in slicer.modules.dicomPlugins:
- self.checkBox = self.pluginSelector.checkBoxByPlugin[pluginClass]
- self.checkBox.connect('stateChanged(int)', self.onPluginStateChanged)
- self.checkBoxByPlugins.append(self.checkBox)
-
- def onPluginStateChanged(self, state):
- settings = qt.QSettings()
- settings.beginWriteArray('DICOM/disabledPlugins')
-
- for key in settings.allKeys():
- settings.remove(key)
-
- plugins = self.pluginSelector.selectedPlugins()
- arrayIndex = 0
- for pluginClass in slicer.modules.dicomPlugins:
- if pluginClass not in plugins:
- settings.setArrayIndex(arrayIndex)
- settings.setValue(pluginClass, 'disabled')
- arrayIndex += 1
-
- settings.endArray()
-
- def enter(self):
- self.onOpenBrowserWidget()
- self.addListenerObservers()
- self.onListenerStateChanged()
- # While DICOM module is active, drag-and-drop always performs DICOM import
- mw = slicer.util.mainWindow()
- if mw:
- mw.installEventFilter(self.dragAndDropEventFilter)
-
- def exit(self):
- mw = slicer.util.mainWindow()
- if mw:
- mw.removeEventFilter(self.dragAndDropEventFilter)
- self.removeListenerObservers()
- self.browserWidget.close()
-
- def addListenerObservers(self):
- if not hasattr(slicer, 'dicomListener'):
- return
- if slicer.dicomListener.process is not None:
- slicer.dicomListener.process.connect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged)
- slicer.dicomListener.fileToBeAddedCallback = self.onListenerToAddFile
- slicer.dicomListener.fileAddedCallback = self.onListenerAddedFile
-
- def removeListenerObservers(self):
- if not hasattr(slicer, 'dicomListener'):
- return
- if slicer.dicomListener.process is not None:
- slicer.dicomListener.process.disconnect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged)
- slicer.dicomListener.fileToBeAddedCallback = None
- slicer.dicomListener.fileAddedCallback = None
-
- def updateGUIFromMRML(self, caller, event):
- pass
-
- def onLayoutChanged(self, viewArrangement):
- self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
-
- def onCurrentItemChanged(self, id):
- plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id)
- if not plugin:
- self.subjectHierarchyCurrentVisibility = False
- return
- self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id)
-
- def onCurrentItemModified(self, id):
- oldSubjectHierarchyCurrentVisibility = self.subjectHierarchyCurrentVisibility
-
- plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id)
- if not plugin:
- self.subjectHierarchyCurrentVisibility = False
- else:
- self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id)
-
- if self.browserWidget is None:
- return
-
- if (oldSubjectHierarchyCurrentVisibility != self.subjectHierarchyCurrentVisibility and
- self.subjectHierarchyCurrentVisibility):
- self.browserWidget.close()
-
- def toggleBrowserWidget(self):
- if self.ui.showBrowserButton.checked:
- self.onOpenBrowserWidget()
- else:
- if self.browserWidget:
+ # connect to the 'Show DICOM Browser' button
+ self.ui.showBrowserButton.connect('clicked()', self.toggleBrowserWidget)
+
+ self.ui.importButton.connect('clicked()', self.importFolder)
+
+ self.ui.subjectHierarchyTree.setMRMLScene(slicer.mrmlScene)
+ self.ui.subjectHierarchyTree.currentItemChanged.connect(self.onCurrentItemChanged)
+ self.ui.subjectHierarchyTree.currentItemModified.connect(self.onCurrentItemModified)
+ self.subjectHierarchyCurrentVisibility = False
+ self.ui.subjectHierarchyTree.setColumnHidden(self.ui.subjectHierarchyTree.model().idColumn, True)
+
+ #
+ # DICOM networking
+ #
+
+ self.ui.networkingFrame.collapsed = True
+ self.ui.queryServerButton.connect('clicked()', self.browserWidget.dicomBrowser, "openQueryDialog()")
+
+ self.ui.toggleListener.connect('toggled(bool)', self.onToggleListener)
+
+ settings = qt.QSettings()
+ self.ui.runListenerAtStart.checked = settingsValue('DICOM/RunListenerAtStart', False, converter=toBool)
+ self.ui.runListenerAtStart.connect('toggled(bool)', self.onRunListenerAtStart)
+
+ # Testing server - not exposed (used for development)
+
+ self.toggleServer = qt.QPushButton("Start Testing Server")
+ self.ui.networkingFrame.layout().addWidget(self.toggleServer)
+ self.toggleServer.connect('clicked()', self.onToggleServer)
+
+ self.verboseServer = qt.QCheckBox("Verbose")
+ self.ui.networkingFrame.layout().addWidget(self.verboseServer)
+
+ # advanced options - not exposed to end users
+ # developers can uncomment these lines to access testing server
+ self.toggleServer.hide()
+ self.verboseServer.hide()
+
+ #
+ # Browser settings
+ #
+
+ self.ui.browserSettingsFrame.collapsed = True
+
+ self.updateDatabaseDirectoryFromBrowser(self.browserWidget.dicomBrowser.databaseDirectory)
+ # Synchronize database selection between browser and this widget
+ self.ui.directoryButton.directoryChanged.connect(self.updateDatabaseDirectoryFromWidget)
+ self.ui.directoryButton.sizePolicy = qt.QSizePolicy(qt.QSizePolicy.Ignored, qt.QSizePolicy.Fixed)
+ self.browserWidget.dicomBrowser.databaseDirectoryChanged.connect(self.updateDatabaseDirectoryFromBrowser)
+
+ self.ui.browserAutoHideCheckBox.checked = not settingsValue('DICOM/BrowserPersistent', False, converter=toBool)
+ self.ui.browserAutoHideCheckBox.stateChanged.connect(self.onBrowserAutoHideStateChanged)
+
+ self.ui.repairDatabaseButton.connect('clicked()', self.browserWidget.dicomBrowser, "onRepairAction()")
+ self.ui.clearDatabaseButton.connect('clicked()', self.onClearDatabase)
+
+ # connect to the main window's dicom button
+ mw = slicer.util.mainWindow()
+ if mw:
+ try:
+ action = slicer.util.findChildren(mw, name='LoadDICOMAction')[0]
+ action.connect('triggered()', self.onOpenBrowserWidget)
+ except IndexError:
+ logging.error('Could not connect to the main window DICOM button')
+
+ self.databaseRefreshRequestTimer = qt.QTimer()
+ self.databaseRefreshRequestTimer.setSingleShot(True)
+ # If not receiving new file for 2 seconds then a database update is triggered.
+ self.databaseRefreshRequestTimer.setInterval(2000)
+ self.databaseRefreshRequestTimer.connect('timeout()', self.requestDatabaseRefresh)
+
+ #
+ # DICOM Plugins selection widget
+ #
+ self.ui.dicomPluginsFrame.collapsed = True
+ self.pluginSelector = DICOMLib.DICOMPluginSelector(self.ui.dicomPluginsFrame)
+ self.ui.dicomPluginsFrame.layout().addWidget(self.pluginSelector)
+ self.checkBoxByPlugins = []
+
+ for pluginClass in slicer.modules.dicomPlugins:
+ self.checkBox = self.pluginSelector.checkBoxByPlugin[pluginClass]
+ self.checkBox.connect('stateChanged(int)', self.onPluginStateChanged)
+ self.checkBoxByPlugins.append(self.checkBox)
+
+ def onPluginStateChanged(self, state):
+ settings = qt.QSettings()
+ settings.beginWriteArray('DICOM/disabledPlugins')
+
+ for key in settings.allKeys():
+ settings.remove(key)
+
+ plugins = self.pluginSelector.selectedPlugins()
+ arrayIndex = 0
+ for pluginClass in slicer.modules.dicomPlugins:
+ if pluginClass not in plugins:
+ settings.setArrayIndex(arrayIndex)
+ settings.setValue(pluginClass, 'disabled')
+ arrayIndex += 1
+
+ settings.endArray()
+
+ def enter(self):
+ self.onOpenBrowserWidget()
+ self.addListenerObservers()
+ self.onListenerStateChanged()
+ # While DICOM module is active, drag-and-drop always performs DICOM import
+ mw = slicer.util.mainWindow()
+ if mw:
+ mw.installEventFilter(self.dragAndDropEventFilter)
+
+ def exit(self):
+ mw = slicer.util.mainWindow()
+ if mw:
+ mw.removeEventFilter(self.dragAndDropEventFilter)
+ self.removeListenerObservers()
self.browserWidget.close()
- def importFolder(self):
- if not DICOMFileDialog.createDefaultDatabase():
- return
- self.browserWidget.dicomBrowser.openImportDialog()
-
- def onOpenBrowserWidget(self):
- slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
-
- def onToggleListener(self, toggled):
- if hasattr(slicer, 'dicomListener'):
- self.removeListenerObservers()
- slicer.modules.DICOMInstance.stopListener()
- if toggled:
- slicer.modules.DICOMInstance.startListener()
- self.addListenerObservers()
- self.onListenerStateChanged()
-
- def onListenerStateChanged(self, newState=None):
- """ Called when the indexer process state changes
- so we can provide feedback to the user
- """
- if hasattr(slicer, 'dicomListener') and slicer.dicomListener.process is not None:
- newState = slicer.dicomListener.process.state()
- else:
- newState = 0
-
- if newState == 0:
- self.ui.listenerStateLabel.text = "not started"
- wasBlocked = self.ui.toggleListener.blockSignals(True)
- self.ui.toggleListener.checked = False
- self.ui.toggleListener.blockSignals(wasBlocked)
- if hasattr(slicer.modules, 'DICOMInstance'): # custom applications may not have the standard DICOM module
- slicer.modules.DICOMInstance.stopListener()
- if newState == 1:
- self.ui.listenerStateLabel.text = "starting"
- if newState == 2:
- port = str(slicer.dicomListener.port) if hasattr(slicer, 'dicomListener') else "unknown"
- self.ui.listenerStateLabel.text = "running at port " + port
- self.ui.toggleListener.checked = True
-
- def onListenerToAddFile(self):
- """ Called when the indexer is about to add a file to the database.
- Works around issue where ctkDICOMModel has open queries that keep the
- database locked.
- """
- pass
+ def addListenerObservers(self):
+ if not hasattr(slicer, 'dicomListener'):
+ return
+ if slicer.dicomListener.process is not None:
+ slicer.dicomListener.process.connect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged)
+ slicer.dicomListener.fileToBeAddedCallback = self.onListenerToAddFile
+ slicer.dicomListener.fileAddedCallback = self.onListenerAddedFile
+
+ def removeListenerObservers(self):
+ if not hasattr(slicer, 'dicomListener'):
+ return
+ if slicer.dicomListener.process is not None:
+ slicer.dicomListener.process.disconnect('stateChanged(QProcess::ProcessState)', self.onListenerStateChanged)
+ slicer.dicomListener.fileToBeAddedCallback = None
+ slicer.dicomListener.fileAddedCallback = None
+
+ def updateGUIFromMRML(self, caller, event):
+ pass
+
+ def onLayoutChanged(self, viewArrangement):
+ self.ui.showBrowserButton.checked = (viewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
+
+ def onCurrentItemChanged(self, id):
+ plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id)
+ if not plugin:
+ self.subjectHierarchyCurrentVisibility = False
+ return
+ self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id)
+
+ def onCurrentItemModified(self, id):
+ oldSubjectHierarchyCurrentVisibility = self.subjectHierarchyCurrentVisibility
+
+ plugin = slicer.qSlicerSubjectHierarchyPluginHandler.instance().getOwnerPluginForSubjectHierarchyItem(id)
+ if not plugin:
+ self.subjectHierarchyCurrentVisibility = False
+ else:
+ self.subjectHierarchyCurrentVisibility = plugin.getDisplayVisibility(id)
- def onListenerAddedFile(self):
- """Called after the listener has added a file.
- Restore and refresh the app model
- """
- newFile = slicer.dicomListener.lastFileAdded
- if newFile:
- slicer.util.showStatusMessage("Received DICOM file: %s" % newFile, 1000)
- self.databaseRefreshRequestTimer.start()
-
- def requestDatabaseRefresh(self):
- logging.debug("Database refresh is requested")
- if slicer.dicomDatabase.isOpen:
- slicer.dicomDatabase.databaseChanged()
-
- def onToggleServer(self):
- if self.testingServer and self.testingServer.qrRunning():
- self.testingServer.stop()
- self.toggleServer.text = "Start Testing Server"
- else:
- #
- # create&configure the testingServer if needed, start the server, and populate it
- #
- if not self.testingServer:
- # find the helper executables (only works on build trees
- # with standard naming conventions)
- self.exeDir = slicer.app.slicerHome
- if slicer.app.intDir:
- self.exeDir = self.exeDir + '/' + slicer.app.intDir
- self.exeDir = self.exeDir + '/../CTK-build/DCMTK-build'
-
- # TODO: deal with Debug/RelWithDebInfo on windows
-
- # set up temp dir
- tmpDir = slicer.app.temporaryPath
- if not os.path.exists(tmpDir):
- os.mkdir(tmpDir)
- self.tmpDir = tmpDir + '/DICOM'
- if not os.path.exists(self.tmpDir):
- os.mkdir(self.tmpDir)
- self.testingServer = DICOMLib.DICOMTestingQRServer(exeDir=self.exeDir, tmpDir=self.tmpDir)
-
- # look for the sample data to load (only works on build trees
- # with standard naming conventions)
- self.dataDir = slicer.app.slicerHome + '/../../Slicer4/Testing/Data/Input/CTHeadAxialDicom'
- files = glob.glob(self.dataDir + '/*.dcm')
-
- # now start the server
- self.testingServer.start(verbose=self.verboseServer.checked, initialFiles=files)
- # self.toggleServer.text = "Stop Testing Server"
-
- def onRunListenerAtStart(self, toggled):
- settings = qt.QSettings()
- settings.setValue('DICOM/RunListenerAtStart', toggled)
-
- def updateDatabaseDirectoryFromWidget(self, databaseDirectory):
- self.browserWidget.dicomBrowser.databaseDirectory = databaseDirectory
-
- def updateDatabaseDirectoryFromBrowser(self, databaseDirectory):
- wasBlocked = self.ui.directoryButton.blockSignals(True)
- self.ui.directoryButton.directory = databaseDirectory
- self.ui.directoryButton.blockSignals(wasBlocked)
-
- def onBrowserAutoHideStateChanged(self, autoHideState):
- if self.browserWidget:
- self.browserWidget.setBrowserPersistence(autoHideState != qt.Qt.Checked)
-
- def onClearDatabase(self):
- patientIds = slicer.dicomDatabase.patients()
- if len(patientIds) == 0:
- slicer.util.infoDisplay("DICOM database is already empty.")
- elif not slicer.util.confirmYesNoDisplay(
- 'Are you sure you want to delete all data and files copied into the database (%d patients)?' % len(patientIds),
- windowTitle='Clear entire DICOM database'):
- return
- slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
- DICOMLib.clearDatabase(slicer.dicomDatabase)
- slicer.app.restoreOverrideCursor()
+ if self.browserWidget is None:
+ return
+
+ if (oldSubjectHierarchyCurrentVisibility != self.subjectHierarchyCurrentVisibility and
+ self.subjectHierarchyCurrentVisibility):
+ self.browserWidget.close()
+
+ def toggleBrowserWidget(self):
+ if self.ui.showBrowserButton.checked:
+ self.onOpenBrowserWidget()
+ else:
+ if self.browserWidget:
+ self.browserWidget.close()
+
+ def importFolder(self):
+ if not DICOMFileDialog.createDefaultDatabase():
+ return
+ self.browserWidget.dicomBrowser.openImportDialog()
+
+ def onOpenBrowserWidget(self):
+ slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView)
+
+ def onToggleListener(self, toggled):
+ if hasattr(slicer, 'dicomListener'):
+ self.removeListenerObservers()
+ slicer.modules.DICOMInstance.stopListener()
+ if toggled:
+ slicer.modules.DICOMInstance.startListener()
+ self.addListenerObservers()
+ self.onListenerStateChanged()
+
+ def onListenerStateChanged(self, newState=None):
+ """ Called when the indexer process state changes
+ so we can provide feedback to the user
+ """
+ if hasattr(slicer, 'dicomListener') and slicer.dicomListener.process is not None:
+ newState = slicer.dicomListener.process.state()
+ else:
+ newState = 0
+
+ if newState == 0:
+ self.ui.listenerStateLabel.text = "not started"
+ wasBlocked = self.ui.toggleListener.blockSignals(True)
+ self.ui.toggleListener.checked = False
+ self.ui.toggleListener.blockSignals(wasBlocked)
+ if hasattr(slicer.modules, 'DICOMInstance'): # custom applications may not have the standard DICOM module
+ slicer.modules.DICOMInstance.stopListener()
+ if newState == 1:
+ self.ui.listenerStateLabel.text = "starting"
+ if newState == 2:
+ port = str(slicer.dicomListener.port) if hasattr(slicer, 'dicomListener') else "unknown"
+ self.ui.listenerStateLabel.text = "running at port " + port
+ self.ui.toggleListener.checked = True
+
+ def onListenerToAddFile(self):
+ """ Called when the indexer is about to add a file to the database.
+ Works around issue where ctkDICOMModel has open queries that keep the
+ database locked.
+ """
+ pass
+
+ def onListenerAddedFile(self):
+ """Called after the listener has added a file.
+ Restore and refresh the app model
+ """
+ newFile = slicer.dicomListener.lastFileAdded
+ if newFile:
+ slicer.util.showStatusMessage("Received DICOM file: %s" % newFile, 1000)
+ self.databaseRefreshRequestTimer.start()
+
+ def requestDatabaseRefresh(self):
+ logging.debug("Database refresh is requested")
+ if slicer.dicomDatabase.isOpen:
+ slicer.dicomDatabase.databaseChanged()
+
+ def onToggleServer(self):
+ if self.testingServer and self.testingServer.qrRunning():
+ self.testingServer.stop()
+ self.toggleServer.text = "Start Testing Server"
+ else:
+ #
+ # create&configure the testingServer if needed, start the server, and populate it
+ #
+ if not self.testingServer:
+ # find the helper executables (only works on build trees
+ # with standard naming conventions)
+ self.exeDir = slicer.app.slicerHome
+ if slicer.app.intDir:
+ self.exeDir = self.exeDir + '/' + slicer.app.intDir
+ self.exeDir = self.exeDir + '/../CTK-build/DCMTK-build'
+
+ # TODO: deal with Debug/RelWithDebInfo on windows
+
+ # set up temp dir
+ tmpDir = slicer.app.temporaryPath
+ if not os.path.exists(tmpDir):
+ os.mkdir(tmpDir)
+ self.tmpDir = tmpDir + '/DICOM'
+ if not os.path.exists(self.tmpDir):
+ os.mkdir(self.tmpDir)
+ self.testingServer = DICOMLib.DICOMTestingQRServer(exeDir=self.exeDir, tmpDir=self.tmpDir)
+
+ # look for the sample data to load (only works on build trees
+ # with standard naming conventions)
+ self.dataDir = slicer.app.slicerHome + '/../../Slicer4/Testing/Data/Input/CTHeadAxialDicom'
+ files = glob.glob(self.dataDir + '/*.dcm')
+
+ # now start the server
+ self.testingServer.start(verbose=self.verboseServer.checked, initialFiles=files)
+ # self.toggleServer.text = "Stop Testing Server"
+
+ def onRunListenerAtStart(self, toggled):
+ settings = qt.QSettings()
+ settings.setValue('DICOM/RunListenerAtStart', toggled)
+
+ def updateDatabaseDirectoryFromWidget(self, databaseDirectory):
+ self.browserWidget.dicomBrowser.databaseDirectory = databaseDirectory
+
+ def updateDatabaseDirectoryFromBrowser(self, databaseDirectory):
+ wasBlocked = self.ui.directoryButton.blockSignals(True)
+ self.ui.directoryButton.directory = databaseDirectory
+ self.ui.directoryButton.blockSignals(wasBlocked)
+
+ def onBrowserAutoHideStateChanged(self, autoHideState):
+ if self.browserWidget:
+ self.browserWidget.setBrowserPersistence(autoHideState != qt.Qt.Checked)
+
+ def onClearDatabase(self):
+ patientIds = slicer.dicomDatabase.patients()
+ if len(patientIds) == 0:
+ slicer.util.infoDisplay("DICOM database is already empty.")
+ elif not slicer.util.confirmYesNoDisplay(
+ 'Are you sure you want to delete all data and files copied into the database (%d patients)?' % len(patientIds),
+ windowTitle='Clear entire DICOM database'):
+ return
+ slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
+ DICOMLib.clearDatabase(slicer.dicomDatabase)
+ slicer.app.restoreOverrideCursor()
diff --git a/Modules/Scripted/DICOMLib/DICOMBrowser.py b/Modules/Scripted/DICOMLib/DICOMBrowser.py
index d68fd12a7a5..c91ae5f0baf 100644
--- a/Modules/Scripted/DICOMLib/DICOMBrowser.py
+++ b/Modules/Scripted/DICOMLib/DICOMBrowser.py
@@ -30,697 +30,697 @@
class SlicerDICOMBrowser(VTKObservationMixin, qt.QWidget):
- """Implement the Qt window showing details and possible
- operations to perform on the selected dicom list item.
- This is a helper used in the DICOMWidget class.
- """
-
- closed = qt.Signal() # Invoked when the dicom widget is closed using the close method
-
- def __init__(self, dicomBrowser=None, parent="mainWindow"):
- VTKObservationMixin.__init__(self)
- qt.QWidget.__init__(self, slicer.util.mainWindow() if parent == "mainWindow" else parent)
-
- self.pluginInstances = {}
- self.fileLists = []
- self.extensionCheckPending = False
-
- self.settings = qt.QSettings()
-
- self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase()
-
- self.browserPersistent = settingsValue('DICOM/BrowserPersistent', False, converter=toBool)
- self.advancedView = settingsValue('DICOM/advancedView', 0, converter=int)
- self.horizontalTables = settingsValue('DICOM/horizontalTables', 0, converter=int)
-
- self.setup()
-
- self.dicomBrowser.connect('directoryImported()', self.onDirectoryImported)
- self.dicomBrowser.connect('sendRequested(QStringList)', self.onSend)
-
- # Load when double-clicked on an item in the browser
- self.dicomBrowser.dicomTableManager().connect('patientsDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
- self.dicomBrowser.dicomTableManager().connect('studiesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
- self.dicomBrowser.dicomTableManager().connect('seriesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
-
- def open(self):
- self.show()
-
- def close(self):
- self.hide()
- self.closed.emit()
-
- def onSend(self, fileList):
- if len(fileList):
- sendDialog = DICOMLib.DICOMSendDialog(fileList, self)
-
- def setup(self, showPreview=False):
- """
- main window is a frame with widgets from the app
- widget repacked into it along with slicer-specific
- extra widgets
+ """Implement the Qt window showing details and possible
+ operations to perform on the selected dicom list item.
+ This is a helper used in the DICOMWidget class.
"""
- self.setWindowTitle('DICOM Browser')
- self.setLayout(qt.QVBoxLayout())
-
- self.dicomBrowser.databaseDirectorySelectorVisible = False
- self.dicomBrowser.toolbarVisible = False
- self.dicomBrowser.sendActionVisible = True
- self.dicomBrowser.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey
- self.dicomBrowser.dicomTableManager().dynamicTableLayout = False
- horizontal = self.settings.setValue('DICOM/horizontalTables', 0)
- self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
- self.layout().addWidget(self.dicomBrowser)
-
- self.userFrame = qt.QWidget()
- self.preview = qt.QWidget()
-
- #
- # preview related column
- #
- self.previewLayout = qt.QVBoxLayout()
- if showPreview:
- self.previewLayout.addWidget(self.preview)
- else:
- self.preview.hide()
-
- #
- # action related column (interacting with slicer)
- #
- self.loadableTableFrame = qt.QWidget()
- self.loadableTableFrame.setMaximumHeight(200)
- self.loadableTableLayout = qt.QVBoxLayout(self.loadableTableFrame)
- self.layout().addWidget(self.loadableTableFrame)
-
- self.loadableTableLayout.addWidget(self.userFrame)
- self.userFrame.hide()
-
- self.loadableTable = DICOMLoadableTable(self.userFrame)
- self.loadableTable.itemChanged.connect(self.onLoadableTableItemChanged)
-
- #
- # button row for action column
- #
- self.actionButtonsFrame = qt.QWidget()
- self.actionButtonsFrame.setMaximumHeight(40)
- self.actionButtonsFrame.objectName = 'ActionButtonsFrame'
- self.layout().addWidget(self.actionButtonsFrame)
-
- self.actionButtonLayout = qt.QHBoxLayout()
- self.actionButtonsFrame.setLayout(self.actionButtonLayout)
-
- self.uncheckAllButton = qt.QPushButton('Uncheck All')
- self.actionButtonLayout.addWidget(self.uncheckAllButton)
- self.uncheckAllButton.connect('clicked()', self.uncheckAllLoadables)
-
- self.actionButtonLayout.addStretch(0.05)
-
- self.examineButton = qt.QPushButton('Examine')
- self.examineButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- self.actionButtonLayout.addWidget(self.examineButton)
- self.examineButton.enabled = False
- self.examineButton.connect('clicked()', self.examineForLoading)
-
- self.loadButton = qt.QPushButton('Load')
- self.loadButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- self.loadButton.toolTip = 'Load selected items into the scene'
- self.actionButtonLayout.addWidget(self.loadButton)
- self.loadButton.connect('clicked()', self.loadCheckedLoadables)
-
- self.actionButtonLayout.addStretch(0.05)
-
- self.advancedViewButton = qt.QCheckBox('Advanced')
- self.advancedViewButton.objectName = 'AdvancedViewCheckBox'
- self.actionButtonLayout.addWidget(self.advancedViewButton)
- self.advancedViewButton.checked = self.advancedView
- self.advancedViewButton.toggled.connect(self.onAdvancedViewButton)
-
- if self.advancedView:
- self.loadableTableFrame.visible = True
- else:
- self.loadableTableFrame.visible = False
- self.examineButton.visible = False
- self.uncheckAllButton.visible = False
-
- #
- # Series selection
- #
- self.dicomBrowser.dicomTableManager().connect('seriesSelectionChanged(QStringList)', self.onSeriesSelected)
-
- #
- # Loadable table widget (advanced)
- # DICOM Plugins selection widget is moved to module panel
- #
- self.loadableTableLayout.addWidget(self.loadableTable)
- self.updateButtonStates()
-
- def updateButtonStates(self):
- if self.advancedView:
- # self.loadButton.enabled = loadEnabled = loadEnabled or loadablesByPlugin[plugin] != []
- loadablesChecked = self.loadableTable.getNumberOfCheckedItems() > 0
- self.loadButton.enabled = loadablesChecked
- self.examineButton.enabled = len(self.fileLists) != 0
- self.uncheckAllButton.enabled = loadablesChecked
- else:
- # seriesSelected = self.dicomBrowser.dicomTableManager().seriesTable().tableView().selectedIndexes()
- self.loadButton.enabled = self.fileLists
-
- def onDirectoryImported(self):
- """The dicom browser will emit multiple directoryImported
- signals during the same operation, so we collapse them
- into a single check for compatible extensions."""
- if not hasattr(slicer.app, 'extensionsManagerModel'):
- # Slicer may not be built with extensions manager support
- return
- if not self.extensionCheckPending:
- self.extensionCheckPending = True
-
- def timerCallback():
- # Prompting for extension may be undesirable in custom applications.
- # DICOM/PromptForExtensions key can be used to disable this feature.
- promptForExtensionsEnabled = settingsValue('DICOM/PromptForExtensions', True, converter=toBool)
- if promptForExtensionsEnabled:
- self.promptForExtensions()
- self.extensionCheckPending = False
+ closed = qt.Signal() # Invoked when the dicom widget is closed using the close method
- qt.QTimer.singleShot(0, timerCallback)
-
- def promptForExtensions(self):
- extensionsToOffer = self.checkForExtensions()
- if len(extensionsToOffer) != 0:
- if len(extensionsToOffer) == 1:
- pluralOrNot = " is"
- else:
- pluralOrNot = "s are"
- message = "The following data type%s in your database:\n\n" % pluralOrNot
- displayedTypeDescriptions = []
- for extension in extensionsToOffer:
- typeDescription = extension['typeDescription']
- if not typeDescription in displayedTypeDescriptions:
- # only display each data type only once
- message += ' ' + typeDescription + '\n'
- displayedTypeDescriptions.append(typeDescription)
- message += "\nThe following extension%s not installed, but may help you work with this data:\n\n" % pluralOrNot
- displayedExtensionNames = []
- for extension in extensionsToOffer:
- extensionName = extension['name']
- if not extensionName in displayedExtensionNames:
- # only display each extension name only once
- message += ' ' + extensionName + '\n'
- displayedExtensionNames.append(extensionName)
- message += "\nYou can install extensions using the Extensions Manager option from the View menu."
- slicer.util.infoDisplay(message, parent=self, windowTitle='DICOM')
-
- def checkForExtensions(self):
- """Check to see if there
- are any registered extensions that might be available to
- help the user work with data in the database.
-
- 1) load extension json description
- 2) load info for each series
- 3) check if data matches
-
- then return matches
-
- See
- https://mantisarchive.slicer.org/view.php?id=4146
- """
+ def __init__(self, dicomBrowser=None, parent="mainWindow"):
+ VTKObservationMixin.__init__(self)
+ qt.QWidget.__init__(self, slicer.util.mainWindow() if parent == "mainWindow" else parent)
- # 1 - load json
- import logging, os, json
- logging.info('Imported a DICOM directory, checking for extensions')
- modulePath = os.path.dirname(slicer.modules.dicom.path)
- extensionDescriptorPath = os.path.join(modulePath, 'DICOMExtensions.json')
- try:
- with open(extensionDescriptorPath) as extensionDescriptorFP:
- extensionDescriptor = extensionDescriptorFP.read()
- dicomExtensions = json.loads(extensionDescriptor)
- except:
- logging.error('Cannot access DICOMExtensions.json file')
- return
-
- # 2 - get series info
- # - iterate though metadata - should be fast even with large database
- # - the fileValue call checks the tag cache so it's fast
- modalityTag = "0008,0060"
- sopClassUIDTag = "0008,0016"
- sopClassUIDs = set()
- modalities = set()
- for patient in slicer.dicomDatabase.patients():
- for study in slicer.dicomDatabase.studiesForPatient(patient):
- for series in slicer.dicomDatabase.seriesForStudy(study):
- instance0 = slicer.dicomDatabase.filesForSeries(series, 1)[0]
- modality = slicer.dicomDatabase.fileValue(instance0, modalityTag)
- sopClassUID = slicer.dicomDatabase.fileValue(instance0, sopClassUIDTag)
- modalities.add(modality)
- sopClassUIDs.add(sopClassUID)
-
- # 3 - check if data matches
- extensionsManagerModel = slicer.app.extensionsManagerModel()
- installedExtensions = extensionsManagerModel.installedExtensions
- extensionsToOffer = []
- for extension in dicomExtensions['extensions']:
- extensionName = extension['name']
- if extensionName not in installedExtensions:
- tagValues = extension['tagValues']
- if 'Modality' in tagValues:
- for modality in tagValues['Modality']:
- if modality in modalities:
- extensionsToOffer.append(extension)
- if 'SOPClassUID' in tagValues:
- for sopClassUID in tagValues['SOPClassUID']:
- if sopClassUID in sopClassUIDs:
- extensionsToOffer.append(extension)
- return extensionsToOffer
-
- def setBrowserPersistence(self, state):
- self.browserPersistent = state
- self.settings.setValue('DICOM/BrowserPersistent', bool(self.browserPersistent))
-
- def onAdvancedViewButton(self, checked):
- self.advancedView = checked
- advancedWidgets = [self.loadableTableFrame, self.examineButton, self.uncheckAllButton]
- for widget in advancedWidgets:
- widget.visible = self.advancedView
- self.updateButtonStates()
-
- self.settings.setValue('DICOM/advancedView', int(self.advancedView))
-
- def onHorizontalViewCheckBox(self):
- horizontal = self.horizontalViewCheckBox.checked
- self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
- self.settings.setValue('DICOM/horizontalTables', int(horizontal))
-
- def onSeriesSelected(self, seriesUIDList):
- self.loadableTable.setLoadables([])
- self.fileLists = self.getFileListsForRole(seriesUIDList, "SeriesUIDList")
- self.updateButtonStates()
-
- def getFileListsForRole(self, uidArgument, role):
- fileLists = []
- if role == "Series":
- fileLists.append(slicer.dicomDatabase.filesForSeries(uidArgument))
- if role == "SeriesUIDList":
- for uid in uidArgument:
- uid = uid.replace("'", "")
- fileLists.append(slicer.dicomDatabase.filesForSeries(uid))
- if role == "Study":
- series = slicer.dicomDatabase.seriesForStudy(uidArgument)
- for serie in series:
- fileLists.append(slicer.dicomDatabase.filesForSeries(serie))
- if role == "Patient":
- studies = slicer.dicomDatabase.studiesForPatient(uidArgument)
- for study in studies:
- series = slicer.dicomDatabase.seriesForStudy(study)
- for serie in series:
- fileList = slicer.dicomDatabase.filesForSeries(serie)
- fileLists.append(fileList)
- return fileLists
-
- def uncheckAllLoadables(self):
- self.loadableTable.uncheckAll()
-
- def onLoadableTableItemChanged(self, item):
- self.updateButtonStates()
-
- def examineForLoading(self):
- """For selected plugins, give user the option
- of what to load"""
-
- (self.loadablesByPlugin, loadEnabled) = self.getLoadablesFromFileLists(self.fileLists)
- DICOMLib.selectHighestConfidenceLoadables(self.loadablesByPlugin)
- self.loadableTable.setLoadables(self.loadablesByPlugin)
- self.updateButtonStates()
-
- def getLoadablesFromFileLists(self, fileLists):
- """Take list of file lists, return loadables by plugin dictionary
- """
+ self.pluginInstances = {}
+ self.fileLists = []
+ self.extensionCheckPending = False
- loadablesByPlugin = {}
- loadEnabled = False
-
- # Get selected plugins from application settings
- # Settings are filled in DICOMWidget using DICOMPluginSelector
- settings = qt.QSettings()
- selectedPlugins = []
- if settings.contains('DICOM/disabledPlugins/size'):
- size = settings.beginReadArray('DICOM/disabledPlugins')
- disabledPlugins = []
-
- for i in range(size):
- settings.setArrayIndex(i)
- disabledPlugins.append(str(settings.allKeys()[0]))
- settings.endArray()
-
- for pluginClass in slicer.modules.dicomPlugins:
- if pluginClass not in disabledPlugins:
- selectedPlugins.append(pluginClass)
- else:
- # All DICOM plugins would be enabled by default
- for pluginClass in slicer.modules.dicomPlugins:
- selectedPlugins.append(pluginClass)
-
- allFileCount = missingFileCount = 0
- for fileList in fileLists:
- for filePath in fileList:
- allFileCount += 1
- if not os.path.exists(filePath):
- missingFileCount += 1
-
- messages = []
- if missingFileCount > 0:
- messages.append("Warning: %d of %d selected files listed in the database cannot be found on disk." % (missingFileCount, allFileCount))
-
- if missingFileCount < allFileCount:
- progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100)
-
- def progressCallback(progressDialog, progressLabel, progressValue):
- progressDialog.labelText = '\nChecking %s' % progressLabel
- slicer.app.processEvents()
- progressDialog.setValue(progressValue)
- slicer.app.processEvents()
- cancelled = progressDialog.wasCanceled
- return cancelled
-
- loadablesByPlugin, loadEnabled = DICOMLib.getLoadablesFromFileLists(fileLists, selectedPlugins, messages,
- lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue),
- self.pluginInstances)
-
- progressDialog.close()
-
- if messages:
- slicer.util.warningDisplay("Warning: %s\n\nSee python console for error message." % ' '.join(messages),
- windowTitle="DICOM", parent=self)
-
- return loadablesByPlugin, loadEnabled
-
- def isFileListInCheckedLoadables(self, fileList):
- for plugin in self.loadablesByPlugin:
- for loadable in self.loadablesByPlugin[plugin]:
- if len(loadable.files) != len(fileList) or len(loadable.files) == 0:
- continue
- inputFileListCopy = copy.deepcopy(fileList)
- loadableFileListCopy = copy.deepcopy(loadable.files)
+ self.settings = qt.QSettings()
+
+ self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase()
+
+ self.browserPersistent = settingsValue('DICOM/BrowserPersistent', False, converter=toBool)
+ self.advancedView = settingsValue('DICOM/advancedView', 0, converter=int)
+ self.horizontalTables = settingsValue('DICOM/horizontalTables', 0, converter=int)
+
+ self.setup()
+
+ self.dicomBrowser.connect('directoryImported()', self.onDirectoryImported)
+ self.dicomBrowser.connect('sendRequested(QStringList)', self.onSend)
+
+ # Load when double-clicked on an item in the browser
+ self.dicomBrowser.dicomTableManager().connect('patientsDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
+ self.dicomBrowser.dicomTableManager().connect('studiesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
+ self.dicomBrowser.dicomTableManager().connect('seriesDoubleClicked(QModelIndex)', self.patientStudySeriesDoubleClicked)
+
+ def open(self):
+ self.show()
+
+ def close(self):
+ self.hide()
+ self.closed.emit()
+
+ def onSend(self, fileList):
+ if len(fileList):
+ sendDialog = DICOMLib.DICOMSendDialog(fileList, self)
+
+ def setup(self, showPreview=False):
+ """
+ main window is a frame with widgets from the app
+ widget repacked into it along with slicer-specific
+ extra widgets
+ """
+
+ self.setWindowTitle('DICOM Browser')
+ self.setLayout(qt.QVBoxLayout())
+
+ self.dicomBrowser.databaseDirectorySelectorVisible = False
+ self.dicomBrowser.toolbarVisible = False
+ self.dicomBrowser.sendActionVisible = True
+ self.dicomBrowser.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey
+ self.dicomBrowser.dicomTableManager().dynamicTableLayout = False
+ horizontal = self.settings.setValue('DICOM/horizontalTables', 0)
+ self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
+ self.layout().addWidget(self.dicomBrowser)
+
+ self.userFrame = qt.QWidget()
+ self.preview = qt.QWidget()
+
+ #
+ # preview related column
+ #
+ self.previewLayout = qt.QVBoxLayout()
+ if showPreview:
+ self.previewLayout.addWidget(self.preview)
+ else:
+ self.preview.hide()
+
+ #
+ # action related column (interacting with slicer)
+ #
+ self.loadableTableFrame = qt.QWidget()
+ self.loadableTableFrame.setMaximumHeight(200)
+ self.loadableTableLayout = qt.QVBoxLayout(self.loadableTableFrame)
+ self.layout().addWidget(self.loadableTableFrame)
+
+ self.loadableTableLayout.addWidget(self.userFrame)
+ self.userFrame.hide()
+
+ self.loadableTable = DICOMLoadableTable(self.userFrame)
+ self.loadableTable.itemChanged.connect(self.onLoadableTableItemChanged)
+
+ #
+ # button row for action column
+ #
+ self.actionButtonsFrame = qt.QWidget()
+ self.actionButtonsFrame.setMaximumHeight(40)
+ self.actionButtonsFrame.objectName = 'ActionButtonsFrame'
+ self.layout().addWidget(self.actionButtonsFrame)
+
+ self.actionButtonLayout = qt.QHBoxLayout()
+ self.actionButtonsFrame.setLayout(self.actionButtonLayout)
+
+ self.uncheckAllButton = qt.QPushButton('Uncheck All')
+ self.actionButtonLayout.addWidget(self.uncheckAllButton)
+ self.uncheckAllButton.connect('clicked()', self.uncheckAllLoadables)
+
+ self.actionButtonLayout.addStretch(0.05)
+
+ self.examineButton = qt.QPushButton('Examine')
+ self.examineButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ self.actionButtonLayout.addWidget(self.examineButton)
+ self.examineButton.enabled = False
+ self.examineButton.connect('clicked()', self.examineForLoading)
+
+ self.loadButton = qt.QPushButton('Load')
+ self.loadButton.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ self.loadButton.toolTip = 'Load selected items into the scene'
+ self.actionButtonLayout.addWidget(self.loadButton)
+ self.loadButton.connect('clicked()', self.loadCheckedLoadables)
+
+ self.actionButtonLayout.addStretch(0.05)
+
+ self.advancedViewButton = qt.QCheckBox('Advanced')
+ self.advancedViewButton.objectName = 'AdvancedViewCheckBox'
+ self.actionButtonLayout.addWidget(self.advancedViewButton)
+ self.advancedViewButton.checked = self.advancedView
+ self.advancedViewButton.toggled.connect(self.onAdvancedViewButton)
+
+ if self.advancedView:
+ self.loadableTableFrame.visible = True
+ else:
+ self.loadableTableFrame.visible = False
+ self.examineButton.visible = False
+ self.uncheckAllButton.visible = False
+
+ #
+ # Series selection
+ #
+ self.dicomBrowser.dicomTableManager().connect('seriesSelectionChanged(QStringList)', self.onSeriesSelected)
+
+ #
+ # Loadable table widget (advanced)
+ # DICOM Plugins selection widget is moved to module panel
+ #
+ self.loadableTableLayout.addWidget(self.loadableTable)
+ self.updateButtonStates()
+
+ def updateButtonStates(self):
+ if self.advancedView:
+ # self.loadButton.enabled = loadEnabled = loadEnabled or loadablesByPlugin[plugin] != []
+ loadablesChecked = self.loadableTable.getNumberOfCheckedItems() > 0
+ self.loadButton.enabled = loadablesChecked
+ self.examineButton.enabled = len(self.fileLists) != 0
+ self.uncheckAllButton.enabled = loadablesChecked
+ else:
+ # seriesSelected = self.dicomBrowser.dicomTableManager().seriesTable().tableView().selectedIndexes()
+ self.loadButton.enabled = self.fileLists
+
+ def onDirectoryImported(self):
+ """The dicom browser will emit multiple directoryImported
+ signals during the same operation, so we collapse them
+ into a single check for compatible extensions."""
+ if not hasattr(slicer.app, 'extensionsManagerModel'):
+ # Slicer may not be built with extensions manager support
+ return
+ if not self.extensionCheckPending:
+ self.extensionCheckPending = True
+
+ def timerCallback():
+ # Prompting for extension may be undesirable in custom applications.
+ # DICOM/PromptForExtensions key can be used to disable this feature.
+ promptForExtensionsEnabled = settingsValue('DICOM/PromptForExtensions', True, converter=toBool)
+ if promptForExtensionsEnabled:
+ self.promptForExtensions()
+ self.extensionCheckPending = False
+
+ qt.QTimer.singleShot(0, timerCallback)
+
+ def promptForExtensions(self):
+ extensionsToOffer = self.checkForExtensions()
+ if len(extensionsToOffer) != 0:
+ if len(extensionsToOffer) == 1:
+ pluralOrNot = " is"
+ else:
+ pluralOrNot = "s are"
+ message = "The following data type%s in your database:\n\n" % pluralOrNot
+ displayedTypeDescriptions = []
+ for extension in extensionsToOffer:
+ typeDescription = extension['typeDescription']
+ if not typeDescription in displayedTypeDescriptions:
+ # only display each data type only once
+ message += ' ' + typeDescription + '\n'
+ displayedTypeDescriptions.append(typeDescription)
+ message += "\nThe following extension%s not installed, but may help you work with this data:\n\n" % pluralOrNot
+ displayedExtensionNames = []
+ for extension in extensionsToOffer:
+ extensionName = extension['name']
+ if not extensionName in displayedExtensionNames:
+ # only display each extension name only once
+ message += ' ' + extensionName + '\n'
+ displayedExtensionNames.append(extensionName)
+ message += "\nYou can install extensions using the Extensions Manager option from the View menu."
+ slicer.util.infoDisplay(message, parent=self, windowTitle='DICOM')
+
+ def checkForExtensions(self):
+ """Check to see if there
+ are any registered extensions that might be available to
+ help the user work with data in the database.
+
+ 1) load extension json description
+ 2) load info for each series
+ 3) check if data matches
+
+ then return matches
+
+ See
+ https://mantisarchive.slicer.org/view.php?id=4146
+ """
+
+ # 1 - load json
+ import logging, os, json
+ logging.info('Imported a DICOM directory, checking for extensions')
+ modulePath = os.path.dirname(slicer.modules.dicom.path)
+ extensionDescriptorPath = os.path.join(modulePath, 'DICOMExtensions.json')
try:
- inputFileListCopy.sort()
- loadableFileListCopy.sort()
- except Exception:
- pass
- isEqual = True
- for pair in zip(inputFileListCopy, loadableFileListCopy):
- if pair[0] != pair[1]:
- print(f"{pair[0]} != {pair[1]}")
- isEqual = False
- break
- if not isEqual:
- continue
- return True
- return False
-
- def patientStudySeriesDoubleClicked(self):
- if self.advancedViewButton.checkState() == 0:
- # basic mode
- self.loadCheckedLoadables()
- else:
- # advanced mode, just examine the double-clicked item, do not load
- self.examineForLoading()
-
- def loadCheckedLoadables(self):
- """Invoke the load method on each plugin for the loadable
- (DICOMLoadable or qSlicerDICOMLoadable) instances that are selected"""
- if self.advancedViewButton.checkState() == 0:
- self.examineForLoading()
-
- self.loadableTable.updateSelectedFromCheckstate()
-
- # TODO: add check that disables all referenced stuff to be considered?
- # get all the references from the checked loadables
- referencedFileLists = []
- for plugin in self.loadablesByPlugin:
- for loadable in self.loadablesByPlugin[plugin]:
- if hasattr(loadable, 'referencedInstanceUIDs'):
- instanceFileList = []
- for instance in loadable.referencedInstanceUIDs:
- instanceFile = slicer.dicomDatabase.fileForInstance(instance)
- if instanceFile != '':
- instanceFileList.append(instanceFile)
- if len(instanceFileList) and not self.isFileListInCheckedLoadables(instanceFileList):
- referencedFileLists.append(instanceFileList)
-
- # if applicable, find all loadables from the file lists
- loadEnabled = False
- if len(referencedFileLists):
- (self.referencedLoadables, loadEnabled) = self.getLoadablesFromFileLists(referencedFileLists)
-
- automaticallyLoadReferences = int(slicer.util.settingsValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.InvalidRole))
- if slicer.app.commandOptions().testingEnabled:
- automaticallyLoadReferences = qt.QMessageBox.No
- if loadEnabled and automaticallyLoadReferences == qt.QMessageBox.InvalidRole:
- self.showReferenceDialogAndProceed()
- elif loadEnabled and automaticallyLoadReferences == qt.QMessageBox.Yes:
- self.addReferencesAndProceed()
- else:
- self.proceedWithReferencedLoadablesSelection()
-
- return
-
- def showReferenceDialogAndProceed(self):
- referencesDialog = DICOMReferencesDialog(self, loadables=self.referencedLoadables)
- answer = referencesDialog.exec_()
- if referencesDialog.rememberChoiceAndStopAskingCheckbox.checked == True:
- if answer == qt.QMessageBox.Yes:
- qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.Yes)
- if answer == qt.QMessageBox.No:
- qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.No)
- if answer == qt.QMessageBox.Yes:
- # each check box corresponds to a referenced loadable that was selected by examine;
- # if the user confirmed that reference should be loaded, add it to the self.loadablesByPlugin dictionary
- for plugin in self.referencedLoadables:
- for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]:
- if referencesDialog.checkboxes[loadable].checked:
- self.loadablesByPlugin[plugin].append(loadable)
- self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin]))
- self.proceedWithReferencedLoadablesSelection()
- elif answer == qt.QMessageBox.No:
- self.proceedWithReferencedLoadablesSelection()
-
- def addReferencesAndProceed(self):
- for plugin in self.referencedLoadables:
- for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]:
- self.loadablesByPlugin[plugin].append(loadable)
- self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin]))
- self.proceedWithReferencedLoadablesSelection()
-
- def proceedWithReferencedLoadablesSelection(self):
- if not self.warnUserIfLoadableWarningsAndProceed():
- return
-
- progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100)
-
- def progressCallback(progressDialog, progressLabel, progressValue):
- progressDialog.labelText = '\nLoading %s' % progressLabel
- slicer.app.processEvents()
- progressDialog.setValue(progressValue)
- slicer.app.processEvents()
- cancelled = progressDialog.wasCanceled
- return cancelled
-
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- messages = []
- loadedNodeIDs = DICOMLib.loadLoadables(self.loadablesByPlugin, messages,
- lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue))
-
- loadedFileParameters = {}
- loadedFileParameters['nodeIDs'] = loadedNodeIDs
- slicer.app.ioManager().emitNewFileLoaded(loadedFileParameters)
-
- qt.QApplication.restoreOverrideCursor()
-
- progressDialog.close()
-
- if messages:
- slicer.util.warningDisplay('\n'.join(messages), windowTitle='DICOM loading')
-
- self.onLoadingFinished()
-
- def warnUserIfLoadableWarningsAndProceed(self):
- warningsInSelectedLoadables = False
- details = ""
- for plugin in self.loadablesByPlugin:
- for loadable in self.loadablesByPlugin[plugin]:
- if loadable.selected and loadable.warning != "":
- warningsInSelectedLoadables = True
- logging.warning('Warning in DICOM plugin ' + plugin.loadType + ' when examining loadable ' + loadable.name +
- ': ' + loadable.warning)
- details += loadable.name + " [" + plugin.loadType + "]: " + loadable.warning + "\n"
- if warningsInSelectedLoadables:
- warning = "Warnings detected during load. Examine data in Advanced mode for details. Load anyway?"
- if not slicer.util.confirmOkCancelDisplay(warning, parent=self, detailedText=details):
+ with open(extensionDescriptorPath) as extensionDescriptorFP:
+ extensionDescriptor = extensionDescriptorFP.read()
+ dicomExtensions = json.loads(extensionDescriptor)
+ except:
+ logging.error('Cannot access DICOMExtensions.json file')
+ return
+
+ # 2 - get series info
+ # - iterate though metadata - should be fast even with large database
+ # - the fileValue call checks the tag cache so it's fast
+ modalityTag = "0008,0060"
+ sopClassUIDTag = "0008,0016"
+ sopClassUIDs = set()
+ modalities = set()
+ for patient in slicer.dicomDatabase.patients():
+ for study in slicer.dicomDatabase.studiesForPatient(patient):
+ for series in slicer.dicomDatabase.seriesForStudy(study):
+ instance0 = slicer.dicomDatabase.filesForSeries(series, 1)[0]
+ modality = slicer.dicomDatabase.fileValue(instance0, modalityTag)
+ sopClassUID = slicer.dicomDatabase.fileValue(instance0, sopClassUIDTag)
+ modalities.add(modality)
+ sopClassUIDs.add(sopClassUID)
+
+ # 3 - check if data matches
+ extensionsManagerModel = slicer.app.extensionsManagerModel()
+ installedExtensions = extensionsManagerModel.installedExtensions
+ extensionsToOffer = []
+ for extension in dicomExtensions['extensions']:
+ extensionName = extension['name']
+ if extensionName not in installedExtensions:
+ tagValues = extension['tagValues']
+ if 'Modality' in tagValues:
+ for modality in tagValues['Modality']:
+ if modality in modalities:
+ extensionsToOffer.append(extension)
+ if 'SOPClassUID' in tagValues:
+ for sopClassUID in tagValues['SOPClassUID']:
+ if sopClassUID in sopClassUIDs:
+ extensionsToOffer.append(extension)
+ return extensionsToOffer
+
+ def setBrowserPersistence(self, state):
+ self.browserPersistent = state
+ self.settings.setValue('DICOM/BrowserPersistent', bool(self.browserPersistent))
+
+ def onAdvancedViewButton(self, checked):
+ self.advancedView = checked
+ advancedWidgets = [self.loadableTableFrame, self.examineButton, self.uncheckAllButton]
+ for widget in advancedWidgets:
+ widget.visible = self.advancedView
+ self.updateButtonStates()
+
+ self.settings.setValue('DICOM/advancedView', int(self.advancedView))
+
+ def onHorizontalViewCheckBox(self):
+ horizontal = self.horizontalViewCheckBox.checked
+ self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
+ self.settings.setValue('DICOM/horizontalTables', int(horizontal))
+
+ def onSeriesSelected(self, seriesUIDList):
+ self.loadableTable.setLoadables([])
+ self.fileLists = self.getFileListsForRole(seriesUIDList, "SeriesUIDList")
+ self.updateButtonStates()
+
+ def getFileListsForRole(self, uidArgument, role):
+ fileLists = []
+ if role == "Series":
+ fileLists.append(slicer.dicomDatabase.filesForSeries(uidArgument))
+ if role == "SeriesUIDList":
+ for uid in uidArgument:
+ uid = uid.replace("'", "")
+ fileLists.append(slicer.dicomDatabase.filesForSeries(uid))
+ if role == "Study":
+ series = slicer.dicomDatabase.seriesForStudy(uidArgument)
+ for serie in series:
+ fileLists.append(slicer.dicomDatabase.filesForSeries(serie))
+ if role == "Patient":
+ studies = slicer.dicomDatabase.studiesForPatient(uidArgument)
+ for study in studies:
+ series = slicer.dicomDatabase.seriesForStudy(study)
+ for serie in series:
+ fileList = slicer.dicomDatabase.filesForSeries(serie)
+ fileLists.append(fileList)
+ return fileLists
+
+ def uncheckAllLoadables(self):
+ self.loadableTable.uncheckAll()
+
+ def onLoadableTableItemChanged(self, item):
+ self.updateButtonStates()
+
+ def examineForLoading(self):
+ """For selected plugins, give user the option
+ of what to load"""
+
+ (self.loadablesByPlugin, loadEnabled) = self.getLoadablesFromFileLists(self.fileLists)
+ DICOMLib.selectHighestConfidenceLoadables(self.loadablesByPlugin)
+ self.loadableTable.setLoadables(self.loadablesByPlugin)
+ self.updateButtonStates()
+
+ def getLoadablesFromFileLists(self, fileLists):
+ """Take list of file lists, return loadables by plugin dictionary
+ """
+
+ loadablesByPlugin = {}
+ loadEnabled = False
+
+ # Get selected plugins from application settings
+ # Settings are filled in DICOMWidget using DICOMPluginSelector
+ settings = qt.QSettings()
+ selectedPlugins = []
+ if settings.contains('DICOM/disabledPlugins/size'):
+ size = settings.beginReadArray('DICOM/disabledPlugins')
+ disabledPlugins = []
+
+ for i in range(size):
+ settings.setArrayIndex(i)
+ disabledPlugins.append(str(settings.allKeys()[0]))
+ settings.endArray()
+
+ for pluginClass in slicer.modules.dicomPlugins:
+ if pluginClass not in disabledPlugins:
+ selectedPlugins.append(pluginClass)
+ else:
+ # All DICOM plugins would be enabled by default
+ for pluginClass in slicer.modules.dicomPlugins:
+ selectedPlugins.append(pluginClass)
+
+ allFileCount = missingFileCount = 0
+ for fileList in fileLists:
+ for filePath in fileList:
+ allFileCount += 1
+ if not os.path.exists(filePath):
+ missingFileCount += 1
+
+ messages = []
+ if missingFileCount > 0:
+ messages.append("Warning: %d of %d selected files listed in the database cannot be found on disk." % (missingFileCount, allFileCount))
+
+ if missingFileCount < allFileCount:
+ progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100)
+
+ def progressCallback(progressDialog, progressLabel, progressValue):
+ progressDialog.labelText = '\nChecking %s' % progressLabel
+ slicer.app.processEvents()
+ progressDialog.setValue(progressValue)
+ slicer.app.processEvents()
+ cancelled = progressDialog.wasCanceled
+ return cancelled
+
+ loadablesByPlugin, loadEnabled = DICOMLib.getLoadablesFromFileLists(fileLists, selectedPlugins, messages,
+ lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue),
+ self.pluginInstances)
+
+ progressDialog.close()
+
+ if messages:
+ slicer.util.warningDisplay("Warning: %s\n\nSee python console for error message." % ' '.join(messages),
+ windowTitle="DICOM", parent=self)
+
+ return loadablesByPlugin, loadEnabled
+
+ def isFileListInCheckedLoadables(self, fileList):
+ for plugin in self.loadablesByPlugin:
+ for loadable in self.loadablesByPlugin[plugin]:
+ if len(loadable.files) != len(fileList) or len(loadable.files) == 0:
+ continue
+ inputFileListCopy = copy.deepcopy(fileList)
+ loadableFileListCopy = copy.deepcopy(loadable.files)
+ try:
+ inputFileListCopy.sort()
+ loadableFileListCopy.sort()
+ except Exception:
+ pass
+ isEqual = True
+ for pair in zip(inputFileListCopy, loadableFileListCopy):
+ if pair[0] != pair[1]:
+ print(f"{pair[0]} != {pair[1]}")
+ isEqual = False
+ break
+ if not isEqual:
+ continue
+ return True
return False
- return True
- def onLoadingFinished(self):
- if not self.browserPersistent:
- self.close()
+ def patientStudySeriesDoubleClicked(self):
+ if self.advancedViewButton.checkState() == 0:
+ # basic mode
+ self.loadCheckedLoadables()
+ else:
+ # advanced mode, just examine the double-clicked item, do not load
+ self.examineForLoading()
+
+ def loadCheckedLoadables(self):
+ """Invoke the load method on each plugin for the loadable
+ (DICOMLoadable or qSlicerDICOMLoadable) instances that are selected"""
+ if self.advancedViewButton.checkState() == 0:
+ self.examineForLoading()
+
+ self.loadableTable.updateSelectedFromCheckstate()
+
+ # TODO: add check that disables all referenced stuff to be considered?
+ # get all the references from the checked loadables
+ referencedFileLists = []
+ for plugin in self.loadablesByPlugin:
+ for loadable in self.loadablesByPlugin[plugin]:
+ if hasattr(loadable, 'referencedInstanceUIDs'):
+ instanceFileList = []
+ for instance in loadable.referencedInstanceUIDs:
+ instanceFile = slicer.dicomDatabase.fileForInstance(instance)
+ if instanceFile != '':
+ instanceFileList.append(instanceFile)
+ if len(instanceFileList) and not self.isFileListInCheckedLoadables(instanceFileList):
+ referencedFileLists.append(instanceFileList)
+
+ # if applicable, find all loadables from the file lists
+ loadEnabled = False
+ if len(referencedFileLists):
+ (self.referencedLoadables, loadEnabled) = self.getLoadablesFromFileLists(referencedFileLists)
+
+ automaticallyLoadReferences = int(slicer.util.settingsValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.InvalidRole))
+ if slicer.app.commandOptions().testingEnabled:
+ automaticallyLoadReferences = qt.QMessageBox.No
+ if loadEnabled and automaticallyLoadReferences == qt.QMessageBox.InvalidRole:
+ self.showReferenceDialogAndProceed()
+ elif loadEnabled and automaticallyLoadReferences == qt.QMessageBox.Yes:
+ self.addReferencesAndProceed()
+ else:
+ self.proceedWithReferencedLoadablesSelection()
+
+ return
+
+ def showReferenceDialogAndProceed(self):
+ referencesDialog = DICOMReferencesDialog(self, loadables=self.referencedLoadables)
+ answer = referencesDialog.exec_()
+ if referencesDialog.rememberChoiceAndStopAskingCheckbox.checked == True:
+ if answer == qt.QMessageBox.Yes:
+ qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.Yes)
+ if answer == qt.QMessageBox.No:
+ qt.QSettings().setValue('DICOM/automaticallyLoadReferences', qt.QMessageBox.No)
+ if answer == qt.QMessageBox.Yes:
+ # each check box corresponds to a referenced loadable that was selected by examine;
+ # if the user confirmed that reference should be loaded, add it to the self.loadablesByPlugin dictionary
+ for plugin in self.referencedLoadables:
+ for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]:
+ if referencesDialog.checkboxes[loadable].checked:
+ self.loadablesByPlugin[plugin].append(loadable)
+ self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin]))
+ self.proceedWithReferencedLoadablesSelection()
+ elif answer == qt.QMessageBox.No:
+ self.proceedWithReferencedLoadablesSelection()
+
+ def addReferencesAndProceed(self):
+ for plugin in self.referencedLoadables:
+ for loadable in [l for l in self.referencedLoadables[plugin] if l.selected]:
+ self.loadablesByPlugin[plugin].append(loadable)
+ self.loadablesByPlugin[plugin] = list(set(self.loadablesByPlugin[plugin]))
+ self.proceedWithReferencedLoadablesSelection()
+
+ def proceedWithReferencedLoadablesSelection(self):
+ if not self.warnUserIfLoadableWarningsAndProceed():
+ return
+
+ progressDialog = slicer.util.createProgressDialog(parent=self, value=0, maximum=100)
+
+ def progressCallback(progressDialog, progressLabel, progressValue):
+ progressDialog.labelText = '\nLoading %s' % progressLabel
+ slicer.app.processEvents()
+ progressDialog.setValue(progressValue)
+ slicer.app.processEvents()
+ cancelled = progressDialog.wasCanceled
+ return cancelled
+
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ messages = []
+ loadedNodeIDs = DICOMLib.loadLoadables(self.loadablesByPlugin, messages,
+ lambda progressLabel, progressValue, progressDialog=progressDialog: progressCallback(progressDialog, progressLabel, progressValue))
+
+ loadedFileParameters = {}
+ loadedFileParameters['nodeIDs'] = loadedNodeIDs
+ slicer.app.ioManager().emitNewFileLoaded(loadedFileParameters)
+
+ qt.QApplication.restoreOverrideCursor()
+
+ progressDialog.close()
+
+ if messages:
+ slicer.util.warningDisplay('\n'.join(messages), windowTitle='DICOM loading')
+
+ self.onLoadingFinished()
+
+ def warnUserIfLoadableWarningsAndProceed(self):
+ warningsInSelectedLoadables = False
+ details = ""
+ for plugin in self.loadablesByPlugin:
+ for loadable in self.loadablesByPlugin[plugin]:
+ if loadable.selected and loadable.warning != "":
+ warningsInSelectedLoadables = True
+ logging.warning('Warning in DICOM plugin ' + plugin.loadType + ' when examining loadable ' + loadable.name +
+ ': ' + loadable.warning)
+ details += loadable.name + " [" + plugin.loadType + "]: " + loadable.warning + "\n"
+ if warningsInSelectedLoadables:
+ warning = "Warnings detected during load. Examine data in Advanced mode for details. Load anyway?"
+ if not slicer.util.confirmOkCancelDisplay(warning, parent=self, detailedText=details):
+ return False
+ return True
+
+ def onLoadingFinished(self):
+ if not self.browserPersistent:
+ self.close()
class DICOMReferencesDialog(qt.QMessageBox):
- WINDOW_TITLE = "Referenced datasets found"
- WINDOW_TEXT = "The loaded DICOM objects contain references to other datasets you did not select for loading. Please " \
- "select Yes if you would like to load the following referenced datasets, No if you only want to load the " \
- "originally selected series, or Cancel to abort loading."
-
- def __init__(self, parent, loadables):
- super().__init__(parent)
- self.loadables = loadables
- self.checkboxes = dict()
- self.setup()
-
- def setup(self):
- self._setBasicProperties()
- self._addTextLabel()
- self._addLoadableCheckboxes()
- self.rememberChoiceAndStopAskingCheckbox = qt.QCheckBox('Remember choice and stop asking')
- self.rememberChoiceAndStopAskingCheckbox.toolTip = 'Can be changed later in Application Settings / DICOM'
- self.yesButton = self.addButton(self.Yes)
- self.yesButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
- self.noButton = self.addButton(self.No)
- self.noButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
- self.cancelButton = self.addButton(self.Cancel)
- self.cancelButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
- self.layout().addWidget(self.yesButton, 3, 0, 1, 1)
- self.layout().addWidget(self.noButton, 3, 1, 1, 1)
- self.layout().addWidget(self.cancelButton, 3, 2, 1, 1)
- self.layout().addWidget(self.rememberChoiceAndStopAskingCheckbox, 2, 0, 1, 3)
-
- def _setBasicProperties(self):
- self.layout().setSpacing(9)
- self.setWindowTitle(self.WINDOW_TITLE)
- fontMetrics = qt.QFontMetrics(qt.QApplication.font(self))
- try:
- self.setMinimumWidth(fontMetrics.horizontalAdvance(self.WINDOW_TITLE))
- except AttributeError:
- # Support Qt < 5.11 lacking QFontMetrics::horizontalAdvance()
- self.setMinimumWidth(fontMetrics.width(self.WINDOW_TITLE))
-
- def _addTextLabel(self):
- label = qt.QLabel(self.WINDOW_TEXT)
- label.wordWrap = True
- self.layout().addWidget(label, 0, 0, 1, 3)
-
- def _addLoadableCheckboxes(self):
- self.checkBoxGroupBox = qt.QGroupBox("References")
- self.checkBoxGroupBox.setLayout(qt.QFormLayout())
- for plugin in self.loadables:
- for loadable in [l for l in self.loadables[plugin] if l.selected]:
- checkBoxText = loadable.name + ' (' + plugin.loadType + ') '
- cb = qt.QCheckBox(checkBoxText, self)
- cb.checked = True
- cb.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
- self.checkboxes[loadable] = cb
- self.checkBoxGroupBox.layout().addWidget(cb)
- self.layout().addWidget(self.checkBoxGroupBox, 1, 0, 1, 3)
+ WINDOW_TITLE = "Referenced datasets found"
+ WINDOW_TEXT = "The loaded DICOM objects contain references to other datasets you did not select for loading. Please " \
+ "select Yes if you would like to load the following referenced datasets, No if you only want to load the " \
+ "originally selected series, or Cancel to abort loading."
+
+ def __init__(self, parent, loadables):
+ super().__init__(parent)
+ self.loadables = loadables
+ self.checkboxes = dict()
+ self.setup()
+
+ def setup(self):
+ self._setBasicProperties()
+ self._addTextLabel()
+ self._addLoadableCheckboxes()
+ self.rememberChoiceAndStopAskingCheckbox = qt.QCheckBox('Remember choice and stop asking')
+ self.rememberChoiceAndStopAskingCheckbox.toolTip = 'Can be changed later in Application Settings / DICOM'
+ self.yesButton = self.addButton(self.Yes)
+ self.yesButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
+ self.noButton = self.addButton(self.No)
+ self.noButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
+ self.cancelButton = self.addButton(self.Cancel)
+ self.cancelButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
+ self.layout().addWidget(self.yesButton, 3, 0, 1, 1)
+ self.layout().addWidget(self.noButton, 3, 1, 1, 1)
+ self.layout().addWidget(self.cancelButton, 3, 2, 1, 1)
+ self.layout().addWidget(self.rememberChoiceAndStopAskingCheckbox, 2, 0, 1, 3)
+
+ def _setBasicProperties(self):
+ self.layout().setSpacing(9)
+ self.setWindowTitle(self.WINDOW_TITLE)
+ fontMetrics = qt.QFontMetrics(qt.QApplication.font(self))
+ try:
+ self.setMinimumWidth(fontMetrics.horizontalAdvance(self.WINDOW_TITLE))
+ except AttributeError:
+ # Support Qt < 5.11 lacking QFontMetrics::horizontalAdvance()
+ self.setMinimumWidth(fontMetrics.width(self.WINDOW_TITLE))
+
+ def _addTextLabel(self):
+ label = qt.QLabel(self.WINDOW_TEXT)
+ label.wordWrap = True
+ self.layout().addWidget(label, 0, 0, 1, 3)
+
+ def _addLoadableCheckboxes(self):
+ self.checkBoxGroupBox = qt.QGroupBox("References")
+ self.checkBoxGroupBox.setLayout(qt.QFormLayout())
+ for plugin in self.loadables:
+ for loadable in [l for l in self.loadables[plugin] if l.selected]:
+ checkBoxText = loadable.name + ' (' + plugin.loadType + ') '
+ cb = qt.QCheckBox(checkBoxText, self)
+ cb.checked = True
+ cb.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred))
+ self.checkboxes[loadable] = cb
+ self.checkBoxGroupBox.layout().addWidget(cb)
+ self.layout().addWidget(self.checkBoxGroupBox, 1, 0, 1, 3)
class DICOMLoadableTable(qt.QTableWidget):
- """Implement the Qt code for a table of
- selectable slicer data to be made from
- the given dicom files
- """
-
- def __init__(self, parent, width=350, height=100):
- super().__init__(parent)
- self.setMinimumHeight(height)
- self.setMinimumWidth(width)
- self.loadables = {}
- self.setLoadables([])
- self.configure()
- slicer.app.connect('aboutToQuit()', self.deleteLater)
-
- def getNumberOfCheckedItems(self):
- return sum(1 for row in range(self.rowCount) if self.item(row, 0).checkState() == qt.Qt.Checked)
-
- def configure(self):
- self.setColumnCount(3)
- self.setHorizontalHeaderLabels(['DICOM Data', 'Reader', 'Warnings'])
- self.setSelectionBehavior(qt.QTableView.SelectRows)
- self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.Stretch)
- self.horizontalHeader().setSectionResizeMode(0, qt.QHeaderView.Interactive)
- self.horizontalHeader().setSectionResizeMode(1, qt.QHeaderView.ResizeToContents)
- self.horizontalHeader().setSectionResizeMode(2, qt.QHeaderView.Stretch)
- self.horizontalScrollMode = qt.QAbstractItemView.ScrollPerPixel
-
- def addLoadableRow(self, loadable, row, reader):
- self.insertRow(row)
- self.loadables[row] = loadable
- item = qt.QTableWidgetItem(loadable.name)
- self.setItem(row, 0, item)
- self.setCheckState(item, loadable)
- self.addReaderColumn(item, reader, row)
- self.addWarningColumn(item, loadable, row)
-
- def setCheckState(self, item, loadable):
- item.setCheckState(qt.Qt.Checked if loadable.selected else qt.Qt.Unchecked)
- item.setToolTip(loadable.tooltip)
-
- def addReaderColumn(self, item, reader, row):
- if not reader:
- return
- readerItem = qt.QTableWidgetItem(reader)
- readerItem.setFlags(readerItem.flags() ^ qt.Qt.ItemIsEditable)
- self.setItem(row, 1, readerItem)
- readerItem.setToolTip(item.toolTip())
-
- def addWarningColumn(self, item, loadable, row):
- warning = loadable.warning if loadable.warning else ''
- warnItem = qt.QTableWidgetItem(warning)
- warnItem.setFlags(warnItem.flags() ^ qt.Qt.ItemIsEditable)
- self.setItem(row, 2, warnItem)
- item.setToolTip(item.toolTip() + "\n" + warning)
- warnItem.setToolTip(item.toolTip())
-
- def setLoadables(self, loadablesByPlugin):
- """Load the table widget with a list
- of volume options (of class DICOMVolume)
+ """Implement the Qt code for a table of
+ selectable slicer data to be made from
+ the given dicom files
"""
- self.clearContents()
- self.setRowCount(0)
- self.loadables = {}
-
- # For each plugin, keep only a single loadable selected for the same file set
- # to prevent loading same data multiple times.
- for plugin in loadablesByPlugin:
- for thisLoadableId in range(len(loadablesByPlugin[plugin])):
- for prevLoadableId in range(0, thisLoadableId):
- thisLoadable = loadablesByPlugin[plugin][thisLoadableId]
- prevLoadable = loadablesByPlugin[plugin][prevLoadableId]
- # fileDifferences will contain all the files that only present in one or the other list (or tuple)
- fileDifferences = set(thisLoadable.files).symmetric_difference(set(prevLoadable.files))
- if (not fileDifferences) and (prevLoadable.selected):
- thisLoadable.selected = False
- break
-
- row = 0
- for selectState in (True, False):
- for plugin in loadablesByPlugin:
- for loadable in loadablesByPlugin[plugin]:
- if loadable.selected == selectState:
- self.addLoadableRow(loadable, row, plugin.loadType)
- row += 1
-
- self.setVerticalHeaderLabels(row * [""])
-
- def uncheckAll(self):
- for row in range(self.rowCount):
- item = self.item(row, 0)
- item.setCheckState(False)
-
- def updateSelectedFromCheckstate(self):
- for row in range(self.rowCount):
- item = self.item(row, 0)
- self.loadables[row].selected = (item.checkState() != 0)
- # updating the names
- self.loadables[row].name = item.text()
+
+ def __init__(self, parent, width=350, height=100):
+ super().__init__(parent)
+ self.setMinimumHeight(height)
+ self.setMinimumWidth(width)
+ self.loadables = {}
+ self.setLoadables([])
+ self.configure()
+ slicer.app.connect('aboutToQuit()', self.deleteLater)
+
+ def getNumberOfCheckedItems(self):
+ return sum(1 for row in range(self.rowCount) if self.item(row, 0).checkState() == qt.Qt.Checked)
+
+ def configure(self):
+ self.setColumnCount(3)
+ self.setHorizontalHeaderLabels(['DICOM Data', 'Reader', 'Warnings'])
+ self.setSelectionBehavior(qt.QTableView.SelectRows)
+ self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.Stretch)
+ self.horizontalHeader().setSectionResizeMode(0, qt.QHeaderView.Interactive)
+ self.horizontalHeader().setSectionResizeMode(1, qt.QHeaderView.ResizeToContents)
+ self.horizontalHeader().setSectionResizeMode(2, qt.QHeaderView.Stretch)
+ self.horizontalScrollMode = qt.QAbstractItemView.ScrollPerPixel
+
+ def addLoadableRow(self, loadable, row, reader):
+ self.insertRow(row)
+ self.loadables[row] = loadable
+ item = qt.QTableWidgetItem(loadable.name)
+ self.setItem(row, 0, item)
+ self.setCheckState(item, loadable)
+ self.addReaderColumn(item, reader, row)
+ self.addWarningColumn(item, loadable, row)
+
+ def setCheckState(self, item, loadable):
+ item.setCheckState(qt.Qt.Checked if loadable.selected else qt.Qt.Unchecked)
+ item.setToolTip(loadable.tooltip)
+
+ def addReaderColumn(self, item, reader, row):
+ if not reader:
+ return
+ readerItem = qt.QTableWidgetItem(reader)
+ readerItem.setFlags(readerItem.flags() ^ qt.Qt.ItemIsEditable)
+ self.setItem(row, 1, readerItem)
+ readerItem.setToolTip(item.toolTip())
+
+ def addWarningColumn(self, item, loadable, row):
+ warning = loadable.warning if loadable.warning else ''
+ warnItem = qt.QTableWidgetItem(warning)
+ warnItem.setFlags(warnItem.flags() ^ qt.Qt.ItemIsEditable)
+ self.setItem(row, 2, warnItem)
+ item.setToolTip(item.toolTip() + "\n" + warning)
+ warnItem.setToolTip(item.toolTip())
+
+ def setLoadables(self, loadablesByPlugin):
+ """Load the table widget with a list
+ of volume options (of class DICOMVolume)
+ """
+ self.clearContents()
+ self.setRowCount(0)
+ self.loadables = {}
+
+ # For each plugin, keep only a single loadable selected for the same file set
+ # to prevent loading same data multiple times.
+ for plugin in loadablesByPlugin:
+ for thisLoadableId in range(len(loadablesByPlugin[plugin])):
+ for prevLoadableId in range(0, thisLoadableId):
+ thisLoadable = loadablesByPlugin[plugin][thisLoadableId]
+ prevLoadable = loadablesByPlugin[plugin][prevLoadableId]
+ # fileDifferences will contain all the files that only present in one or the other list (or tuple)
+ fileDifferences = set(thisLoadable.files).symmetric_difference(set(prevLoadable.files))
+ if (not fileDifferences) and (prevLoadable.selected):
+ thisLoadable.selected = False
+ break
+
+ row = 0
+ for selectState in (True, False):
+ for plugin in loadablesByPlugin:
+ for loadable in loadablesByPlugin[plugin]:
+ if loadable.selected == selectState:
+ self.addLoadableRow(loadable, row, plugin.loadType)
+ row += 1
+
+ self.setVerticalHeaderLabels(row * [""])
+
+ def uncheckAll(self):
+ for row in range(self.rowCount):
+ item = self.item(row, 0)
+ item.setCheckState(False)
+
+ def updateSelectedFromCheckstate(self):
+ for row in range(self.rowCount):
+ item = self.item(row, 0)
+ self.loadables[row].selected = (item.checkState() != 0)
+ # updating the names
+ self.loadables[row].name = item.text()
diff --git a/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py b/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py
index 733b2bb3c3f..7c44a2a9830 100644
--- a/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py
+++ b/Modules/Scripted/DICOMLib/DICOMExportScalarVolume.py
@@ -19,32 +19,32 @@
class DICOMExportScalarVolume:
- """Code to export slicer data to dicom database
- TODO: delete temp directories and files
- """
-
- def __init__(self, studyUID, volumeNode, tags, directory, filenamePrefix=None):
- """
- studyUID parameter is not used (studyUID is retrieved from tags).
+ """Code to export slicer data to dicom database
+ TODO: delete temp directories and files
"""
- self.studyUID = studyUID
- self.volumeNode = volumeNode
- self.tags = tags
- self.directory = directory
- self.filenamePrefix = filenamePrefix if filenamePrefix else "IMG"
- # self.referenceFile = None
-
- # TODO: May come in use when appending to existing study
- # def parametersFromStudy(self,studyUID=None):
- # """Return a dictionary of the required conversion parameters
- # based on the studyUID found in the dicom dictionary (empty if
- # not well defined"""
- # if not studyUID:
- # studyUID = self.studyUID
-
- # # TODO: we should install dicom.dic with slicer and use it to
- # # define the tag to name mapping
- # tags = {
+
+ def __init__(self, studyUID, volumeNode, tags, directory, filenamePrefix=None):
+ """
+ studyUID parameter is not used (studyUID is retrieved from tags).
+ """
+ self.studyUID = studyUID
+ self.volumeNode = volumeNode
+ self.tags = tags
+ self.directory = directory
+ self.filenamePrefix = filenamePrefix if filenamePrefix else "IMG"
+ # self.referenceFile = None
+
+ # TODO: May come in use when appending to existing study
+ # def parametersFromStudy(self,studyUID=None):
+ # """Return a dictionary of the required conversion parameters
+ # based on the studyUID found in the dicom dictionary (empty if
+ # not well defined"""
+ # if not studyUID:
+ # studyUID = self.studyUID
+
+ # # TODO: we should install dicom.dic with slicer and use it to
+ # # define the tag to name mapping
+ # tags = {
# "0010,0010": "Patient Name",
# "0010,0020": "Patient ID",
# "0010,4000": "Patient Comments",
@@ -54,114 +54,114 @@ def __init__(self, studyUID, volumeNode, tags, directory, filenamePrefix=None):
# "0008,0060": "Modality",
# "0008,0070": "Manufacturer",
# "0008,1090": "Model",
- # }
- # seriesNumbers = []
- # p = {}
- # if studyUID:
- # series = slicer.dicomDatabase.seriesForStudy(studyUID)
- # # first find a unique series number
- # for serie in series:
- # files = slicer.dicomDatabase.filesForSeries(serie, 1)
- # if len(files):
- # slicer.dicomDatabase.loadFileHeader(files[0])
- # dump = slicer.dicomDatabase.headerValue('0020,0011')
- # try:
- # value = dump[dump.index('[')+1:dump.index(']')]
- # seriesNumbers.append(int(value))
- # except ValueError:
- # pass
- # for i in xrange(len(series)+1):
- # if not i in seriesNumbers:
- # p['Series Number'] = i
- # break
-
- # # now find the other values from any file (use first file in first series)
- # if len(series):
- # p['Series Number'] = str(len(series)+1) # doesn't need to be unique, but we try
- # files = slicer.dicomDatabase.filesForSeries(series[0], 1)
- # if len(files):
- # self.referenceFile = files[0]
- # slicer.dicomDatabase.loadFileHeader(self.referenceFile)
- # for tag in tags.keys():
- # dump = slicer.dicomDatabase.headerValue(tag)
- # try:
- # value = dump[dump.index('[')+1:dump.index(']')]
- # except ValueError:
- # value = "Unknown"
- # p[tags[tag]] = value
- # return p
-
- def progress(self, string):
- # TODO: make this a callback for a gui progress dialog
- print(string)
-
- def export(self):
- """
- Export the volume data using the ITK-based utility
- TODO: confirm that resulting file is valid - may need to change the CLI
- to include more parameters or do a new implementation ctk/DCMTK
- See:
- https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM
- TODO: add more parameters to the CLI and/or find a different
- mechanism for creating the DICOM files
- """
- cliparameters = {}
- # Patient
- cliparameters['patientName'] = self.tags['Patient Name']
- cliparameters['patientID'] = self.tags['Patient ID']
- cliparameters['patientBirthDate'] = self.tags['Patient Birth Date']
- cliparameters['patientSex'] = self.tags['Patient Sex'] if self.tags['Patient Sex'] else "[unknown]"
- cliparameters['patientComments'] = self.tags['Patient Comments']
- # Study
- cliparameters['studyID'] = self.tags['Study ID']
- cliparameters['studyDate'] = self.tags['Study Date']
- cliparameters['studyTime'] = self.tags['Study Time']
- cliparameters['studyDescription'] = self.tags['Study Description']
- cliparameters['modality'] = self.tags['Modality']
- cliparameters['manufacturer'] = self.tags['Manufacturer']
- cliparameters['model'] = self.tags['Model']
- # Series
- cliparameters['seriesDescription'] = self.tags['Series Description']
- cliparameters['seriesNumber'] = self.tags['Series Number']
- cliparameters['seriesDate'] = self.tags['Series Date']
- cliparameters['seriesTime'] = self.tags['Series Time']
- # Image
- displayNode = self.volumeNode.GetDisplayNode()
- if displayNode:
- if displayNode.IsA('vtkMRMLScalarVolumeDisplayNode'):
- cliparameters['windowCenter'] = str(displayNode.GetLevel())
- cliparameters['windowWidth'] = str(displayNode.GetWindow())
- else:
- # labelmap volume
- scalarRange = displayNode.GetScalarRange()
- cliparameters['windowCenter'] = str((scalarRange[0] + scalarRange[0]) / 2.0)
- cliparameters['windowWidth'] = str(scalarRange[1] - scalarRange[0])
- cliparameters['contentDate'] = self.tags['Content Date']
- cliparameters['contentTime'] = self.tags['Content Time']
-
- # UIDs
- cliparameters['studyInstanceUID'] = self.tags['Study Instance UID']
- cliparameters['seriesInstanceUID'] = self.tags['Series Instance UID']
- if 'Frame of Reference UID' in self.tags:
- cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID']
- elif 'Frame of Reference Instance UID' in self.tags:
- logging.warning('Usage of "Frame of Reference Instance UID" is deprecated, use "Frame of Reference UID" instead.')
- cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID']
- cliparameters['inputVolume'] = self.volumeNode.GetID()
-
- cliparameters['dicomDirectory'] = self.directory
- cliparameters['dicomPrefix'] = self.filenamePrefix
-
- #
- # run the task (in the background)
- # - use the GUI to provide progress feedback
- # - use the GUI's Logic to invoke the task
- #
- if not hasattr(slicer.modules, 'createdicomseries'):
- logging.error("CreateDICOMSeries module is not found")
- return False
- dicomWrite = slicer.modules.createdicomseries
- cliNode = slicer.cli.run(dicomWrite, None, cliparameters, wait_for_completion=True)
- success = (cliNode.GetStatus() == cliNode.Completed)
- slicer.mrmlScene.RemoveNode(cliNode)
- return success
+ # }
+ # seriesNumbers = []
+ # p = {}
+ # if studyUID:
+ # series = slicer.dicomDatabase.seriesForStudy(studyUID)
+ # # first find a unique series number
+ # for serie in series:
+ # files = slicer.dicomDatabase.filesForSeries(serie, 1)
+ # if len(files):
+ # slicer.dicomDatabase.loadFileHeader(files[0])
+ # dump = slicer.dicomDatabase.headerValue('0020,0011')
+ # try:
+ # value = dump[dump.index('[')+1:dump.index(']')]
+ # seriesNumbers.append(int(value))
+ # except ValueError:
+ # pass
+ # for i in xrange(len(series)+1):
+ # if not i in seriesNumbers:
+ # p['Series Number'] = i
+ # break
+
+ # # now find the other values from any file (use first file in first series)
+ # if len(series):
+ # p['Series Number'] = str(len(series)+1) # doesn't need to be unique, but we try
+ # files = slicer.dicomDatabase.filesForSeries(series[0], 1)
+ # if len(files):
+ # self.referenceFile = files[0]
+ # slicer.dicomDatabase.loadFileHeader(self.referenceFile)
+ # for tag in tags.keys():
+ # dump = slicer.dicomDatabase.headerValue(tag)
+ # try:
+ # value = dump[dump.index('[')+1:dump.index(']')]
+ # except ValueError:
+ # value = "Unknown"
+ # p[tags[tag]] = value
+ # return p
+
+ def progress(self, string):
+ # TODO: make this a callback for a gui progress dialog
+ print(string)
+
+ def export(self):
+ """
+ Export the volume data using the ITK-based utility
+ TODO: confirm that resulting file is valid - may need to change the CLI
+ to include more parameters or do a new implementation ctk/DCMTK
+ See:
+ https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM
+ TODO: add more parameters to the CLI and/or find a different
+ mechanism for creating the DICOM files
+ """
+ cliparameters = {}
+ # Patient
+ cliparameters['patientName'] = self.tags['Patient Name']
+ cliparameters['patientID'] = self.tags['Patient ID']
+ cliparameters['patientBirthDate'] = self.tags['Patient Birth Date']
+ cliparameters['patientSex'] = self.tags['Patient Sex'] if self.tags['Patient Sex'] else "[unknown]"
+ cliparameters['patientComments'] = self.tags['Patient Comments']
+ # Study
+ cliparameters['studyID'] = self.tags['Study ID']
+ cliparameters['studyDate'] = self.tags['Study Date']
+ cliparameters['studyTime'] = self.tags['Study Time']
+ cliparameters['studyDescription'] = self.tags['Study Description']
+ cliparameters['modality'] = self.tags['Modality']
+ cliparameters['manufacturer'] = self.tags['Manufacturer']
+ cliparameters['model'] = self.tags['Model']
+ # Series
+ cliparameters['seriesDescription'] = self.tags['Series Description']
+ cliparameters['seriesNumber'] = self.tags['Series Number']
+ cliparameters['seriesDate'] = self.tags['Series Date']
+ cliparameters['seriesTime'] = self.tags['Series Time']
+ # Image
+ displayNode = self.volumeNode.GetDisplayNode()
+ if displayNode:
+ if displayNode.IsA('vtkMRMLScalarVolumeDisplayNode'):
+ cliparameters['windowCenter'] = str(displayNode.GetLevel())
+ cliparameters['windowWidth'] = str(displayNode.GetWindow())
+ else:
+ # labelmap volume
+ scalarRange = displayNode.GetScalarRange()
+ cliparameters['windowCenter'] = str((scalarRange[0] + scalarRange[0]) / 2.0)
+ cliparameters['windowWidth'] = str(scalarRange[1] - scalarRange[0])
+ cliparameters['contentDate'] = self.tags['Content Date']
+ cliparameters['contentTime'] = self.tags['Content Time']
+
+ # UIDs
+ cliparameters['studyInstanceUID'] = self.tags['Study Instance UID']
+ cliparameters['seriesInstanceUID'] = self.tags['Series Instance UID']
+ if 'Frame of Reference UID' in self.tags:
+ cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID']
+ elif 'Frame of Reference Instance UID' in self.tags:
+ logging.warning('Usage of "Frame of Reference Instance UID" is deprecated, use "Frame of Reference UID" instead.')
+ cliparameters['frameOfReferenceUID'] = self.tags['Frame of Reference UID']
+ cliparameters['inputVolume'] = self.volumeNode.GetID()
+
+ cliparameters['dicomDirectory'] = self.directory
+ cliparameters['dicomPrefix'] = self.filenamePrefix
+
+ #
+ # run the task (in the background)
+ # - use the GUI to provide progress feedback
+ # - use the GUI's Logic to invoke the task
+ #
+ if not hasattr(slicer.modules, 'createdicomseries'):
+ logging.error("CreateDICOMSeries module is not found")
+ return False
+ dicomWrite = slicer.modules.createdicomseries
+ cliNode = slicer.cli.run(dicomWrite, None, cliparameters, wait_for_completion=True)
+ success = (cliNode.GetStatus() == cliNode.Completed)
+ slicer.mrmlScene.RemoveNode(cliNode)
+ return success
diff --git a/Modules/Scripted/DICOMLib/DICOMExportScene.py b/Modules/Scripted/DICOMLib/DICOMExportScene.py
index 403321afe27..7814d0f2793 100644
--- a/Modules/Scripted/DICOMLib/DICOMExportScene.py
+++ b/Modules/Scripted/DICOMLib/DICOMExportScene.py
@@ -27,142 +27,142 @@
class DICOMExportScene:
- """Export slicer scene to dicom database
- """
-
- def __init__(self, referenceFile, saveDirectoryPath=None):
- # File used as reference for DICOM export. Provides most of the DICOM tags.
- # If not specified, the first file in the DICOM database is used.
- self.referenceFile = referenceFile
- # Directory where all the intermediate files are saved.
- self.saveDirectoryPath = saveDirectoryPath
- # Path and filename of the Slicer Data Bundle DICOM file
- self.sdbFile = None
- # Path to the screenshot image file that is saved with the scene and in the Secondary Capture.
- # If not specified, then the default scene saving method is used to generate the image.
- self.imageFile = None
- # Study description string to save in the tags. Default is "Slicer Scene Export"
- self.studyDescription = None
- # Series description string to save in the tags. Default is "Slicer Data Bundle"
- self.seriesDescription = None
- # Optional tags.
- # Dictionary where the keys are the tag names (such as StudyInstanceUID), and the values are the tag values
- self.optionalTags = {}
-
- def progress(self, string):
- # TODO: make this a callback for a gui progress dialog
- logging.info(string)
-
- def export(self):
- # Perform export
- success = self.createDICOMFileForScene()
- return success
-
- def createDICOMFileForScene(self):
- """
- Export the scene data:
- - first to a directory using the utility in the mrmlScene
- - create a zip file using the application logic
- - create secondary capture based on the sample dataset
- - add the zip file as a private creator tag
- TODO: confirm that resulting file is valid - may need to change the CLI
- to include more parameters or do a new implementation ctk/DCMTK
- See:
- https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM
+ """Export slicer scene to dicom database
"""
- # set up temp directories and files
- if self.saveDirectoryPath is None:
- self.saveDirectoryPath = tempfile.mkdtemp('', 'dicomExport', slicer.app.temporaryPath)
- self.zipFile = os.path.join(self.saveDirectoryPath, "scene.zip")
- self.dumpFile = os.path.join(self.saveDirectoryPath, "dump.dcm")
- self.templateFile = os.path.join(self.saveDirectoryPath, "template.dcm")
- self.sdbFile = os.path.join(self.saveDirectoryPath, "SlicerDataBundle.dcm")
- if self.studyDescription is None:
- self.studyDescription = 'Slicer Scene Export'
- if self.seriesDescription is None:
- self.seriesDescription = 'Slicer Data Bundle'
-
- # get the screen image if not specified
- if self.imageFile is None:
- self.progress('Saving Image...')
- self.imageFile = os.path.join(self.saveDirectoryPath, "scene.jpg")
- image = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow())
- image.save(self.imageFile)
- imageReader = vtk.vtkJPEGReader()
- imageReader.SetFileName(self.imageFile)
- imageReader.Update()
-
- # Clean up paths on Windows (some commands and operations are not performed properly with mixed slash and backslash)
- self.saveDirectoryPath = self.saveDirectoryPath.replace('\\', '/')
- self.imageFile = self.imageFile.replace('\\', '/')
- self.zipFile = self.zipFile.replace('\\', '/')
- self.dumpFile = self.dumpFile.replace('\\', '/')
- self.templateFile = self.templateFile.replace('\\', '/')
- self.sdbFile = self.sdbFile.replace('\\', '/')
-
- # save the scene to the temp dir
- self.progress('Saving scene into MRB...')
- if not slicer.mrmlScene.WriteToMRB(self.zipFile, imageReader.GetOutput()):
- logging.error('Failed to save scene into MRB file: ' + self.zipFile)
- return False
-
- zipSize = os.path.getsize(self.zipFile)
-
- # now create the dicom file
- # - create the dump (capture stdout)
- # cmd = "dcmdump --print-all --write-pixel %s %s" % (self.saveDirectoryPath, self.referenceFile)
- self.progress('Making dicom reference file...')
- logging.info('Using reference file ' + str(self.referenceFile))
- args = ['--print-all', '--write-pixel', self.saveDirectoryPath, self.referenceFile]
- dumpByteArray = DICOMLib.DICOMCommand('dcmdump', args).start()
- dump = str(dumpByteArray.data(), encoding='utf-8')
-
- # append this to the dumped output and save the result as self.saveDirectoryPath/dcm.dump
- # with %s as self.zipFile and %d being its size in bytes
- zipSizeString = "%d" % zipSize
-
- # hack: encode the file zip file size as part of the creator string
- # because none of the normal types (UL, DS, LO) seem to survive
- # the dump2dcm step (possibly due to the Unknown nature of the private tag)
- creatorString = "3D Slicer %s" % zipSizeString
- candygram = """(cadb,0010) LO [%s] # %d, 1 PrivateCreator
+ def __init__(self, referenceFile, saveDirectoryPath=None):
+ # File used as reference for DICOM export. Provides most of the DICOM tags.
+ # If not specified, the first file in the DICOM database is used.
+ self.referenceFile = referenceFile
+ # Directory where all the intermediate files are saved.
+ self.saveDirectoryPath = saveDirectoryPath
+ # Path and filename of the Slicer Data Bundle DICOM file
+ self.sdbFile = None
+ # Path to the screenshot image file that is saved with the scene and in the Secondary Capture.
+ # If not specified, then the default scene saving method is used to generate the image.
+ self.imageFile = None
+ # Study description string to save in the tags. Default is "Slicer Scene Export"
+ self.studyDescription = None
+ # Series description string to save in the tags. Default is "Slicer Data Bundle"
+ self.seriesDescription = None
+ # Optional tags.
+ # Dictionary where the keys are the tag names (such as StudyInstanceUID), and the values are the tag values
+ self.optionalTags = {}
+
+ def progress(self, string):
+ # TODO: make this a callback for a gui progress dialog
+ logging.info(string)
+
+ def export(self):
+ # Perform export
+ success = self.createDICOMFileForScene()
+ return success
+
+ def createDICOMFileForScene(self):
+ """
+ Export the scene data:
+ - first to a directory using the utility in the mrmlScene
+ - create a zip file using the application logic
+ - create secondary capture based on the sample dataset
+ - add the zip file as a private creator tag
+ TODO: confirm that resulting file is valid - may need to change the CLI
+ to include more parameters or do a new implementation ctk/DCMTK
+ See:
+ https://sourceforge.net/apps/mediawiki/gdcm/index.php?title=Writing_DICOM
+ """
+
+ # set up temp directories and files
+ if self.saveDirectoryPath is None:
+ self.saveDirectoryPath = tempfile.mkdtemp('', 'dicomExport', slicer.app.temporaryPath)
+ self.zipFile = os.path.join(self.saveDirectoryPath, "scene.zip")
+ self.dumpFile = os.path.join(self.saveDirectoryPath, "dump.dcm")
+ self.templateFile = os.path.join(self.saveDirectoryPath, "template.dcm")
+ self.sdbFile = os.path.join(self.saveDirectoryPath, "SlicerDataBundle.dcm")
+ if self.studyDescription is None:
+ self.studyDescription = 'Slicer Scene Export'
+ if self.seriesDescription is None:
+ self.seriesDescription = 'Slicer Data Bundle'
+
+ # get the screen image if not specified
+ if self.imageFile is None:
+ self.progress('Saving Image...')
+ self.imageFile = os.path.join(self.saveDirectoryPath, "scene.jpg")
+ image = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow())
+ image.save(self.imageFile)
+ imageReader = vtk.vtkJPEGReader()
+ imageReader.SetFileName(self.imageFile)
+ imageReader.Update()
+
+ # Clean up paths on Windows (some commands and operations are not performed properly with mixed slash and backslash)
+ self.saveDirectoryPath = self.saveDirectoryPath.replace('\\', '/')
+ self.imageFile = self.imageFile.replace('\\', '/')
+ self.zipFile = self.zipFile.replace('\\', '/')
+ self.dumpFile = self.dumpFile.replace('\\', '/')
+ self.templateFile = self.templateFile.replace('\\', '/')
+ self.sdbFile = self.sdbFile.replace('\\', '/')
+
+ # save the scene to the temp dir
+ self.progress('Saving scene into MRB...')
+ if not slicer.mrmlScene.WriteToMRB(self.zipFile, imageReader.GetOutput()):
+ logging.error('Failed to save scene into MRB file: ' + self.zipFile)
+ return False
+
+ zipSize = os.path.getsize(self.zipFile)
+
+ # now create the dicom file
+ # - create the dump (capture stdout)
+ # cmd = "dcmdump --print-all --write-pixel %s %s" % (self.saveDirectoryPath, self.referenceFile)
+ self.progress('Making dicom reference file...')
+ logging.info('Using reference file ' + str(self.referenceFile))
+ args = ['--print-all', '--write-pixel', self.saveDirectoryPath, self.referenceFile]
+ dumpByteArray = DICOMLib.DICOMCommand('dcmdump', args).start()
+ dump = str(dumpByteArray.data(), encoding='utf-8')
+
+ # append this to the dumped output and save the result as self.saveDirectoryPath/dcm.dump
+ # with %s as self.zipFile and %d being its size in bytes
+ zipSizeString = "%d" % zipSize
+
+ # hack: encode the file zip file size as part of the creator string
+ # because none of the normal types (UL, DS, LO) seem to survive
+ # the dump2dcm step (possibly due to the Unknown nature of the private tag)
+ creatorString = "3D Slicer %s" % zipSizeString
+ candygram = """(cadb,0010) LO [%s] # %d, 1 PrivateCreator
(cadb,1008) LO [%s] # 4, 1 Unknown Tag & Data
(cadb,1010) OB =%s # %d, 1 Unknown Tag & Data
""" % (creatorString, len(creatorString), zipSizeString, self.zipFile, zipSize)
- dump = dump + candygram
-
- logging.debug('dumping to: %s' % self.dumpFile)
- fp = open(self.dumpFile, 'w')
- fp.write(dump)
- fp.close()
-
- self.progress('Encapsulating scene in DICOM dump...')
- args = [self.dumpFile, self.templateFile, '--generate-new-uids', '--overwrite-uids', '--ignore-errors']
- DICOMLib.DICOMCommand('dump2dcm', args).start()
-
- # now create the Secondary Capture data set
- # cmd = "img2dcm -k 'InstanceNumber=1' -k 'SeriesDescription=Slicer Data Bundle' -df %s/template.dcm %s %s" % (self.saveDirectoryPath, self.imageFile, self.sdbFile)
- args = [
- '-k', 'InstanceNumber=1',
- '-k', 'StudyDescription=%s' % str(self.studyDescription),
- '-k', 'SeriesDescription=%s' % str(self.seriesDescription),
- '--dataset-from', self.templateFile,
- self.imageFile, self.sdbFile]
- argIndex = 6
- for key, value in self.optionalTags.items():
- args.insert(argIndex, '-k')
- tagNameValue = f'{str(key)}={str(value)}'
- args.insert(argIndex + 1, tagNameValue)
- argIndex += 2
- self.progress('Creating DICOM binary file...')
- DICOMLib.DICOMCommand('img2dcm', args).start()
-
- self.progress('Deleting temporary files...')
- os.remove(self.zipFile)
- os.remove(self.dumpFile)
- os.remove(self.templateFile)
-
- self.progress('Done')
- return True
+ dump = dump + candygram
+
+ logging.debug('dumping to: %s' % self.dumpFile)
+ fp = open(self.dumpFile, 'w')
+ fp.write(dump)
+ fp.close()
+
+ self.progress('Encapsulating scene in DICOM dump...')
+ args = [self.dumpFile, self.templateFile, '--generate-new-uids', '--overwrite-uids', '--ignore-errors']
+ DICOMLib.DICOMCommand('dump2dcm', args).start()
+
+ # now create the Secondary Capture data set
+ # cmd = "img2dcm -k 'InstanceNumber=1' -k 'SeriesDescription=Slicer Data Bundle' -df %s/template.dcm %s %s" % (self.saveDirectoryPath, self.imageFile, self.sdbFile)
+ args = [
+ '-k', 'InstanceNumber=1',
+ '-k', 'StudyDescription=%s' % str(self.studyDescription),
+ '-k', 'SeriesDescription=%s' % str(self.seriesDescription),
+ '--dataset-from', self.templateFile,
+ self.imageFile, self.sdbFile]
+ argIndex = 6
+ for key, value in self.optionalTags.items():
+ args.insert(argIndex, '-k')
+ tagNameValue = f'{str(key)}={str(value)}'
+ args.insert(argIndex + 1, tagNameValue)
+ argIndex += 2
+ self.progress('Creating DICOM binary file...')
+ DICOMLib.DICOMCommand('img2dcm', args).start()
+
+ self.progress('Deleting temporary files...')
+ os.remove(self.zipFile)
+ os.remove(self.dumpFile)
+ os.remove(self.templateFile)
+
+ self.progress('Done')
+ return True
diff --git a/Modules/Scripted/DICOMLib/DICOMPlugin.py b/Modules/Scripted/DICOMLib/DICOMPlugin.py
index 261707b3850..eb9d7a0f995 100644
--- a/Modules/Scripted/DICOMLib/DICOMPlugin.py
+++ b/Modules/Scripted/DICOMLib/DICOMPlugin.py
@@ -22,46 +22,46 @@
#
class DICOMLoadable:
- """Container class for things that can be
- loaded from dicom files into slicer.
- Each plugin returns a list of instances from its
- evaluate method and accepts a list of these
- in its load method corresponding to the things
- the user has selected for loading
- NOTE: This class is deprecated, use qSlicerDICOMLoadable
- instead.
- """
-
- def __init__(self, qLoadable=None):
- if qLoadable is None:
- # the file list of the data to be loaded
- self.files = []
- # name exposed to the user for the node
- self.name = "Unknown"
- # extra information the user sees on mouse over of the thing
- self.tooltip = "No further information available"
- # things the user should know before loading this data
- self.warning = ""
- # is the object checked for loading by default
- self.selected = False
- # confidence - from 0 to 1 where 0 means low chance
- # that the user actually wants to load their data this
- # way up to 1, which means that the plugin is very confident
- # that this is the best way to load the data.
- # When more than one plugin marks the same series as
- # selected, the one with the highest confidence is
- # actually selected by default. In the case of a tie,
- # both series are selected for loading.
- self.confidence = 0.5
- else:
- self.name = qLoadable.name
- self.tooltip = qLoadable.tooltip
- self.warning = qLoadable.warning
- self.files = []
- for file in qLoadable.files:
- self.files.append(file)
- self.selected = qLoadable.selected
- self.confidence = qLoadable.confidence
+ """Container class for things that can be
+ loaded from dicom files into slicer.
+ Each plugin returns a list of instances from its
+ evaluate method and accepts a list of these
+ in its load method corresponding to the things
+ the user has selected for loading
+ NOTE: This class is deprecated, use qSlicerDICOMLoadable
+ instead.
+ """
+
+ def __init__(self, qLoadable=None):
+ if qLoadable is None:
+ # the file list of the data to be loaded
+ self.files = []
+ # name exposed to the user for the node
+ self.name = "Unknown"
+ # extra information the user sees on mouse over of the thing
+ self.tooltip = "No further information available"
+ # things the user should know before loading this data
+ self.warning = ""
+ # is the object checked for loading by default
+ self.selected = False
+ # confidence - from 0 to 1 where 0 means low chance
+ # that the user actually wants to load their data this
+ # way up to 1, which means that the plugin is very confident
+ # that this is the best way to load the data.
+ # When more than one plugin marks the same series as
+ # selected, the one with the highest confidence is
+ # actually selected by default. In the case of a tie,
+ # both series are selected for loading.
+ self.confidence = 0.5
+ else:
+ self.name = qLoadable.name
+ self.tooltip = qLoadable.tooltip
+ self.warning = qLoadable.warning
+ self.files = []
+ for file in qLoadable.files:
+ self.files.append(file)
+ self.selected = qLoadable.selected
+ self.confidence = qLoadable.confidence
#
@@ -69,316 +69,316 @@ def __init__(self, qLoadable=None):
#
class DICOMPlugin:
- """ Base class for DICOM plugins
- """
-
- def __init__(self):
- # displayed for the user as the plugin handling the load
- self.loadType = "Generic DICOM"
- # a dictionary that maps a list of files to a list of loadables
- # (so that subsequent requests for the same info can be
- # serviced quickly)
- self.loadableCache = {}
- # tags is a dictionary of symbolic name keys mapping to
- # hex tag number values (as in {'pixelData': '7fe0,0010'}).
- # Each subclass should define the tags it will be using in
- # calls to the DICOM database so that any needed values
- # can be efficiently pre-fetched if possible.
- self.tags = {}
- self.tags['seriesDescription'] = "0008,103E"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['frameOfReferenceUID'] = "0020,0052"
-
- def findPrivateTag(self, ds, group, element, privateCreator):
- """Helper function to get private tag from private creator name.
- Example:
- ds = pydicom.read_file(...)
- tag = self.findPrivateTag(ds, 0x0021, 0x40, "General Electric Company 01")
- value = ds[tag].value
- """
- for tag, data_element in ds.items():
- if (tag.group == group) and (tag.element < 0x0100):
- data_element_value = data_element.value
- if type(data_element.value) == bytes:
- data_element_value = data_element_value.decode()
- if data_element_value.rstrip() == privateCreator:
- import pydicom as dicom
- return dicom.tag.Tag(group, (tag.element << 8) + element)
- return None
-
- def isDetailedLogging(self):
- """Helper function that returns True if detailed DICOM logging is enabled.
- If enabled then the plugin can log as many details as it wants, even if it
- makes loading slower or adds lots of information to the application log.
- """
- return slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
-
- def hashFiles(self, files):
- """Create a hash key for a list of files"""
- try:
- import hashlib
- except:
- return None
- m = hashlib.md5()
- for f in files:
- # Unicode-objects must be encoded before hashing
- m.update(f.encode('UTF-8', 'ignore'))
- return(m.digest())
-
- def getCachedLoadables(self, files):
- """ Helper method to access the results of a previous
- examination of a list of files"""
- key = self.hashFiles(files)
- if key in self.loadableCache:
- return self.loadableCache[key]
- return None
-
- def cacheLoadables(self, files, loadables):
- """ Helper method to store the results of examining a list
- of files for later quick access"""
- key = self.hashFiles(files)
- self.loadableCache[key] = loadables
-
- def examineForImport(self, fileList):
- """Look at the list of lists of filenames and return
- a list of DICOMLoadables that are options for loading
- Virtual: should be overridden by the subclass
- """
- return []
-
- def examine(self, fileList):
- """Backwards compatibility function for examineForImport
- (renamed on introducing examineForExport to avoid confusion)
+ """ Base class for DICOM plugins
"""
- return self.examineForImport(fileList)
- def load(self, loadable):
- """Accept a DICOMLoadable and perform the operation to convert
- the referenced data into MRML nodes
- Virtual: should be overridden by the subclass
- """
- return True
-
- def examineForExport(self, subjectHierarchyItemID):
- """Return a list of DICOMExportable instances that describe the
- available techniques that this plugin offers to convert MRML
- data associated to a subject hierarchy item into DICOM data
- Virtual: should be overridden by the subclass
- """
- return []
-
- def export(self, exportable):
- """Export an exportable (one series) to file(s)
- Return error message, empty if success
- Virtual: should be overridden by the subclass
- """
- return ""
-
- def defaultSeriesNodeName(self, seriesUID):
- """Generate a name suitable for use as a mrml node name based
- on the series level data in the database"""
- instanceFilePaths = slicer.dicomDatabase.filesForSeries(seriesUID, 1)
- if len(instanceFilePaths) == 0:
- return "Unnamed Series"
- seriesDescription = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesDescription'])
- seriesNumber = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesNumber'])
- name = seriesDescription
- if seriesDescription == "":
- name = "Unnamed Series"
- if seriesNumber != "":
- name = seriesNumber + ": " + name
- return name
-
- def addSeriesInSubjectHierarchy(self, loadable, dataNode):
- """Add loaded DICOM series into subject hierarchy.
- The DICOM tags are read from the first file referenced by the
- given loadable. The dataNode argument is associated to the created
- series node and provides fallback name in case of empty series
- description.
- This function should be called from the load() function of
- each subclass of the DICOMPlugin class.
- """
- tags = {}
- tags['seriesInstanceUID'] = "0020,000E"
- tags['seriesModality'] = "0008,0060"
- tags['seriesNumber'] = "0020,0011"
- tags['frameOfReferenceUID'] = "0020,0052"
- tags['studyInstanceUID'] = "0020,000D"
- tags['studyID'] = "0020,0010"
- tags['studyDescription'] = "0008,1030"
- tags['studyDate'] = "0008,0020"
- tags['studyTime'] = "0008,0030"
- tags['patientID'] = "0010,0020"
- tags['patientName'] = "0010,0010"
- tags['patientSex'] = "0010,0040"
- tags['patientBirthDate'] = "0010,0030"
- tags['patientComments'] = "0010,4000"
- tags['classUID'] = "0008,0016"
- tags['instanceUID'] = "0008,0018"
-
- # Import and check dependencies
- try:
- slicer.vtkSlicerSubjectHierarchyModuleLogic
- except AttributeError:
- logging.error('Unable to create subject hierarchy: Subject Hierarchy module logic not found')
- return
-
- # Validate dataNode argument
- if dataNode is None or not dataNode.IsA('vtkMRMLNode'):
- logging.error('Unable to create subject hierarchy items: invalid data node provided')
- return
-
- # Get first file to access DICOM tags from it
- firstFile = loadable.files[0]
-
- # Get subject hierarchy node and basic IDs
- shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- sceneItemID = shn.GetSceneItemID()
-
- # Set up subject hierarchy item
- seriesItemID = shn.CreateItem(sceneItemID, dataNode)
-
- # Specify details of series item
- seriesInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['seriesInstanceUID'])
- shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), seriesInstanceUid)
- shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesModalityAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['seriesModality']))
- shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesNumberAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['seriesNumber']))
- shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMFrameOfReferenceUIDAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['frameOfReferenceUID']))
- # Set instance UIDs
- instanceUIDs = ""
- for file in loadable.files:
- uid = slicer.dicomDatabase.fileValue(file, tags['instanceUID'])
- if uid == "":
- uid = "Unknown"
- instanceUIDs += uid + " "
- instanceUIDs = instanceUIDs[:-1] # strip last space
- shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMInstanceUIDName(), instanceUIDs)
-
- # Set referenced instance UIDs from loadable to series
- referencedInstanceUIDs = ""
- if hasattr(loadable, 'referencedInstanceUIDs'):
- for instanceUID in loadable.referencedInstanceUIDs:
- referencedInstanceUIDs += instanceUID + " "
- referencedInstanceUIDs = referencedInstanceUIDs[:-1] # strip last space
- shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMReferencedInstanceUIDsAttributeName(),
- referencedInstanceUIDs)
-
- # Add series item to hierarchy under the right study and patient items. If they are present then used, if not, then created
- studyInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['studyInstanceUID'])
- patientId = slicer.dicomDatabase.fileValue(firstFile, tags['patientID'])
- if not patientId:
- # Patient ID tag is required DICOM tag and it cannot be empty. Unfortunately, we may get DICOM files that do not follow
- # the standard (e.g., incorrectly anonymized) and have empty patient tag. We generate a unique ID from the study instance UID.
- # The DICOM browser uses the study instance UID as patient ID directly, but this would not work in the subject hierarchy, because
- # then the DICOM UID of the patient and study tag would be the same, so we add a prefix ("Patient-").
- patientId = "Patient-" + studyInstanceUid
- patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId)
- studyId = slicer.dicomDatabase.fileValue(firstFile, tags['studyID'])
- studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid)
- slicer.vtkSlicerSubjectHierarchyModuleLogic.InsertDicomSeriesInHierarchy(shn, patientId, studyInstanceUid, seriesInstanceUid)
-
- if not patientItemID:
- patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId)
- if patientItemID:
- # Add attributes for DICOM tags
- patientName = slicer.dicomDatabase.fileValue(firstFile, tags['patientName'])
- if patientName == '':
- patientName = 'No name'
-
- shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameAttributeName(),
- patientName)
- shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDAttributeName(),
- patientId)
- shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['patientSex']))
- patientBirthDate = slicer.dicomDatabase.fileValue(firstFile, tags['patientBirthDate'])
- shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateAttributeName(),
- patientBirthDate)
- shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['patientComments']))
- # Set item name
- patientItemName = patientName
- if pluginHandlerSingleton.displayPatientIDInSubjectHierarchyItemName:
- patientItemName += ' (' + str(patientId) + ')'
- if pluginHandlerSingleton.displayPatientBirthDateInSubjectHierarchyItemName and patientBirthDate != '':
- patientItemName += ' (' + str(patientBirthDate) + ')'
- shn.SetItemName(patientItemID, patientItemName)
-
- if not studyItemID:
- studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid)
- if studyItemID:
- # Add attributes for DICOM tags
- studyDescription = slicer.dicomDatabase.fileValue(firstFile, tags['studyDescription'])
- if studyDescription == '':
- studyDescription = 'No study description'
-
- shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionAttributeName(),
- studyDescription)
- studyDate = slicer.dicomDatabase.fileValue(firstFile, tags['studyDate'])
- shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyInstanceUIDAttributeName(),
- studyInstanceUid)
- shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDAttributeName(),
- studyId)
- shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateAttributeName(),
- studyDate)
- shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeAttributeName(),
- slicer.dicomDatabase.fileValue(firstFile, tags['studyTime']))
- # Set item name
- studyItemName = studyDescription
- if pluginHandlerSingleton.displayStudyIDInSubjectHierarchyItemName:
- studyItemName += ' (' + str(studyId) + ')'
- if pluginHandlerSingleton.displayStudyDateInSubjectHierarchyItemName and studyDate != '':
- studyItemName += ' (' + str(studyDate) + ')'
- shn.SetItemName(studyItemID, studyItemName)
-
- def mapSOPClassUIDToModality(self, sopClassUID):
- # Note more specialized definitions can be specified for MR by more
- # specialized plugins, see codes 110800 and on in
- # https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_D.html
- MRname2UID = {
- "MR Image Storage": "1.2.840.10008.5.1.4.1.1.4",
- "Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.1",
- "Legacy Converted Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.4"
- }
- CTname2UID = {
- "CT Image Storage": "1.2.840.10008.5.1.4.1.1.2",
- "Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.1",
- "Legacy Converted Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.2"
- }
- PETname2UID = {
- "Positron Emission Tomography Image Storage": "1.2.840.10008.5.1.4.1.1.128",
- "Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.130",
- "Legacy Converted Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.128.1"
- }
-
- if sopClassUID in MRname2UID.values():
- return "MR"
- elif sopClassUID in CTname2UID.values():
- return "CT"
- elif sopClassUID in PETname2UID.values():
- return "PT"
- else:
- return None
-
- def mapSOPClassUIDToDICOMQuantityAndUnits(self, sopClassUID):
-
- quantity = None
- units = None
-
- modality = self.mapSOPClassUIDToModality(sopClassUID)
- if modality == "MR":
- quantity = slicer.vtkCodedEntry()
- quantity.SetValueSchemeMeaning("110852", "DCM", "MR signal intensity")
- units = slicer.vtkCodedEntry()
- units.SetValueSchemeMeaning("1", "UCUM", "no units")
- elif modality == "CT":
- quantity = slicer.vtkCodedEntry()
- quantity.SetValueSchemeMeaning("112031", "DCM", "Attenuation Coefficient")
- units = slicer.vtkCodedEntry()
- units.SetValueSchemeMeaning("[hnsf'U]", "UCUM", "Hounsfield unit")
-
- return (quantity, units)
+ def __init__(self):
+ # displayed for the user as the plugin handling the load
+ self.loadType = "Generic DICOM"
+ # a dictionary that maps a list of files to a list of loadables
+ # (so that subsequent requests for the same info can be
+ # serviced quickly)
+ self.loadableCache = {}
+ # tags is a dictionary of symbolic name keys mapping to
+ # hex tag number values (as in {'pixelData': '7fe0,0010'}).
+ # Each subclass should define the tags it will be using in
+ # calls to the DICOM database so that any needed values
+ # can be efficiently pre-fetched if possible.
+ self.tags = {}
+ self.tags['seriesDescription'] = "0008,103E"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['frameOfReferenceUID'] = "0020,0052"
+
+ def findPrivateTag(self, ds, group, element, privateCreator):
+ """Helper function to get private tag from private creator name.
+ Example:
+ ds = pydicom.read_file(...)
+ tag = self.findPrivateTag(ds, 0x0021, 0x40, "General Electric Company 01")
+ value = ds[tag].value
+ """
+ for tag, data_element in ds.items():
+ if (tag.group == group) and (tag.element < 0x0100):
+ data_element_value = data_element.value
+ if type(data_element.value) == bytes:
+ data_element_value = data_element_value.decode()
+ if data_element_value.rstrip() == privateCreator:
+ import pydicom as dicom
+ return dicom.tag.Tag(group, (tag.element << 8) + element)
+ return None
+
+ def isDetailedLogging(self):
+ """Helper function that returns True if detailed DICOM logging is enabled.
+ If enabled then the plugin can log as many details as it wants, even if it
+ makes loading slower or adds lots of information to the application log.
+ """
+ return slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
+
+ def hashFiles(self, files):
+ """Create a hash key for a list of files"""
+ try:
+ import hashlib
+ except:
+ return None
+ m = hashlib.md5()
+ for f in files:
+ # Unicode-objects must be encoded before hashing
+ m.update(f.encode('UTF-8', 'ignore'))
+ return(m.digest())
+
+ def getCachedLoadables(self, files):
+ """ Helper method to access the results of a previous
+ examination of a list of files"""
+ key = self.hashFiles(files)
+ if key in self.loadableCache:
+ return self.loadableCache[key]
+ return None
+
+ def cacheLoadables(self, files, loadables):
+ """ Helper method to store the results of examining a list
+ of files for later quick access"""
+ key = self.hashFiles(files)
+ self.loadableCache[key] = loadables
+
+ def examineForImport(self, fileList):
+ """Look at the list of lists of filenames and return
+ a list of DICOMLoadables that are options for loading
+ Virtual: should be overridden by the subclass
+ """
+ return []
+
+ def examine(self, fileList):
+ """Backwards compatibility function for examineForImport
+ (renamed on introducing examineForExport to avoid confusion)
+ """
+ return self.examineForImport(fileList)
+
+ def load(self, loadable):
+ """Accept a DICOMLoadable and perform the operation to convert
+ the referenced data into MRML nodes
+ Virtual: should be overridden by the subclass
+ """
+ return True
+
+ def examineForExport(self, subjectHierarchyItemID):
+ """Return a list of DICOMExportable instances that describe the
+ available techniques that this plugin offers to convert MRML
+ data associated to a subject hierarchy item into DICOM data
+ Virtual: should be overridden by the subclass
+ """
+ return []
+
+ def export(self, exportable):
+ """Export an exportable (one series) to file(s)
+ Return error message, empty if success
+ Virtual: should be overridden by the subclass
+ """
+ return ""
+
+ def defaultSeriesNodeName(self, seriesUID):
+ """Generate a name suitable for use as a mrml node name based
+ on the series level data in the database"""
+ instanceFilePaths = slicer.dicomDatabase.filesForSeries(seriesUID, 1)
+ if len(instanceFilePaths) == 0:
+ return "Unnamed Series"
+ seriesDescription = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesDescription'])
+ seriesNumber = slicer.dicomDatabase.fileValue(instanceFilePaths[0], self.tags['seriesNumber'])
+ name = seriesDescription
+ if seriesDescription == "":
+ name = "Unnamed Series"
+ if seriesNumber != "":
+ name = seriesNumber + ": " + name
+ return name
+
+ def addSeriesInSubjectHierarchy(self, loadable, dataNode):
+ """Add loaded DICOM series into subject hierarchy.
+ The DICOM tags are read from the first file referenced by the
+ given loadable. The dataNode argument is associated to the created
+ series node and provides fallback name in case of empty series
+ description.
+ This function should be called from the load() function of
+ each subclass of the DICOMPlugin class.
+ """
+ tags = {}
+ tags['seriesInstanceUID'] = "0020,000E"
+ tags['seriesModality'] = "0008,0060"
+ tags['seriesNumber'] = "0020,0011"
+ tags['frameOfReferenceUID'] = "0020,0052"
+ tags['studyInstanceUID'] = "0020,000D"
+ tags['studyID'] = "0020,0010"
+ tags['studyDescription'] = "0008,1030"
+ tags['studyDate'] = "0008,0020"
+ tags['studyTime'] = "0008,0030"
+ tags['patientID'] = "0010,0020"
+ tags['patientName'] = "0010,0010"
+ tags['patientSex'] = "0010,0040"
+ tags['patientBirthDate'] = "0010,0030"
+ tags['patientComments'] = "0010,4000"
+ tags['classUID'] = "0008,0016"
+ tags['instanceUID'] = "0008,0018"
+
+ # Import and check dependencies
+ try:
+ slicer.vtkSlicerSubjectHierarchyModuleLogic
+ except AttributeError:
+ logging.error('Unable to create subject hierarchy: Subject Hierarchy module logic not found')
+ return
+
+ # Validate dataNode argument
+ if dataNode is None or not dataNode.IsA('vtkMRMLNode'):
+ logging.error('Unable to create subject hierarchy items: invalid data node provided')
+ return
+
+ # Get first file to access DICOM tags from it
+ firstFile = loadable.files[0]
+
+ # Get subject hierarchy node and basic IDs
+ shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ sceneItemID = shn.GetSceneItemID()
+
+ # Set up subject hierarchy item
+ seriesItemID = shn.CreateItem(sceneItemID, dataNode)
+
+ # Specify details of series item
+ seriesInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['seriesInstanceUID'])
+ shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), seriesInstanceUid)
+ shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesModalityAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['seriesModality']))
+ shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMSeriesNumberAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['seriesNumber']))
+ shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMFrameOfReferenceUIDAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['frameOfReferenceUID']))
+ # Set instance UIDs
+ instanceUIDs = ""
+ for file in loadable.files:
+ uid = slicer.dicomDatabase.fileValue(file, tags['instanceUID'])
+ if uid == "":
+ uid = "Unknown"
+ instanceUIDs += uid + " "
+ instanceUIDs = instanceUIDs[:-1] # strip last space
+ shn.SetItemUID(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMInstanceUIDName(), instanceUIDs)
+
+ # Set referenced instance UIDs from loadable to series
+ referencedInstanceUIDs = ""
+ if hasattr(loadable, 'referencedInstanceUIDs'):
+ for instanceUID in loadable.referencedInstanceUIDs:
+ referencedInstanceUIDs += instanceUID + " "
+ referencedInstanceUIDs = referencedInstanceUIDs[:-1] # strip last space
+ shn.SetItemAttribute(seriesItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMReferencedInstanceUIDsAttributeName(),
+ referencedInstanceUIDs)
+
+ # Add series item to hierarchy under the right study and patient items. If they are present then used, if not, then created
+ studyInstanceUid = slicer.dicomDatabase.fileValue(firstFile, tags['studyInstanceUID'])
+ patientId = slicer.dicomDatabase.fileValue(firstFile, tags['patientID'])
+ if not patientId:
+ # Patient ID tag is required DICOM tag and it cannot be empty. Unfortunately, we may get DICOM files that do not follow
+ # the standard (e.g., incorrectly anonymized) and have empty patient tag. We generate a unique ID from the study instance UID.
+ # The DICOM browser uses the study instance UID as patient ID directly, but this would not work in the subject hierarchy, because
+ # then the DICOM UID of the patient and study tag would be the same, so we add a prefix ("Patient-").
+ patientId = "Patient-" + studyInstanceUid
+ patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId)
+ studyId = slicer.dicomDatabase.fileValue(firstFile, tags['studyID'])
+ studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid)
+ slicer.vtkSlicerSubjectHierarchyModuleLogic.InsertDicomSeriesInHierarchy(shn, patientId, studyInstanceUid, seriesInstanceUid)
+
+ if not patientItemID:
+ patientItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), patientId)
+ if patientItemID:
+ # Add attributes for DICOM tags
+ patientName = slicer.dicomDatabase.fileValue(firstFile, tags['patientName'])
+ if patientName == '':
+ patientName = 'No name'
+
+ shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameAttributeName(),
+ patientName)
+ shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDAttributeName(),
+ patientId)
+ shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['patientSex']))
+ patientBirthDate = slicer.dicomDatabase.fileValue(firstFile, tags['patientBirthDate'])
+ shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateAttributeName(),
+ patientBirthDate)
+ shn.SetItemAttribute(patientItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['patientComments']))
+ # Set item name
+ patientItemName = patientName
+ if pluginHandlerSingleton.displayPatientIDInSubjectHierarchyItemName:
+ patientItemName += ' (' + str(patientId) + ')'
+ if pluginHandlerSingleton.displayPatientBirthDateInSubjectHierarchyItemName and patientBirthDate != '':
+ patientItemName += ' (' + str(patientBirthDate) + ')'
+ shn.SetItemName(patientItemID, patientItemName)
+
+ if not studyItemID:
+ studyItemID = shn.GetItemByUID(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName(), studyInstanceUid)
+ if studyItemID:
+ # Add attributes for DICOM tags
+ studyDescription = slicer.dicomDatabase.fileValue(firstFile, tags['studyDescription'])
+ if studyDescription == '':
+ studyDescription = 'No study description'
+
+ shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionAttributeName(),
+ studyDescription)
+ studyDate = slicer.dicomDatabase.fileValue(firstFile, tags['studyDate'])
+ shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyInstanceUIDAttributeName(),
+ studyInstanceUid)
+ shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDAttributeName(),
+ studyId)
+ shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateAttributeName(),
+ studyDate)
+ shn.SetItemAttribute(studyItemID, slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeAttributeName(),
+ slicer.dicomDatabase.fileValue(firstFile, tags['studyTime']))
+ # Set item name
+ studyItemName = studyDescription
+ if pluginHandlerSingleton.displayStudyIDInSubjectHierarchyItemName:
+ studyItemName += ' (' + str(studyId) + ')'
+ if pluginHandlerSingleton.displayStudyDateInSubjectHierarchyItemName and studyDate != '':
+ studyItemName += ' (' + str(studyDate) + ')'
+ shn.SetItemName(studyItemID, studyItemName)
+
+ def mapSOPClassUIDToModality(self, sopClassUID):
+ # Note more specialized definitions can be specified for MR by more
+ # specialized plugins, see codes 110800 and on in
+ # https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_D.html
+ MRname2UID = {
+ "MR Image Storage": "1.2.840.10008.5.1.4.1.1.4",
+ "Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.1",
+ "Legacy Converted Enhanced MR Image Storage": "1.2.840.10008.5.1.4.1.1.4.4"
+ }
+ CTname2UID = {
+ "CT Image Storage": "1.2.840.10008.5.1.4.1.1.2",
+ "Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.1",
+ "Legacy Converted Enhanced CT Image Storage": "1.2.840.10008.5.1.4.1.1.2.2"
+ }
+ PETname2UID = {
+ "Positron Emission Tomography Image Storage": "1.2.840.10008.5.1.4.1.1.128",
+ "Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.130",
+ "Legacy Converted Enhanced PET Image Storage": "1.2.840.10008.5.1.4.1.1.128.1"
+ }
+
+ if sopClassUID in MRname2UID.values():
+ return "MR"
+ elif sopClassUID in CTname2UID.values():
+ return "CT"
+ elif sopClassUID in PETname2UID.values():
+ return "PT"
+ else:
+ return None
+
+ def mapSOPClassUIDToDICOMQuantityAndUnits(self, sopClassUID):
+
+ quantity = None
+ units = None
+
+ modality = self.mapSOPClassUIDToModality(sopClassUID)
+ if modality == "MR":
+ quantity = slicer.vtkCodedEntry()
+ quantity.SetValueSchemeMeaning("110852", "DCM", "MR signal intensity")
+ units = slicer.vtkCodedEntry()
+ units.SetValueSchemeMeaning("1", "UCUM", "no units")
+ elif modality == "CT":
+ quantity = slicer.vtkCodedEntry()
+ quantity.SetValueSchemeMeaning("112031", "DCM", "Attenuation Coefficient")
+ units = slicer.vtkCodedEntry()
+ units.SetValueSchemeMeaning("[hnsf'U]", "UCUM", "Hounsfield unit")
+
+ return (quantity, units)
diff --git a/Modules/Scripted/DICOMLib/DICOMPluginSelector.py b/Modules/Scripted/DICOMLib/DICOMPluginSelector.py
index 106d56353fb..26971cef4e7 100644
--- a/Modules/Scripted/DICOMLib/DICOMPluginSelector.py
+++ b/Modules/Scripted/DICOMLib/DICOMPluginSelector.py
@@ -4,58 +4,58 @@
class DICOMPluginSelector(qt.QWidget):
- """Implement the Qt code for a table of
- selectable DICOM Plugins that determine
- which mappings from DICOM to slicer datatypes
- will be considered.
- """
-
- def __init__(self, parent, width=50, height=100):
- super().__init__(parent)
- self.setMinimumHeight(height)
- self.setMinimumWidth(width)
- verticalBox = qt.QVBoxLayout()
- self.checkBoxByPlugin = {}
-
- for pluginClass in slicer.modules.dicomPlugins:
- self.checkBoxByPlugin[pluginClass] = qt.QCheckBox(pluginClass)
- verticalBox.addWidget(self.checkBoxByPlugin[pluginClass])
-
- # Pack vertical box with plugins into a scroll area widget
- verticalBoxWidget = qt.QWidget()
- scrollAreaBox = qt.QVBoxLayout()
- verticalBoxWidget.setLayout(verticalBox)
- scrollArea = qt.QScrollArea()
- scrollArea.setWidget(verticalBoxWidget)
- scrollAreaBox.addWidget(scrollArea)
- self.setLayout(scrollAreaBox)
- settings = qt.QSettings()
-
- if settings.contains('DICOM/disabledPlugins/size'):
- size = settings.beginReadArray('DICOM/disabledPlugins')
- disabledPlugins = []
-
- for i in range(size):
- settings.setArrayIndex(i)
- disabledPlugins.append(str(settings.allKeys()[0]))
- settings.endArray()
-
- for pluginClass in slicer.modules.dicomPlugins:
- if pluginClass in disabledPlugins:
- self.checkBoxByPlugin[pluginClass].checked = False
+ """Implement the Qt code for a table of
+ selectable DICOM Plugins that determine
+ which mappings from DICOM to slicer datatypes
+ will be considered.
+ """
+
+ def __init__(self, parent, width=50, height=100):
+ super().__init__(parent)
+ self.setMinimumHeight(height)
+ self.setMinimumWidth(width)
+ verticalBox = qt.QVBoxLayout()
+ self.checkBoxByPlugin = {}
+
+ for pluginClass in slicer.modules.dicomPlugins:
+ self.checkBoxByPlugin[pluginClass] = qt.QCheckBox(pluginClass)
+ verticalBox.addWidget(self.checkBoxByPlugin[pluginClass])
+
+ # Pack vertical box with plugins into a scroll area widget
+ verticalBoxWidget = qt.QWidget()
+ scrollAreaBox = qt.QVBoxLayout()
+ verticalBoxWidget.setLayout(verticalBox)
+ scrollArea = qt.QScrollArea()
+ scrollArea.setWidget(verticalBoxWidget)
+ scrollAreaBox.addWidget(scrollArea)
+ self.setLayout(scrollAreaBox)
+ settings = qt.QSettings()
+
+ if settings.contains('DICOM/disabledPlugins/size'):
+ size = settings.beginReadArray('DICOM/disabledPlugins')
+ disabledPlugins = []
+
+ for i in range(size):
+ settings.setArrayIndex(i)
+ disabledPlugins.append(str(settings.allKeys()[0]))
+ settings.endArray()
+
+ for pluginClass in slicer.modules.dicomPlugins:
+ if pluginClass in disabledPlugins:
+ self.checkBoxByPlugin[pluginClass].checked = False
+ else:
+ # Activate plugins for the ones who are not in the disabled list
+ # and also plugins installed with extensions
+ self.checkBoxByPlugin[pluginClass].checked = True
else:
- # Activate plugins for the ones who are not in the disabled list
- # and also plugins installed with extensions
- self.checkBoxByPlugin[pluginClass].checked = True
- else:
- # All DICOM plugins would be enabled by default
- for pluginClass in slicer.modules.dicomPlugins:
- self.checkBoxByPlugin[pluginClass].checked = True
-
- def selectedPlugins(self):
- """Return a list of selected plugins"""
- selectedPlugins = []
- for pluginClass in slicer.modules.dicomPlugins:
- if self.checkBoxByPlugin[pluginClass].checked:
- selectedPlugins.append(pluginClass)
- return selectedPlugins
+ # All DICOM plugins would be enabled by default
+ for pluginClass in slicer.modules.dicomPlugins:
+ self.checkBoxByPlugin[pluginClass].checked = True
+
+ def selectedPlugins(self):
+ """Return a list of selected plugins"""
+ selectedPlugins = []
+ for pluginClass in slicer.modules.dicomPlugins:
+ if self.checkBoxByPlugin[pluginClass].checked:
+ selectedPlugins.append(pluginClass)
+ return selectedPlugins
diff --git a/Modules/Scripted/DICOMLib/DICOMProcesses.py b/Modules/Scripted/DICOMLib/DICOMProcesses.py
index 433244dd179..a2f8e83f692 100644
--- a/Modules/Scripted/DICOMLib/DICOMProcesses.py
+++ b/Modules/Scripted/DICOMLib/DICOMProcesses.py
@@ -30,596 +30,596 @@
class DICOMProcess:
- """helper class to run dcmtk's executables
- Code here depends only on python and DCMTK executables
- """
-
- def __init__(self):
- self.process = None
- self.connections = {}
- pathOptions = (
- '/../DCMTK-build/bin/Debug',
- '/../DCMTK-build/bin/Release',
- '/../DCMTK-build/bin/RelWithDebInfo',
- '/../DCMTK-build/bin/MinSizeRel',
- '/../DCMTK-build/bin',
- '/../CTK-build/CMakeExternals/Install/bin',
- '/bin'
- )
+ """helper class to run dcmtk's executables
+ Code here depends only on python and DCMTK executables
+ """
- self.exeDir = None
- for path in pathOptions:
- testPath = slicer.app.slicerHome + path
- if os.path.exists(testPath):
- self.exeDir = testPath
- break
- if not self.exeDir:
- raise UserWarning("Could not find a valid path to DICOM helper applications")
-
- self.exeExtension = ""
- if os.name == 'nt':
- self.exeExtension = '.exe'
-
- self.QProcessState = {0: 'NotRunning', 1: 'Starting', 2: 'Running', }
-
- def __del__(self):
- self.stop()
-
- def start(self, cmd, args):
- if self.process is not None:
- self.stop()
- self.cmd = cmd
- self.args = args
-
- # start the server!
- self.process = qt.QProcess()
- self.process.connect('stateChanged(QProcess::ProcessState)', self.onStateChanged)
- logging.debug(("Starting %s with " % cmd, args))
- self.process.start(cmd, args)
-
- def onStateChanged(self, newState):
- logging.debug(f"Process {self.cmd} now in state {self.QProcessState[newState]}")
- if newState == 0 and self.process:
- stdout = self.process.readAllStandardOutput()
- stderr = self.process.readAllStandardError()
- logging.debug('DICOM process error code is: %d' % self.process.error())
- logging.debug('DICOM process standard out is: %s' % stdout)
- logging.debug('DICOM process standard error is: %s' % stderr)
- return stdout, stderr
- return None, None
-
- def stop(self):
- if hasattr(self, 'process'):
- if self.process:
- logging.debug("stopping DICOM process")
- self.process.kill()
- # Wait up to 3 seconds for the process to stop
- self.process.waitForFinished(3000)
+ def __init__(self):
self.process = None
+ self.connections = {}
+ pathOptions = (
+ '/../DCMTK-build/bin/Debug',
+ '/../DCMTK-build/bin/Release',
+ '/../DCMTK-build/bin/RelWithDebInfo',
+ '/../DCMTK-build/bin/MinSizeRel',
+ '/../DCMTK-build/bin',
+ '/../CTK-build/CMakeExternals/Install/bin',
+ '/bin'
+ )
+
+ self.exeDir = None
+ for path in pathOptions:
+ testPath = slicer.app.slicerHome + path
+ if os.path.exists(testPath):
+ self.exeDir = testPath
+ break
+ if not self.exeDir:
+ raise UserWarning("Could not find a valid path to DICOM helper applications")
+
+ self.exeExtension = ""
+ if os.name == 'nt':
+ self.exeExtension = '.exe'
+
+ self.QProcessState = {0: 'NotRunning', 1: 'Starting', 2: 'Running', }
+
+ def __del__(self):
+ self.stop()
+
+ def start(self, cmd, args):
+ if self.process is not None:
+ self.stop()
+ self.cmd = cmd
+ self.args = args
+
+ # start the server!
+ self.process = qt.QProcess()
+ self.process.connect('stateChanged(QProcess::ProcessState)', self.onStateChanged)
+ logging.debug(("Starting %s with " % cmd, args))
+ self.process.start(cmd, args)
+
+ def onStateChanged(self, newState):
+ logging.debug(f"Process {self.cmd} now in state {self.QProcessState[newState]}")
+ if newState == 0 and self.process:
+ stdout = self.process.readAllStandardOutput()
+ stderr = self.process.readAllStandardError()
+ logging.debug('DICOM process error code is: %d' % self.process.error())
+ logging.debug('DICOM process standard out is: %s' % stdout)
+ logging.debug('DICOM process standard error is: %s' % stderr)
+ return stdout, stderr
+ return None, None
+
+ def stop(self):
+ if hasattr(self, 'process'):
+ if self.process:
+ logging.debug("stopping DICOM process")
+ self.process.kill()
+ # Wait up to 3 seconds for the process to stop
+ self.process.waitForFinished(3000)
+ self.process = None
class DICOMCommand(DICOMProcess):
- """
- Run a generic dcmtk command and return the stdout
- """
-
- def __init__(self, cmd, args):
- super().__init__()
- self.executable = self.exeDir + '/' + cmd + self.exeExtension
- self.args = args
-
- def __del__(self):
- super().__del__()
-
- def start(self):
- # run the process!
- self.process = qt.QProcess()
- logging.debug(('DICOM process running: ', self.executable, self.args))
- self.process.start(self.executable, self.args)
- self.process.waitForFinished()
- if self.process.exitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0:
- stdout = self.process.readAllStandardOutput()
- stderr = self.process.readAllStandardError()
- logging.debug('DICOM process exit status is: %d' % self.process.exitStatus())
- logging.debug('DICOM process exit code is: %d' % self.process.exitCode())
- logging.debug('DICOM process error is: %d' % self.process.error())
- logging.debug('DICOM process standard out is: %s' % stdout)
- logging.debug('DICOM process standard error is: %s' % stderr)
- raise UserWarning(f"Could not run {self.executable} with {self.args}")
- stdout = self.process.readAllStandardOutput()
- return stdout
+ """
+ Run a generic dcmtk command and return the stdout
+ """
+
+ def __init__(self, cmd, args):
+ super().__init__()
+ self.executable = self.exeDir + '/' + cmd + self.exeExtension
+ self.args = args
+
+ def __del__(self):
+ super().__del__()
+
+ def start(self):
+ # run the process!
+ self.process = qt.QProcess()
+ logging.debug(('DICOM process running: ', self.executable, self.args))
+ self.process.start(self.executable, self.args)
+ self.process.waitForFinished()
+ if self.process.exitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0:
+ stdout = self.process.readAllStandardOutput()
+ stderr = self.process.readAllStandardError()
+ logging.debug('DICOM process exit status is: %d' % self.process.exitStatus())
+ logging.debug('DICOM process exit code is: %d' % self.process.exitCode())
+ logging.debug('DICOM process error is: %d' % self.process.error())
+ logging.debug('DICOM process standard out is: %s' % stdout)
+ logging.debug('DICOM process standard error is: %s' % stderr)
+ raise UserWarning(f"Could not run {self.executable} with {self.args}")
+ stdout = self.process.readAllStandardOutput()
+ return stdout
class DICOMStoreSCPProcess(DICOMProcess):
- """helper class to run dcmtk's storescp
- Code here depends only on python and DCMTK executables
- TODO: it might make sense to refactor this as a generic tool
- for interacting with DCMTK
- """
-
- STORESCP_PROCESS_FILE_NAME = "storescp"
-
- def __init__(self, incomingDataDir, incomingPort=None):
- super().__init__()
-
- self.incomingDataDir = incomingDataDir
- if not os.path.exists(self.incomingDataDir):
- os.mkdir(self.incomingDataDir)
-
- if incomingPort:
- assert isinstance(incomingPort, int)
- self.port = str(incomingPort)
- else:
- settings = qt.QSettings()
- self.port = settings.value('StoragePort')
- if not self.port:
- settings.setValue('StoragePort', '11112')
- self.port = settings.value('StoragePort')
-
- self.storescpExecutable = os.path.join(self.exeDir, self.STORESCP_PROCESS_FILE_NAME + self.exeExtension)
- self.dcmdumpExecutable = os.path.join(self.exeDir, 'dcmdump' + self.exeExtension)
-
- def __del__(self):
- super().__del__()
-
- def onStateChanged(self, newState):
- stdout, stderr = super().onStateChanged(newState)
- if stderr and stderr.size():
- slicer.util.errorDisplay("An error occurred. For further information click 'Show Details...'",
- windowTitle=self.__class__.__name__, detailedText=str(stderr))
- return stdout, stderr
-
- def start(self, cmd=None, args=None):
- # Offer to terminate running SCP processes.
- # They may be started by other applications, listening on other ports, so we try to start ours anyway.
- self.killStoreSCPProcesses()
- onReceptionCallback = '%s --load-short --print-short --print-filename --search PatientName "%s/#f"' \
- % (self.dcmdumpExecutable, self.incomingDataDir)
- args = [str(self.port), '--accept-all', '--output-directory', self.incomingDataDir, '--exec-sync',
- '--exec-on-reception', onReceptionCallback]
- logging.debug("Starting storescp process")
- super().start(self.storescpExecutable, args)
- self.process.connect('readyReadStandardOutput()', self.readFromStandardOutput)
-
- def killStoreSCPProcesses(self):
- uniqueListener = True
- if os.name == 'nt':
- uniqueListener = self.killStoreSCPProcessesNT(uniqueListener)
- elif os.name == 'posix':
- uniqueListener = self.killStoreSCPProcessesPosix(uniqueListener)
- return uniqueListener
-
- def killStoreSCPProcessesPosix(self, uniqueListener):
- p = subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE)
- out, err = p.communicate()
- for line in out.splitlines():
- line = line.decode()
- if self.STORESCP_PROCESS_FILE_NAME in line:
- pid = int(line.split(None, 1)[0])
- uniqueListener = self.notifyUserAboutRunningStoreSCP(pid)
- return uniqueListener
-
- def findAndKillProcessNT(self, processName, killProcess):
- """Find (and optionally terminate) processes by the specified name.
- Returns true if process by that name exists (after attempting to
- terminate the process).
+ """helper class to run dcmtk's storescp
+ Code here depends only on python and DCMTK executables
+ TODO: it might make sense to refactor this as a generic tool
+ for interacting with DCMTK
"""
- import ctypes
- import ctypes.wintypes
- import os.path
-
- psapi = ctypes.WinDLL('Psapi.dll')
- enum_processes = psapi.EnumProcesses
- enum_processes.restype = ctypes.wintypes.BOOL
- get_process_image_file_name = psapi.GetProcessImageFileNameA
- get_process_image_file_name.restype = ctypes.wintypes.DWORD
-
- kernel32 = ctypes.WinDLL('kernel32.dll')
- open_process = kernel32.OpenProcess
- open_process.restype = ctypes.wintypes.HANDLE
- terminate_process = kernel32.TerminateProcess
- terminate_process.restype = ctypes.wintypes.BOOL
- close_handle = kernel32.CloseHandle
-
- MAX_PATH = 260
- PROCESS_TERMINATE = 0x0001
- PROCESS_QUERY_INFORMATION = 0x0400
-
- count = 512
- while True:
- process_ids = (ctypes.wintypes.DWORD * count)()
- cb = ctypes.sizeof(process_ids)
- bytes_returned = ctypes.wintypes.DWORD()
- if enum_processes(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)):
- if bytes_returned.value < cb:
- break
+
+ STORESCP_PROCESS_FILE_NAME = "storescp"
+
+ def __init__(self, incomingDataDir, incomingPort=None):
+ super().__init__()
+
+ self.incomingDataDir = incomingDataDir
+ if not os.path.exists(self.incomingDataDir):
+ os.mkdir(self.incomingDataDir)
+
+ if incomingPort:
+ assert isinstance(incomingPort, int)
+ self.port = str(incomingPort)
else:
- count *= 2
- else:
- logging.error("Call to EnumProcesses failed")
+ settings = qt.QSettings()
+ self.port = settings.value('StoragePort')
+ if not self.port:
+ settings.setValue('StoragePort', '11112')
+ self.port = settings.value('StoragePort')
+
+ self.storescpExecutable = os.path.join(self.exeDir, self.STORESCP_PROCESS_FILE_NAME + self.exeExtension)
+ self.dcmdumpExecutable = os.path.join(self.exeDir, 'dcmdump' + self.exeExtension)
+
+ def __del__(self):
+ super().__del__()
+
+ def onStateChanged(self, newState):
+ stdout, stderr = super().onStateChanged(newState)
+ if stderr and stderr.size():
+ slicer.util.errorDisplay("An error occurred. For further information click 'Show Details...'",
+ windowTitle=self.__class__.__name__, detailedText=str(stderr))
+ return stdout, stderr
+
+ def start(self, cmd=None, args=None):
+ # Offer to terminate running SCP processes.
+ # They may be started by other applications, listening on other ports, so we try to start ours anyway.
+ self.killStoreSCPProcesses()
+ onReceptionCallback = '%s --load-short --print-short --print-filename --search PatientName "%s/#f"' \
+ % (self.dcmdumpExecutable, self.incomingDataDir)
+ args = [str(self.port), '--accept-all', '--output-directory', self.incomingDataDir, '--exec-sync',
+ '--exec-on-reception', onReceptionCallback]
+ logging.debug("Starting storescp process")
+ super().start(self.storescpExecutable, args)
+ self.process.connect('readyReadStandardOutput()', self.readFromStandardOutput)
+
+ def killStoreSCPProcesses(self):
+ uniqueListener = True
+ if os.name == 'nt':
+ uniqueListener = self.killStoreSCPProcessesNT(uniqueListener)
+ elif os.name == 'posix':
+ uniqueListener = self.killStoreSCPProcessesPosix(uniqueListener)
+ return uniqueListener
+
+ def killStoreSCPProcessesPosix(self, uniqueListener):
+ p = subprocess.Popen(['ps', '-A'], stdout=subprocess.PIPE)
+ out, err = p.communicate()
+ for line in out.splitlines():
+ line = line.decode()
+ if self.STORESCP_PROCESS_FILE_NAME in line:
+ pid = int(line.split(None, 1)[0])
+ uniqueListener = self.notifyUserAboutRunningStoreSCP(pid)
+ return uniqueListener
+
+ def findAndKillProcessNT(self, processName, killProcess):
+ """Find (and optionally terminate) processes by the specified name.
+ Returns true if process by that name exists (after attempting to
+ terminate the process).
+ """
+ import ctypes
+ import ctypes.wintypes
+ import os.path
+
+ psapi = ctypes.WinDLL('Psapi.dll')
+ enum_processes = psapi.EnumProcesses
+ enum_processes.restype = ctypes.wintypes.BOOL
+ get_process_image_file_name = psapi.GetProcessImageFileNameA
+ get_process_image_file_name.restype = ctypes.wintypes.DWORD
+
+ kernel32 = ctypes.WinDLL('kernel32.dll')
+ open_process = kernel32.OpenProcess
+ open_process.restype = ctypes.wintypes.HANDLE
+ terminate_process = kernel32.TerminateProcess
+ terminate_process.restype = ctypes.wintypes.BOOL
+ close_handle = kernel32.CloseHandle
+
+ MAX_PATH = 260
+ PROCESS_TERMINATE = 0x0001
+ PROCESS_QUERY_INFORMATION = 0x0400
+
+ count = 512
+ while True:
+ process_ids = (ctypes.wintypes.DWORD * count)()
+ cb = ctypes.sizeof(process_ids)
+ bytes_returned = ctypes.wintypes.DWORD()
+ if enum_processes(ctypes.byref(process_ids), cb, ctypes.byref(bytes_returned)):
+ if bytes_returned.value < cb:
+ break
+ else:
+ count *= 2
+ else:
+ logging.error("Call to EnumProcesses failed")
+ return False
+
+ processMayBeStillRunning = False
+
+ for index in range(int(bytes_returned.value / ctypes.sizeof(ctypes.wintypes.DWORD))):
+ process_id = process_ids[index]
+ h_process = open_process(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, False, process_id)
+ if h_process:
+ image_file_name = (ctypes.c_char * MAX_PATH)()
+ if get_process_image_file_name(h_process, image_file_name, MAX_PATH) > 0:
+ filename = os.path.basename(image_file_name.value)
+ if filename.decode() == processName:
+ # Found the process we are looking for
+ if not killProcess:
+ # we don't need to kill the process, just indicate that there is a process to kill
+ res = close_handle(h_process)
+ return True
+ if not terminate_process(h_process, 1):
+ # failed to terminate process, it may be still running
+ processMayBeStillRunning = True
+
+ res = close_handle(h_process)
+
+ return processMayBeStillRunning
+
+ def isStoreSCPProcessesRunningNT(self):
+ return self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False)
+
+ def killStoreSCPProcessesNT(self, uniqueListener):
+ if self.isStoreSCPProcessesRunningNT():
+ uniqueListener = self.notifyUserAboutRunningStoreSCP()
+ return uniqueListener
+
+ def readFromStandardOutput(self, readLineCallback=None):
+ lines = []
+ while self.process.canReadLine():
+ line = str(self.process.readLine())
+ lines.append(line)
+ logging.debug("Output from {}: {}".format(self.__class__.__name__, "\n".join(lines)))
+ if readLineCallback:
+ for line in lines:
+ # Remove stray newline and single-quote characters
+ clearLine = line.replace('\\r', '').replace('\\n', '').replace('\'', '').strip()
+ readLineCallback(clearLine)
+ self.readFromStandardError()
+
+ def readFromStandardError(self):
+ stdErr = str(self.process.readAllStandardError())
+ if stdErr:
+ logging.debug(f"Error output from {self.__class__.__name__}: {stdErr}")
+
+ def notifyUserAboutRunningStoreSCP(self, pid=None):
+ if slicer.util.confirmYesNoDisplay('There are other DICOM listeners running.\n Do you want to end them?'):
+ if os.name == 'nt':
+ self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, True)
+ # Killing processes can take a while, so we retry a couple of times until we confirm that there
+ # are no more listeners.
+ retryAttempts = 5
+ while retryAttempts:
+ if not self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False):
+ break
+ retryAttempts -= 1
+ time.sleep(1)
+ elif os.name == 'posix':
+ import signal
+ os.kill(pid, signal.SIGKILL)
+ return True
return False
- processMayBeStillRunning = False
-
- for index in range(int(bytes_returned.value / ctypes.sizeof(ctypes.wintypes.DWORD))):
- process_id = process_ids[index]
- h_process = open_process(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, False, process_id)
- if h_process:
- image_file_name = (ctypes.c_char * MAX_PATH)()
- if get_process_image_file_name(h_process, image_file_name, MAX_PATH) > 0:
- filename = os.path.basename(image_file_name.value)
- if filename.decode() == processName:
- # Found the process we are looking for
- if not killProcess:
- # we don't need to kill the process, just indicate that there is a process to kill
- res = close_handle(h_process)
- return True
- if not terminate_process(h_process, 1):
- # failed to terminate process, it may be still running
- processMayBeStillRunning = True
-
- res = close_handle(h_process)
-
- return processMayBeStillRunning
-
- def isStoreSCPProcessesRunningNT(self):
- return self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False)
-
- def killStoreSCPProcessesNT(self, uniqueListener):
- if self.isStoreSCPProcessesRunningNT():
- uniqueListener = self.notifyUserAboutRunningStoreSCP()
- return uniqueListener
-
- def readFromStandardOutput(self, readLineCallback=None):
- lines = []
- while self.process.canReadLine():
- line = str(self.process.readLine())
- lines.append(line)
- logging.debug("Output from {}: {}".format(self.__class__.__name__, "\n".join(lines)))
- if readLineCallback:
- for line in lines:
- # Remove stray newline and single-quote characters
- clearLine = line.replace('\\r', '').replace('\\n', '').replace('\'', '').strip()
- readLineCallback(clearLine)
- self.readFromStandardError()
-
- def readFromStandardError(self):
- stdErr = str(self.process.readAllStandardError())
- if stdErr:
- logging.debug(f"Error output from {self.__class__.__name__}: {stdErr}")
-
- def notifyUserAboutRunningStoreSCP(self, pid=None):
- if slicer.util.confirmYesNoDisplay('There are other DICOM listeners running.\n Do you want to end them?'):
- if os.name == 'nt':
- self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, True)
- # Killing processes can take a while, so we retry a couple of times until we confirm that there
- # are no more listeners.
- retryAttempts = 5
- while retryAttempts:
- if not self.findAndKillProcessNT(self.STORESCP_PROCESS_FILE_NAME + self.exeExtension, False):
- break
- retryAttempts -= 1
- time.sleep(1)
- elif os.name == 'posix':
- import signal
- os.kill(pid, signal.SIGKILL)
- return True
- return False
-
class DICOMListener(DICOMStoreSCPProcess):
- """helper class that uses storscp process including indexing
- into Slicer DICOMdatabase.
- TODO: down the line we might have ctkDICOMListener perform
- this task as a QObject callable from PythonQt
- """
-
- def __init__(self, database, fileToBeAddedCallback=None, fileAddedCallback=None):
- self.dicomDatabase = database
- self.indexer = ctk.ctkDICOMIndexer()
- # Enable background indexing to improve performance.
- self.indexer.backgroundImportEnabled = True
- self.fileToBeAddedCallback = fileToBeAddedCallback
- self.fileAddedCallback = fileAddedCallback
- self.lastFileAdded = None
-
- # A timer is used to ensure that indexing is completed after new files come in,
- # but without enforcing completing the indexing after each file (because
- # waiting for indexing to be completed has an overhead).
- autoUpdateDelaySec = 10.0
- self.delayedAutoUpdateTimer = qt.QTimer()
- self.delayedAutoUpdateTimer.setSingleShot(True)
- self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000
- self.delayedAutoUpdateTimer.connect('timeout()', self.completeIncomingFilesIndexing)
-
- # List of received files that are being indexed
- self.incomingFiles = []
- # After self.incomingFiles reaches maximumIncomingFiles, indexing will be forced
- # to limit disk space usage (until indexing is completed, the file is present both
- # in the incoming folder and in the database) and make sure some updates are visible
- # in the DICOM browser (even if files are continuously coming in).
- # Smaller values result in more frequent updates, slightly less disk space usage,
- # slightly slower import.
- self.maximumIncomingFiles = 400
-
- databaseDirectory = self.dicomDatabase.databaseDirectory
- if not databaseDirectory:
- raise UserWarning('Database directory not set: cannot start DICOMListener')
- if not os.path.exists(databaseDirectory):
- os.mkdir(databaseDirectory)
- incomingDir = databaseDirectory + "/incoming"
- super().__init__(incomingDataDir=incomingDir)
-
- def __del__(self):
- super().__del__()
-
- def readFromStandardOutput(self):
- super().readFromStandardOutput(readLineCallback=self.processStdoutLine)
-
- def completeIncomingFilesIndexing(self):
- """Complete indexing of all incoming files and remove them from the incoming folder."""
- logging.debug(f"Complete indexing for indexing to complete for {len(self.incomingFiles)} files.")
- import os
- self.indexer.waitForImportFinished()
- for dicomFilePath in self.incomingFiles:
- os.remove(dicomFilePath)
- self.incomingFiles = []
-
- def processStdoutLine(self, line):
- searchTag = '# dcmdump (1/1): '
- tagStart = line.find(searchTag)
- if tagStart != -1:
- dicomFilePath = line[tagStart + len(searchTag):].strip()
- slicer.dicomFilePath = dicomFilePath
- logging.debug("indexing: %s " % dicomFilePath)
- if self.fileToBeAddedCallback:
- self.fileToBeAddedCallback()
- self.indexer.addFile(self.dicomDatabase, dicomFilePath, True)
- self.incomingFiles.append(dicomFilePath)
- if len(self.incomingFiles) < self.maximumIncomingFiles:
- self.delayedAutoUpdateTimer.start()
- else:
- # Limit of pending incoming files is reached, complete indexing of files
- # that we have received so far.
- self.delayedAutoUpdateTimer.stop()
- self.completeIncomingFilesIndexing()
- self.lastFileAdded = dicomFilePath
- if self.fileAddedCallback:
- logging.debug("calling callback...")
- self.fileAddedCallback()
- logging.debug("callback done")
- else:
- logging.debug("no callback")
+ """helper class that uses storscp process including indexing
+ into Slicer DICOMdatabase.
+ TODO: down the line we might have ctkDICOMListener perform
+ this task as a QObject callable from PythonQt
+ """
+
+ def __init__(self, database, fileToBeAddedCallback=None, fileAddedCallback=None):
+ self.dicomDatabase = database
+ self.indexer = ctk.ctkDICOMIndexer()
+ # Enable background indexing to improve performance.
+ self.indexer.backgroundImportEnabled = True
+ self.fileToBeAddedCallback = fileToBeAddedCallback
+ self.fileAddedCallback = fileAddedCallback
+ self.lastFileAdded = None
+
+ # A timer is used to ensure that indexing is completed after new files come in,
+ # but without enforcing completing the indexing after each file (because
+ # waiting for indexing to be completed has an overhead).
+ autoUpdateDelaySec = 10.0
+ self.delayedAutoUpdateTimer = qt.QTimer()
+ self.delayedAutoUpdateTimer.setSingleShot(True)
+ self.delayedAutoUpdateTimer.interval = autoUpdateDelaySec * 1000
+ self.delayedAutoUpdateTimer.connect('timeout()', self.completeIncomingFilesIndexing)
+
+ # List of received files that are being indexed
+ self.incomingFiles = []
+ # After self.incomingFiles reaches maximumIncomingFiles, indexing will be forced
+ # to limit disk space usage (until indexing is completed, the file is present both
+ # in the incoming folder and in the database) and make sure some updates are visible
+ # in the DICOM browser (even if files are continuously coming in).
+ # Smaller values result in more frequent updates, slightly less disk space usage,
+ # slightly slower import.
+ self.maximumIncomingFiles = 400
+
+ databaseDirectory = self.dicomDatabase.databaseDirectory
+ if not databaseDirectory:
+ raise UserWarning('Database directory not set: cannot start DICOMListener')
+ if not os.path.exists(databaseDirectory):
+ os.mkdir(databaseDirectory)
+ incomingDir = databaseDirectory + "/incoming"
+ super().__init__(incomingDataDir=incomingDir)
+
+ def __del__(self):
+ super().__del__()
+
+ def readFromStandardOutput(self):
+ super().readFromStandardOutput(readLineCallback=self.processStdoutLine)
+
+ def completeIncomingFilesIndexing(self):
+ """Complete indexing of all incoming files and remove them from the incoming folder."""
+ logging.debug(f"Complete indexing for indexing to complete for {len(self.incomingFiles)} files.")
+ import os
+ self.indexer.waitForImportFinished()
+ for dicomFilePath in self.incomingFiles:
+ os.remove(dicomFilePath)
+ self.incomingFiles = []
+
+ def processStdoutLine(self, line):
+ searchTag = '# dcmdump (1/1): '
+ tagStart = line.find(searchTag)
+ if tagStart != -1:
+ dicomFilePath = line[tagStart + len(searchTag):].strip()
+ slicer.dicomFilePath = dicomFilePath
+ logging.debug("indexing: %s " % dicomFilePath)
+ if self.fileToBeAddedCallback:
+ self.fileToBeAddedCallback()
+ self.indexer.addFile(self.dicomDatabase, dicomFilePath, True)
+ self.incomingFiles.append(dicomFilePath)
+ if len(self.incomingFiles) < self.maximumIncomingFiles:
+ self.delayedAutoUpdateTimer.start()
+ else:
+ # Limit of pending incoming files is reached, complete indexing of files
+ # that we have received so far.
+ self.delayedAutoUpdateTimer.stop()
+ self.completeIncomingFilesIndexing()
+ self.lastFileAdded = dicomFilePath
+ if self.fileAddedCallback:
+ logging.debug("calling callback...")
+ self.fileAddedCallback()
+ logging.debug("callback done")
+ else:
+ logging.debug("no callback")
class DICOMSender(DICOMProcess):
- """ Code to send files to a remote host.
- (Uses storescu from dcmtk.)
- """
- extended_dicom_config_path = 'DICOM/dcmtk/storescu-seg.cfg'
-
- def __init__(self, files, address, protocol=None, progressCallback=None, aeTitle=None):
- """protocol: can be DIMSE (default) or DICOMweb
- port: optional (if not specified then address URL should contain it)
- """
- super().__init__()
- self.files = files
- self.destinationUrl = qt.QUrl().fromUserInput(address)
- if aeTitle:
- self.aeTitle = aeTitle
- else:
- self.aeTitle = "CTK"
- self.protocol = protocol if protocol is not None else "DIMSE"
- self.progressCallback = progressCallback
- if not self.progressCallback:
- self.progressCallback = self.defaultProgressCallback
- self.send()
-
- def __del__(self):
- super().__del__()
-
- def defaultProgressCallback(self, s):
- logging.debug(s)
-
- def send(self):
- self.progressCallback("Starting send to %s using self.protocol" % self.destinationUrl.toString())
-
- if self.protocol == "DICOMweb":
- # DICOMweb
- # Ensure that correct version of dicomweb-client Python package is installed
- needRestart = False
- needInstall = False
- minimumDicomwebClientVersion = "0.51"
- try:
- import dicomweb_client
- from packaging import version
- if version.parse(dicomweb_client.__version__) < version.parse(minimumDicomwebClientVersion):
- if not slicer.util.confirmOkCancelDisplay(f"DICOMweb sending requires installation of dicomweb-client (version {minimumDicomwebClientVersion} or later).\nClick OK to upgrade dicomweb-client and restart the application."):
- self.showBrowserOnEnter = False
- return
- needRestart = True
- needInstall = True
- except ModuleNotFoundError:
- needInstall = True
-
- if needInstall:
- # pythonweb-client 0.50 was broken (https://github.com/MGHComputationalPathology/dicomweb-client/issues/41)
- progressDialog = slicer.util.createProgressDialog(labelText='Upgrading dicomweb-client. This may take a minute...', maximum=0)
- slicer.app.processEvents()
- slicer.util.pip_install(f'dicomweb-client>={minimumDicomwebClientVersion}')
- import dicomweb_client
- progressDialog.close()
- if needRestart:
- slicer.util.restart()
-
- # Establish connection
- import dicomweb_client.log
- dicomweb_client.log.configure_logging(2)
- from dicomweb_client.api import DICOMwebClient
- effectiveServerUrl = self.destinationUrl.toString()
- session = None
- headers = {}
- # Setting up of the DICOMweb client from various server parameters can be done
- # in plugins in the future, but for now just hardcode special initialization
- # steps for a few server types.
- if "kheops" in effectiveServerUrl:
- # Kheops DICOMweb API endpoint from browser view URL
- url = qt.QUrl(effectiveServerUrl)
- if url.path().startswith('/view/'):
- # This is a Kheops viewer URL.
- # Retrieve the token from the viewer URL and use the Kheops API URL to connect to the server.
- token = url.path().replace('/view/', '')
- effectiveServerUrl = "https://demo.kheops.online/api"
- from requests.auth import HTTPBasicAuth
- from dicomweb_client.session_utils import create_session_from_auth
- auth = HTTPBasicAuth('token', token)
- session = create_session_from_auth(auth)
-
- client = DICOMwebClient(url=effectiveServerUrl, session=session, headers=headers)
-
- for file in self.files:
- if not self.progressCallback(f"Sending {file} to {self.destinationUrl.toString()} using {self.protocol}"):
- raise UserWarning("Sending was cancelled, upload is incomplete.")
- import pydicom
- dataset = pydicom.dcmread(file)
- client.store_instances(datasets=[dataset])
- else:
- # DIMSE (traditional DICOM networking)
- for file in self.files:
- self.start(file)
- if not self.progressCallback(f"Sent {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"):
- raise UserWarning("Sending was cancelled, upload is incomplete.")
-
- def dicomSend(self, file, config=None, config_profile='Default'):
- """Send DICOM file to the specified modality."""
- self.storeSCUExecutable = self.exeDir + '/storescu' + self.exeExtension
-
- # TODO: maybe use dcmsend (is smarter about the compress/decompress)
-
- args = []
-
- # Utilize custom configuration
- if config and os.path.exists(config):
- args.extend(('-xf', config, config_profile))
-
- # Core arguments: hostname, port, AEC, file
- args.extend((self.destinationUrl.host(), str(self.destinationUrl.port()), "-aec", self.aeTitle, file))
-
- # Execute SCU CLI program and wait for termination. Uses super().start() to access the
- # to initialize the background process and wait for completion of the transfer.
- super().start(self.storeSCUExecutable, args)
- self.process.waitForFinished()
- return not (self.process.ExitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0)
-
- def start(self, file):
- """ Send DICOM file to the specified modality. If the transfer fails due to
- an unsupported presentation context, attempt the transfer a second time using
- a custom configuration that provides.
+ """ Code to send files to a remote host.
+ (Uses storescu from dcmtk.)
"""
+ extended_dicom_config_path = 'DICOM/dcmtk/storescu-seg.cfg'
+
+ def __init__(self, files, address, protocol=None, progressCallback=None, aeTitle=None):
+ """protocol: can be DIMSE (default) or DICOMweb
+ port: optional (if not specified then address URL should contain it)
+ """
+ super().__init__()
+ self.files = files
+ self.destinationUrl = qt.QUrl().fromUserInput(address)
+ if aeTitle:
+ self.aeTitle = aeTitle
+ else:
+ self.aeTitle = "CTK"
+ self.protocol = protocol if protocol is not None else "DIMSE"
+ self.progressCallback = progressCallback
+ if not self.progressCallback:
+ self.progressCallback = self.defaultProgressCallback
+ self.send()
+
+ def __del__(self):
+ super().__del__()
+
+ def defaultProgressCallback(self, s):
+ logging.debug(s)
+
+ def send(self):
+ self.progressCallback("Starting send to %s using self.protocol" % self.destinationUrl.toString())
+
+ if self.protocol == "DICOMweb":
+ # DICOMweb
+ # Ensure that correct version of dicomweb-client Python package is installed
+ needRestart = False
+ needInstall = False
+ minimumDicomwebClientVersion = "0.51"
+ try:
+ import dicomweb_client
+ from packaging import version
+ if version.parse(dicomweb_client.__version__) < version.parse(minimumDicomwebClientVersion):
+ if not slicer.util.confirmOkCancelDisplay(f"DICOMweb sending requires installation of dicomweb-client (version {minimumDicomwebClientVersion} or later).\nClick OK to upgrade dicomweb-client and restart the application."):
+ self.showBrowserOnEnter = False
+ return
+ needRestart = True
+ needInstall = True
+ except ModuleNotFoundError:
+ needInstall = True
+
+ if needInstall:
+ # pythonweb-client 0.50 was broken (https://github.com/MGHComputationalPathology/dicomweb-client/issues/41)
+ progressDialog = slicer.util.createProgressDialog(labelText='Upgrading dicomweb-client. This may take a minute...', maximum=0)
+ slicer.app.processEvents()
+ slicer.util.pip_install(f'dicomweb-client>={minimumDicomwebClientVersion}')
+ import dicomweb_client
+ progressDialog.close()
+ if needRestart:
+ slicer.util.restart()
+
+ # Establish connection
+ import dicomweb_client.log
+ dicomweb_client.log.configure_logging(2)
+ from dicomweb_client.api import DICOMwebClient
+ effectiveServerUrl = self.destinationUrl.toString()
+ session = None
+ headers = {}
+ # Setting up of the DICOMweb client from various server parameters can be done
+ # in plugins in the future, but for now just hardcode special initialization
+ # steps for a few server types.
+ if "kheops" in effectiveServerUrl:
+ # Kheops DICOMweb API endpoint from browser view URL
+ url = qt.QUrl(effectiveServerUrl)
+ if url.path().startswith('/view/'):
+ # This is a Kheops viewer URL.
+ # Retrieve the token from the viewer URL and use the Kheops API URL to connect to the server.
+ token = url.path().replace('/view/', '')
+ effectiveServerUrl = "https://demo.kheops.online/api"
+ from requests.auth import HTTPBasicAuth
+ from dicomweb_client.session_utils import create_session_from_auth
+ auth = HTTPBasicAuth('token', token)
+ session = create_session_from_auth(auth)
+
+ client = DICOMwebClient(url=effectiveServerUrl, session=session, headers=headers)
+
+ for file in self.files:
+ if not self.progressCallback(f"Sending {file} to {self.destinationUrl.toString()} using {self.protocol}"):
+ raise UserWarning("Sending was cancelled, upload is incomplete.")
+ import pydicom
+ dataset = pydicom.dcmread(file)
+ client.store_instances(datasets=[dataset])
+ else:
+ # DIMSE (traditional DICOM networking)
+ for file in self.files:
+ self.start(file)
+ if not self.progressCallback(f"Sent {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"):
+ raise UserWarning("Sending was cancelled, upload is incomplete.")
+
+ def dicomSend(self, file, config=None, config_profile='Default'):
+ """Send DICOM file to the specified modality."""
+ self.storeSCUExecutable = self.exeDir + '/storescu' + self.exeExtension
+
+ # TODO: maybe use dcmsend (is smarter about the compress/decompress)
- if self.dicomSend(file):
- # success
- return True
+ args = []
- stdout = self.process.readAllStandardOutput()
- stderr = self.process.readAllStandardError()
- logging.debug('DICOM send using standard configuration failed: process error code is %d' % self.process.error())
- logging.debug('DICOM send process standard out is: %s' % stdout)
- logging.debug('DICOM send process standard error is: %s' % stderr)
+ # Utilize custom configuration
+ if config and os.path.exists(config):
+ args.extend(('-xf', config, config_profile))
- # Retry transfer with alternative configuration with presentation contexts which support SEG/SR.
- # A common cause of failure is an incomplete set of dcmtk/DCMSCU presentation context UIDS.
- # Refer to https://book.orthanc-server.com/faq/dcmtk-tricks.html#id2 for additional detail.
- logging.info('Retry transfer with alternative dicomscu configuration: %s' % self.extended_dicom_config_path)
+ # Core arguments: hostname, port, AEC, file
+ args.extend((self.destinationUrl.host(), str(self.destinationUrl.port()), "-aec", self.aeTitle, file))
- # Terminate transfer and notify user of failure
- if self.dicomSend(file, config=os.path.join(RESOURCE_ROOT, self.extended_dicom_config_path)):
- # success
- return True
+ # Execute SCU CLI program and wait for termination. Uses super().start() to access the
+ # to initialize the background process and wait for completion of the transfer.
+ super().start(self.storeSCUExecutable, args)
+ self.process.waitForFinished()
+ return not (self.process.ExitStatus() == qt.QProcess.CrashExit or self.process.exitCode() != 0)
- stdout = self.process.readAllStandardOutput()
- stderr = self.process.readAllStandardError()
- logging.debug('DICOM send using extended configuration failed: process error code is %d' % self.process.error())
- logging.debug('DICOM send process standard out is: %s' % stdout)
- logging.debug('DICOM send process standard error is: %s' % stderr)
+ def start(self, file):
+ """ Send DICOM file to the specified modality. If the transfer fails due to
+ an unsupported presentation context, attempt the transfer a second time using
+ a custom configuration that provides.
+ """
- userMsg = f"Could not send {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"
- raise UserWarning(userMsg)
+ if self.dicomSend(file):
+ # success
+ return True
+
+ stdout = self.process.readAllStandardOutput()
+ stderr = self.process.readAllStandardError()
+ logging.debug('DICOM send using standard configuration failed: process error code is %d' % self.process.error())
+ logging.debug('DICOM send process standard out is: %s' % stdout)
+ logging.debug('DICOM send process standard error is: %s' % stderr)
+
+ # Retry transfer with alternative configuration with presentation contexts which support SEG/SR.
+ # A common cause of failure is an incomplete set of dcmtk/DCMSCU presentation context UIDS.
+ # Refer to https://book.orthanc-server.com/faq/dcmtk-tricks.html#id2 for additional detail.
+ logging.info('Retry transfer with alternative dicomscu configuration: %s' % self.extended_dicom_config_path)
+
+ # Terminate transfer and notify user of failure
+ if self.dicomSend(file, config=os.path.join(RESOURCE_ROOT, self.extended_dicom_config_path)):
+ # success
+ return True
+
+ stdout = self.process.readAllStandardOutput()
+ stderr = self.process.readAllStandardError()
+ logging.debug('DICOM send using extended configuration failed: process error code is %d' % self.process.error())
+ logging.debug('DICOM send process standard out is: %s' % stdout)
+ logging.debug('DICOM send process standard error is: %s' % stderr)
+
+ userMsg = f"Could not send {file} to {self.destinationUrl.host()}:{self.destinationUrl.port()}"
+ raise UserWarning(userMsg)
class DICOMTestingQRServer:
- """helper class to set up the DICOM servers
- Code here depends only on python and DCMTK executables
- TODO: it might make sense to refactor this as a generic tool
- for interacting with DCMTK
- """
- # TODO: make this use DICOMProcess superclass
-
- def __init__(self, exeDir=".", tmpDir="./DICOM"):
- self.qrProcess = None
- self.tmpDir = tmpDir
- self.exeDir = exeDir
-
- def __del__(self):
- self.stop()
-
- def qrRunning(self):
- return self.qrProcess is not None
-
- def start(self, verbose=False, initialFiles=None):
- if self.qrRunning():
- self.stop()
-
- self.dcmqrscpExecutable = self.exeDir + '/dcmqrdb/apps/dcmqrscp'
- self.storeSCUExecutable = self.exeDir + '/dcmnet/apps/storescu'
-
- # make the config file
- cfg = self.tmpDir + "/dcmqrscp.cfg"
- self.makeConfigFile(cfg, storageDirectory=self.tmpDir)
-
- # start the server!
- cmdLine = [self.dcmqrscpExecutable]
- if verbose:
- cmdLine.append('--verbose')
- cmdLine.append('--config')
- cmdLine.append(cfg)
- self.qrProcess = subprocess.Popen(cmdLine)
- # TODO: handle output
- # stdin=subprocess.PIPE,
- # stdout=subprocess.PIPE,
- # stderr=subprocess.PIPE)
-
- # push the data to the server!
- if initialFiles:
- cmdLine = [self.storeSCUExecutable]
- if verbose:
- cmdLine.append('--verbose')
- cmdLine.append('-aec')
- cmdLine.append('CTK_AE')
- cmdLine.append('-aet')
- cmdLine.append('CTK_AE')
- cmdLine.append('localhost')
- cmdLine.append('11112')
- cmdLine += initialFiles
- p = subprocess.Popen(cmdLine)
- p.wait()
-
- def stop(self):
- self.qrProcess.kill()
- self.qrProcess.communicate()
- self.qrProcess.wait()
- self.qrProcess = None
-
- def makeConfigFile(self, configFile, storageDirectory='.'):
- """ make a config file for the local instance with just
- the parts we need (comments and examples removed).
- For examples and the full syntax
- see dcmqrdb/etc/dcmqrscp.cfg and
- dcmqrdb/docs/dcmqrcnf.txt in the dcmtk source
- available from dcmtk.org or the ctk distribution
+ """helper class to set up the DICOM servers
+ Code here depends only on python and DCMTK executables
+ TODO: it might make sense to refactor this as a generic tool
+ for interacting with DCMTK
"""
-
- template = """
+ # TODO: make this use DICOMProcess superclass
+
+ def __init__(self, exeDir=".", tmpDir="./DICOM"):
+ self.qrProcess = None
+ self.tmpDir = tmpDir
+ self.exeDir = exeDir
+
+ def __del__(self):
+ self.stop()
+
+ def qrRunning(self):
+ return self.qrProcess is not None
+
+ def start(self, verbose=False, initialFiles=None):
+ if self.qrRunning():
+ self.stop()
+
+ self.dcmqrscpExecutable = self.exeDir + '/dcmqrdb/apps/dcmqrscp'
+ self.storeSCUExecutable = self.exeDir + '/dcmnet/apps/storescu'
+
+ # make the config file
+ cfg = self.tmpDir + "/dcmqrscp.cfg"
+ self.makeConfigFile(cfg, storageDirectory=self.tmpDir)
+
+ # start the server!
+ cmdLine = [self.dcmqrscpExecutable]
+ if verbose:
+ cmdLine.append('--verbose')
+ cmdLine.append('--config')
+ cmdLine.append(cfg)
+ self.qrProcess = subprocess.Popen(cmdLine)
+ # TODO: handle output
+ # stdin=subprocess.PIPE,
+ # stdout=subprocess.PIPE,
+ # stderr=subprocess.PIPE)
+
+ # push the data to the server!
+ if initialFiles:
+ cmdLine = [self.storeSCUExecutable]
+ if verbose:
+ cmdLine.append('--verbose')
+ cmdLine.append('-aec')
+ cmdLine.append('CTK_AE')
+ cmdLine.append('-aet')
+ cmdLine.append('CTK_AE')
+ cmdLine.append('localhost')
+ cmdLine.append('11112')
+ cmdLine += initialFiles
+ p = subprocess.Popen(cmdLine)
+ p.wait()
+
+ def stop(self):
+ self.qrProcess.kill()
+ self.qrProcess.communicate()
+ self.qrProcess.wait()
+ self.qrProcess = None
+
+ def makeConfigFile(self, configFile, storageDirectory='.'):
+ """ make a config file for the local instance with just
+ the parts we need (comments and examples removed).
+ For examples and the full syntax
+ see dcmqrdb/etc/dcmqrscp.cfg and
+ dcmqrdb/docs/dcmqrcnf.txt in the dcmtk source
+ available from dcmtk.org or the ctk distribution
+ """
+
+ template = """
# Global Configuration Parameters
NetworkType = "tcp"
NetworkTCPPort = 11112
@@ -639,8 +639,8 @@ def makeConfigFile(self, configFile, storageDirectory='.'):
CTK_AE %s RW (200, 1024mb) ANY
AETable END
"""
- config = template % storageDirectory
+ config = template % storageDirectory
- fp = open(configFile, 'w')
- fp.write(config)
- fp.close()
+ fp = open(configFile, 'w')
+ fp.write(config)
+ fp.close()
diff --git a/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py b/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py
index d59f7c60ca3..b39ab7fd9be 100644
--- a/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py
+++ b/Modules/Scripted/DICOMLib/DICOMRecentActivityWidget.py
@@ -7,130 +7,130 @@
class DICOMRecentActivityWidget(qt.QWidget):
- """Display the recent activity of the slicer DICOM database
- Example:
- slicer.util.selectModule('DICOM')
- import DICOMLib
- w = DICOMLib.DICOMRecentActivityWidget(None, slicer.dicomDatabase, slicer.modules.DICOMInstance.browserWidget)
- w.update()
- w.show()
- """
-
- def __init__(self, parent, dicomDatabase=None, browserWidget=None):
- """If browserWidget is specified (e.g., set to slicer.modules.DICOMInstance.browserWidget)
- then clicking on an item selects the series in that browserWidget.
+ """Display the recent activity of the slicer DICOM database
+ Example:
+ slicer.util.selectModule('DICOM')
+ import DICOMLib
+ w = DICOMLib.DICOMRecentActivityWidget(None, slicer.dicomDatabase, slicer.modules.DICOMInstance.browserWidget)
+ w.update()
+ w.show()
"""
- super().__init__(parent)
- if dicomDatabase:
- self.dicomDatabase = dicomDatabase
- else:
- self.dicomDatabase = slicer.dicomDatabase
- self.browserWidget = browserWidget
- self.recentSeries = []
- self.name = 'recentActivityWidget'
- self.setLayout(qt.QVBoxLayout())
- self.statusLabel = qt.QLabel()
- self.layout().addWidget(self.statusLabel)
- self.statusLabel.text = ''
+ def __init__(self, parent, dicomDatabase=None, browserWidget=None):
+ """If browserWidget is specified (e.g., set to slicer.modules.DICOMInstance.browserWidget)
+ then clicking on an item selects the series in that browserWidget.
+ """
+ super().__init__(parent)
+ if dicomDatabase:
+ self.dicomDatabase = dicomDatabase
+ else:
+ self.dicomDatabase = slicer.dicomDatabase
+ self.browserWidget = browserWidget
+ self.recentSeries = []
+ self.name = 'recentActivityWidget'
+ self.setLayout(qt.QVBoxLayout())
- self.scrollArea = qt.QScrollArea()
- self.layout().addWidget(self.scrollArea)
- self.listWidget = qt.QListWidget()
- self.listWidget.name = 'recentActivityListWidget'
- self.scrollArea.setWidget(self.listWidget)
- self.scrollArea.setWidgetResizable(True)
- self.listWidget.setProperty('SH_ItemView_ActivateItemOnSingleClick', 1)
- self.listWidget.connect('activated(QModelIndex)', self.onActivated)
+ self.statusLabel = qt.QLabel()
+ self.layout().addWidget(self.statusLabel)
+ self.statusLabel.text = ''
- self.refreshButton = qt.QPushButton()
- self.layout().addWidget(self.refreshButton)
- self.refreshButton.text = 'Refresh'
- self.refreshButton.connect('clicked()', self.update)
+ self.scrollArea = qt.QScrollArea()
+ self.layout().addWidget(self.scrollArea)
+ self.listWidget = qt.QListWidget()
+ self.listWidget.name = 'recentActivityListWidget'
+ self.scrollArea.setWidget(self.listWidget)
+ self.scrollArea.setWidgetResizable(True)
+ self.listWidget.setProperty('SH_ItemView_ActivateItemOnSingleClick', 1)
+ self.listWidget.connect('activated(QModelIndex)', self.onActivated)
- self.tags = {}
- self.tags['seriesDescription'] = "0008,103e"
- self.tags['patientName'] = "0010,0010"
+ self.refreshButton = qt.QPushButton()
+ self.layout().addWidget(self.refreshButton)
+ self.refreshButton.text = 'Refresh'
+ self.refreshButton.connect('clicked()', self.update)
- class seriesWithTime:
- """helper class to track series and time..."""
+ self.tags = {}
+ self.tags['seriesDescription'] = "0008,103e"
+ self.tags['patientName'] = "0010,0010"
- def __init__(self, series, elapsedSinceInsert, insertDateTime, text):
- self.series = series
- self.elapsedSinceInsert = elapsedSinceInsert
- self.insertDateTime = insertDateTime
- self.text = text
+ class seriesWithTime:
+ """helper class to track series and time..."""
- @staticmethod
- def compareSeriesTimes(a, b):
- if a.elapsedSinceInsert > b.elapsedSinceInsert:
- return 1
- else:
- return -1
+ def __init__(self, series, elapsedSinceInsert, insertDateTime, text):
+ self.series = series
+ self.elapsedSinceInsert = elapsedSinceInsert
+ self.insertDateTime = insertDateTime
+ self.text = text
- def recentSeriesList(self):
- """Return a list of series sorted by insert time
- (counting backwards from today)
- Assume that first insert time of series is valid
- for entire series (should be close enough for this purpose)
- """
- recentSeries = []
- now = qt.QDateTime.currentDateTime()
- for patient in self.dicomDatabase.patients():
- for study in self.dicomDatabase.studiesForPatient(patient):
- for series in self.dicomDatabase.seriesForStudy(study):
- files = self.dicomDatabase.filesForSeries(series, 1)
- if len(files) > 0:
- instance = self.dicomDatabase.instanceForFile(files[0])
- seriesTime = self.dicomDatabase.insertDateTimeForInstance(instance)
- try:
- patientName = self.dicomDatabase.instanceValue(instance, self.tags['patientName'])
- except RuntimeError:
- # this indicates that the particular instance is no longer
- # accessible to the dicom database, so we should ignore it here
- continue
- seriesDescription = self.dicomDatabase.instanceValue(instance, self.tags['seriesDescription'])
- elapsed = seriesTime.secsTo(now)
- secondsPerHour = 60 * 60
- secondsPerDay = secondsPerHour * 24
- timeNote = None
- if elapsed < secondsPerDay:
- timeNote = 'Today'
- elif elapsed < 7 * secondsPerDay:
- timeNote = 'Past Week'
- elif elapsed < 30 * 7 * secondsPerDay:
- timeNote = 'Past Month'
- if timeNote:
- text = f"{timeNote}: {seriesDescription} for {patientName}"
- recentSeries.append(self.seriesWithTime(series, elapsed, seriesTime, text))
- recentSeries.sort(key=cmp_to_key(self.compareSeriesTimes))
- return recentSeries
+ @staticmethod
+ def compareSeriesTimes(a, b):
+ if a.elapsedSinceInsert > b.elapsedSinceInsert:
+ return 1
+ else:
+ return -1
- def update(self):
- """Load the table widget with header values for the file
- """
- self.listWidget.clear()
- secondsPerHour = 60 * 60
- insertsPastHour = 0
- self.recentSeries = self.recentSeriesList()
- for series in self.recentSeries:
- self.listWidget.addItem(series.text)
- if series.elapsedSinceInsert < secondsPerHour:
- insertsPastHour += 1
- self.statusLabel.text = '%d series added to database in the past hour' % insertsPastHour
- if len(self.recentSeries) > 0:
- statusMessage = "Most recent DICOM Database addition: %s" % self.recentSeries[0].insertDateTime.toString()
- slicer.util.showStatusMessage(statusMessage, 10000)
+ def recentSeriesList(self):
+ """Return a list of series sorted by insert time
+ (counting backwards from today)
+ Assume that first insert time of series is valid
+ for entire series (should be close enough for this purpose)
+ """
+ recentSeries = []
+ now = qt.QDateTime.currentDateTime()
+ for patient in self.dicomDatabase.patients():
+ for study in self.dicomDatabase.studiesForPatient(patient):
+ for series in self.dicomDatabase.seriesForStudy(study):
+ files = self.dicomDatabase.filesForSeries(series, 1)
+ if len(files) > 0:
+ instance = self.dicomDatabase.instanceForFile(files[0])
+ seriesTime = self.dicomDatabase.insertDateTimeForInstance(instance)
+ try:
+ patientName = self.dicomDatabase.instanceValue(instance, self.tags['patientName'])
+ except RuntimeError:
+ # this indicates that the particular instance is no longer
+ # accessible to the dicom database, so we should ignore it here
+ continue
+ seriesDescription = self.dicomDatabase.instanceValue(instance, self.tags['seriesDescription'])
+ elapsed = seriesTime.secsTo(now)
+ secondsPerHour = 60 * 60
+ secondsPerDay = secondsPerHour * 24
+ timeNote = None
+ if elapsed < secondsPerDay:
+ timeNote = 'Today'
+ elif elapsed < 7 * secondsPerDay:
+ timeNote = 'Past Week'
+ elif elapsed < 30 * 7 * secondsPerDay:
+ timeNote = 'Past Month'
+ if timeNote:
+ text = f"{timeNote}: {seriesDescription} for {patientName}"
+ recentSeries.append(self.seriesWithTime(series, elapsed, seriesTime, text))
+ recentSeries.sort(key=cmp_to_key(self.compareSeriesTimes))
+ return recentSeries
+
+ def update(self):
+ """Load the table widget with header values for the file
+ """
+ self.listWidget.clear()
+ secondsPerHour = 60 * 60
+ insertsPastHour = 0
+ self.recentSeries = self.recentSeriesList()
+ for series in self.recentSeries:
+ self.listWidget.addItem(series.text)
+ if series.elapsedSinceInsert < secondsPerHour:
+ insertsPastHour += 1
+ self.statusLabel.text = '%d series added to database in the past hour' % insertsPastHour
+ if len(self.recentSeries) > 0:
+ statusMessage = "Most recent DICOM Database addition: %s" % self.recentSeries[0].insertDateTime.toString()
+ slicer.util.showStatusMessage(statusMessage, 10000)
- def onActivated(self, modelIndex):
- logging.debug('Recent activity widget selected row: %d (%s)' % (modelIndex.row(), self.recentSeries[modelIndex.row()].text))
- if not self.browserWidget:
- return
- # Select series in the series table
- series = self.recentSeries[modelIndex.row()]
- seriesUID = series.series
- seriesTableView = self.browserWidget.dicomBrowser.dicomTableManager().seriesTable().tableView()
- foundModelIndex = seriesTableView.model().match(seriesTableView.model().index(0, 0), qt.Qt.ItemDataRole(), seriesUID, 1)
- if foundModelIndex:
- row = foundModelIndex[0].row()
- seriesTableView.selectRow(row)
+ def onActivated(self, modelIndex):
+ logging.debug('Recent activity widget selected row: %d (%s)' % (modelIndex.row(), self.recentSeries[modelIndex.row()].text))
+ if not self.browserWidget:
+ return
+ # Select series in the series table
+ series = self.recentSeries[modelIndex.row()]
+ seriesUID = series.series
+ seriesTableView = self.browserWidget.dicomBrowser.dicomTableManager().seriesTable().tableView()
+ foundModelIndex = seriesTableView.model().match(seriesTableView.model().index(0, 0), qt.Qt.ItemDataRole(), seriesUID, 1)
+ if foundModelIndex:
+ row = foundModelIndex[0].row()
+ seriesTableView.selectRow(row)
diff --git a/Modules/Scripted/DICOMLib/DICOMSendDialog.py b/Modules/Scripted/DICOMLib/DICOMSendDialog.py
index 06a1e251a48..564e4dd454e 100644
--- a/Modules/Scripted/DICOMLib/DICOMSendDialog.py
+++ b/Modules/Scripted/DICOMLib/DICOMSendDialog.py
@@ -7,101 +7,101 @@
class DICOMSendDialog(qt.QDialog):
- """Implement the Qt dialog for doing a DICOM Send (storage SCU)
- """
-
- def __init__(self, files, parent="mainWindow"):
- super().__init__(slicer.util.mainWindow() if parent == "mainWindow" else parent)
- self.setWindowTitle('Send DICOM Study')
- self.setWindowModality(1)
- self.setLayout(qt.QVBoxLayout())
- self.files = files
- self.cancelRequested = False
- self.sendingIsInProgress = False
- self.setMinimumWidth(200)
- self.open()
-
- def open(self):
- self.studyLabel = qt.QLabel('Send %d items to destination' % len(self.files))
- self.layout().addWidget(self.studyLabel)
-
- # Send Parameters
- self.dicomFrame = qt.QFrame(self)
- self.dicomFormLayout = qt.QFormLayout()
- self.dicomFrame.setLayout(self.dicomFormLayout)
-
- self.settings = qt.QSettings()
-
- self.protocolSelectorCombobox = qt.QComboBox()
- self.protocolSelectorCombobox.addItems(["DIMSE", "DICOMweb"])
- self.protocolSelectorCombobox.setCurrentText(self.settings.value('DICOM/Send/Protocol', 'DIMSE'))
- self.protocolSelectorCombobox.currentIndexChanged.connect(self.onProtocolSelectorChange)
- self.dicomFormLayout.addRow("Protocol: ", self.protocolSelectorCombobox)
-
- self.serverAETitleEdit = qt.QLineEdit()
- self.serverAETitleEdit.setToolTip("AE Title")
- self.serverAETitleEdit.text = self.settings.value('DICOM/Send/AETitle', 'CTK')
- self.dicomFormLayout.addRow("AE Title: ", self.serverAETitleEdit)
- # Enable AET only for DIMSE
- self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE'
-
- self.serverAddressLineEdit = qt.QLineEdit()
- self.serverAddressLineEdit.setToolTip("Address includes hostname and port number in standard URL format (hostname:port).")
- self.serverAddressLineEdit.text = self.settings.value('DICOM/Send/URL', '')
- self.dicomFormLayout.addRow("Destination Address: ", self.serverAddressLineEdit)
-
- self.layout().addWidget(self.dicomFrame)
-
- # button box
- self.bbox = qt.QDialogButtonBox(self)
- self.bbox.addButton(self.bbox.Ok)
- self.bbox.addButton(self.bbox.Cancel)
- self.bbox.accepted.connect(self.onOk)
- self.bbox.rejected.connect(self.onCancel)
- self.layout().addWidget(self.bbox)
-
- self.progressBar = qt.QProgressBar(self.parent().window())
- self.progressBar.hide()
- self.dicomFormLayout.addRow(self.progressBar)
-
- qt.QDialog.open(self)
-
- def onProtocolSelectorChange(self):
- # Enable AET only for DIMSE
- self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE'
-
- def onOk(self):
- self.sendingIsInProgress = True
- address = self.serverAddressLineEdit.text
- aeTitle = self.serverAETitleEdit.text
- protocol = self.protocolSelectorCombobox.currentText
- self.settings.setValue('DICOM/Send/URL', address)
- self.settings.setValue('DICOM/Send/AETitle', aeTitle)
- self.settings.setValue('DICOM/Send/Protocol', protocol)
- self.progressBar.value = 0
- self.progressBar.maximum = len(self.files) + 1
- self.progressBar.show()
- self.cancelRequested = False
- okButton = self.bbox.button(self.bbox.Ok)
-
- with slicer.util.tryWithErrorDisplay("DICOM sending failed."):
- okButton.enabled = False
- DICOMLib.DICOMSender(self.files, address, protocol, aeTitle=aeTitle, progressCallback=self.onProgress)
- logging.debug("DICOM sending of %s files succeeded" % len(self.files))
- self.close()
-
- okButton.enabled = True
- self.sendingIsInProgress = False
-
- def onCancel(self):
- if self.sendingIsInProgress:
- self.cancelRequested = True
- else:
- self.close()
-
- def onProgress(self, message):
- self.progressBar.value += 1
- # message can be long, do not display it, but still log it (might be useful for troubleshooting)
- logging.debug("DICOM send: " + message)
- slicer.app.processEvents()
- return not self.cancelRequested
+ """Implement the Qt dialog for doing a DICOM Send (storage SCU)
+ """
+
+ def __init__(self, files, parent="mainWindow"):
+ super().__init__(slicer.util.mainWindow() if parent == "mainWindow" else parent)
+ self.setWindowTitle('Send DICOM Study')
+ self.setWindowModality(1)
+ self.setLayout(qt.QVBoxLayout())
+ self.files = files
+ self.cancelRequested = False
+ self.sendingIsInProgress = False
+ self.setMinimumWidth(200)
+ self.open()
+
+ def open(self):
+ self.studyLabel = qt.QLabel('Send %d items to destination' % len(self.files))
+ self.layout().addWidget(self.studyLabel)
+
+ # Send Parameters
+ self.dicomFrame = qt.QFrame(self)
+ self.dicomFormLayout = qt.QFormLayout()
+ self.dicomFrame.setLayout(self.dicomFormLayout)
+
+ self.settings = qt.QSettings()
+
+ self.protocolSelectorCombobox = qt.QComboBox()
+ self.protocolSelectorCombobox.addItems(["DIMSE", "DICOMweb"])
+ self.protocolSelectorCombobox.setCurrentText(self.settings.value('DICOM/Send/Protocol', 'DIMSE'))
+ self.protocolSelectorCombobox.currentIndexChanged.connect(self.onProtocolSelectorChange)
+ self.dicomFormLayout.addRow("Protocol: ", self.protocolSelectorCombobox)
+
+ self.serverAETitleEdit = qt.QLineEdit()
+ self.serverAETitleEdit.setToolTip("AE Title")
+ self.serverAETitleEdit.text = self.settings.value('DICOM/Send/AETitle', 'CTK')
+ self.dicomFormLayout.addRow("AE Title: ", self.serverAETitleEdit)
+ # Enable AET only for DIMSE
+ self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE'
+
+ self.serverAddressLineEdit = qt.QLineEdit()
+ self.serverAddressLineEdit.setToolTip("Address includes hostname and port number in standard URL format (hostname:port).")
+ self.serverAddressLineEdit.text = self.settings.value('DICOM/Send/URL', '')
+ self.dicomFormLayout.addRow("Destination Address: ", self.serverAddressLineEdit)
+
+ self.layout().addWidget(self.dicomFrame)
+
+ # button box
+ self.bbox = qt.QDialogButtonBox(self)
+ self.bbox.addButton(self.bbox.Ok)
+ self.bbox.addButton(self.bbox.Cancel)
+ self.bbox.accepted.connect(self.onOk)
+ self.bbox.rejected.connect(self.onCancel)
+ self.layout().addWidget(self.bbox)
+
+ self.progressBar = qt.QProgressBar(self.parent().window())
+ self.progressBar.hide()
+ self.dicomFormLayout.addRow(self.progressBar)
+
+ qt.QDialog.open(self)
+
+ def onProtocolSelectorChange(self):
+ # Enable AET only for DIMSE
+ self.serverAETitleEdit.enabled = self.protocolSelectorCombobox.currentText == 'DIMSE'
+
+ def onOk(self):
+ self.sendingIsInProgress = True
+ address = self.serverAddressLineEdit.text
+ aeTitle = self.serverAETitleEdit.text
+ protocol = self.protocolSelectorCombobox.currentText
+ self.settings.setValue('DICOM/Send/URL', address)
+ self.settings.setValue('DICOM/Send/AETitle', aeTitle)
+ self.settings.setValue('DICOM/Send/Protocol', protocol)
+ self.progressBar.value = 0
+ self.progressBar.maximum = len(self.files) + 1
+ self.progressBar.show()
+ self.cancelRequested = False
+ okButton = self.bbox.button(self.bbox.Ok)
+
+ with slicer.util.tryWithErrorDisplay("DICOM sending failed."):
+ okButton.enabled = False
+ DICOMLib.DICOMSender(self.files, address, protocol, aeTitle=aeTitle, progressCallback=self.onProgress)
+ logging.debug("DICOM sending of %s files succeeded" % len(self.files))
+ self.close()
+
+ okButton.enabled = True
+ self.sendingIsInProgress = False
+
+ def onCancel(self):
+ if self.sendingIsInProgress:
+ self.cancelRequested = True
+ else:
+ self.close()
+
+ def onProgress(self, message):
+ self.progressBar.value += 1
+ # message can be long, do not display it, but still log it (might be useful for troubleshooting)
+ logging.debug("DICOM send: " + message)
+ slicer.app.processEvents()
+ return not self.cancelRequested
diff --git a/Modules/Scripted/DICOMLib/DICOMUtils.py b/Modules/Scripted/DICOMLib/DICOMUtils.py
index ae29b9c62f2..a5917bae10e 100644
--- a/Modules/Scripted/DICOMLib/DICOMUtils.py
+++ b/Modules/Scripted/DICOMLib/DICOMUtils.py
@@ -23,571 +23,571 @@
# ------------------------------------------------------------------------------
def loadPatientByUID(patientUID):
- """ Load patient by patient UID from DICOM database.
- Returns list of loaded node ids.
-
- Example: load all data from a DICOM folder (using a temporary DICOM database)
-
- dicomDataDir = "c:/my/folder/with/dicom-files" # input folder with DICOM files
- loadedNodeIDs = [] # this list will contain the list of all loaded node IDs
-
- from DICOMLib import DICOMUtils
- with DICOMUtils.TemporaryDICOMDatabase() as db:
- DICOMUtils.importDicom(dicomDataDir, db)
- patientUIDs = db.patients()
- for patientUID in patientUIDs:
- loadedNodeIDs.extend(DICOMUtils.loadPatientByUID(patientUID))
-
- This method expecs a patientUID in the form returned by
- db.patients(), which are (integer) strings unique for the current database.
- The actual contents of these strings are implementation specific
- and should not be relied on (may be changed).
- See ctkDICOMDatabasePrivate::insertPatient for more details.
-
- See loadPatientByPatientID to use the PatientID field
- of the dicom header.
-
- Note that we have these methods because
- there is no way to have a globally unique patient ID.
- They are issued by different institutions so there may be
- name clashes. This is is in contrast to other places where UID is
- used in dicom and in this code (studyUID, seriesUID, instanceUID),
- where it is common to assume that
- the IDs are unique because the dicom standard provides
- mechanisms to support generating unique values
- (although there is no way to know for sure that
- the creator of an instance actually created a unique value
- rather than just copying an existing one).
- """
- if not slicer.dicomDatabase.isOpen:
- raise OSError('DICOM module or database cannot be accessed')
-
- patientUIDstr = str(patientUID)
- if not patientUIDstr in slicer.dicomDatabase.patients():
- raise OSError('No patient found with DICOM database UID %s' % patientUIDstr)
-
- # Select all series in selected patient
- studies = slicer.dicomDatabase.studiesForPatient(patientUIDstr)
- if len(studies) == 0:
- raise OSError('No studies found in patient with DICOM database UID ' + patientUIDstr)
-
- series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies]
- seriesUIDs = [uid for uidList in series for uid in uidList]
- if len(seriesUIDs) == 0:
- raise OSError('No series found in patient with DICOM database UID ' + patientUIDstr)
-
- return loadSeriesByUID(seriesUIDs)
+ """ Load patient by patient UID from DICOM database.
+ Returns list of loaded node ids.
+
+ Example: load all data from a DICOM folder (using a temporary DICOM database)
+
+ dicomDataDir = "c:/my/folder/with/dicom-files" # input folder with DICOM files
+ loadedNodeIDs = [] # this list will contain the list of all loaded node IDs
+
+ from DICOMLib import DICOMUtils
+ with DICOMUtils.TemporaryDICOMDatabase() as db:
+ DICOMUtils.importDicom(dicomDataDir, db)
+ patientUIDs = db.patients()
+ for patientUID in patientUIDs:
+ loadedNodeIDs.extend(DICOMUtils.loadPatientByUID(patientUID))
+
+ This method expecs a patientUID in the form returned by
+ db.patients(), which are (integer) strings unique for the current database.
+ The actual contents of these strings are implementation specific
+ and should not be relied on (may be changed).
+ See ctkDICOMDatabasePrivate::insertPatient for more details.
+
+ See loadPatientByPatientID to use the PatientID field
+ of the dicom header.
+
+ Note that we have these methods because
+ there is no way to have a globally unique patient ID.
+ They are issued by different institutions so there may be
+ name clashes. This is is in contrast to other places where UID is
+ used in dicom and in this code (studyUID, seriesUID, instanceUID),
+ where it is common to assume that
+ the IDs are unique because the dicom standard provides
+ mechanisms to support generating unique values
+ (although there is no way to know for sure that
+ the creator of an instance actually created a unique value
+ rather than just copying an existing one).
+ """
+ if not slicer.dicomDatabase.isOpen:
+ raise OSError('DICOM module or database cannot be accessed')
+
+ patientUIDstr = str(patientUID)
+ if not patientUIDstr in slicer.dicomDatabase.patients():
+ raise OSError('No patient found with DICOM database UID %s' % patientUIDstr)
+
+ # Select all series in selected patient
+ studies = slicer.dicomDatabase.studiesForPatient(patientUIDstr)
+ if len(studies) == 0:
+ raise OSError('No studies found in patient with DICOM database UID ' + patientUIDstr)
+
+ series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies]
+ seriesUIDs = [uid for uidList in series for uid in uidList]
+ if len(seriesUIDs) == 0:
+ raise OSError('No series found in patient with DICOM database UID ' + patientUIDstr)
+
+ return loadSeriesByUID(seriesUIDs)
# ------------------------------------------------------------------------------
def getDatabasePatientUIDByPatientName(name):
- """ Get patient UID by patient name for easy loading of a patient
- """
- if not slicer.dicomDatabase.isOpen:
- raise OSError('DICOM module or database cannot be accessed')
-
- patients = slicer.dicomDatabase.patients()
- for patientUID in patients:
- currentName = slicer.dicomDatabase.nameForPatient(patientUID)
- if currentName == name:
- return patientUID
- return None
+ """ Get patient UID by patient name for easy loading of a patient
+ """
+ if not slicer.dicomDatabase.isOpen:
+ raise OSError('DICOM module or database cannot be accessed')
+
+ patients = slicer.dicomDatabase.patients()
+ for patientUID in patients:
+ currentName = slicer.dicomDatabase.nameForPatient(patientUID)
+ if currentName == name:
+ return patientUID
+ return None
# ------------------------------------------------------------------------------
def loadPatientByName(patientName):
- """ Load patient by patient name from DICOM database.
- Returns list of loaded node ids.
- """
- patientUID = getDatabasePatientUIDByPatientName(patientName)
- if patientUID is None:
- raise OSError('Patient not found by name %s' % patientName)
- return loadPatientByUID(patientUID)
+ """ Load patient by patient name from DICOM database.
+ Returns list of loaded node ids.
+ """
+ patientUID = getDatabasePatientUIDByPatientName(patientName)
+ if patientUID is None:
+ raise OSError('Patient not found by name %s' % patientName)
+ return loadPatientByUID(patientUID)
# ------------------------------------------------------------------------------
def getDatabasePatientUIDByPatientID(patientID):
- """ Get database patient UID by DICOM patient ID for easy loading of a patient
- """
- if not slicer.dicomDatabase.isOpen:
- raise OSError('DICOM module or database cannot be accessed')
-
- patients = slicer.dicomDatabase.patients()
- for patientUID in patients:
- # Get first file of first series
- studies = slicer.dicomDatabase.studiesForPatient(patientUID)
- series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies]
- seriesUIDs = [uid for uidList in series for uid in uidList]
- if len(seriesUIDs) == 0:
- continue
- filePaths = slicer.dicomDatabase.filesForSeries(seriesUIDs[0], 1)
- if len(filePaths) == 0:
- continue
- firstFile = filePaths[0]
- # Get PatientID from first file
- currentPatientID = slicer.dicomDatabase.fileValue(slicer.util.longPath(firstFile), "0010,0020")
- if currentPatientID == patientID:
- return patientUID
- return None
+ """ Get database patient UID by DICOM patient ID for easy loading of a patient
+ """
+ if not slicer.dicomDatabase.isOpen:
+ raise OSError('DICOM module or database cannot be accessed')
+
+ patients = slicer.dicomDatabase.patients()
+ for patientUID in patients:
+ # Get first file of first series
+ studies = slicer.dicomDatabase.studiesForPatient(patientUID)
+ series = [slicer.dicomDatabase.seriesForStudy(study) for study in studies]
+ seriesUIDs = [uid for uidList in series for uid in uidList]
+ if len(seriesUIDs) == 0:
+ continue
+ filePaths = slicer.dicomDatabase.filesForSeries(seriesUIDs[0], 1)
+ if len(filePaths) == 0:
+ continue
+ firstFile = filePaths[0]
+ # Get PatientID from first file
+ currentPatientID = slicer.dicomDatabase.fileValue(slicer.util.longPath(firstFile), "0010,0020")
+ if currentPatientID == patientID:
+ return patientUID
+ return None
# ------------------------------------------------------------------------------
def loadPatientByPatientID(patientID):
- """ Load patient from DICOM database by DICOM PatientID.
- Returns list of loaded node ids.
- """
- patientUID = getDatabasePatientUIDByPatientID(patientID)
- if patientUID is None:
- raise OSError('Patient not found by PatientID %s' % patientID)
- return loadPatientByUID(patientUID)
+ """ Load patient from DICOM database by DICOM PatientID.
+ Returns list of loaded node ids.
+ """
+ patientUID = getDatabasePatientUIDByPatientID(patientID)
+ if patientUID is None:
+ raise OSError('Patient not found by PatientID %s' % patientID)
+ return loadPatientByUID(patientUID)
# ------------------------------------------------------------------------------
def loadPatient(uid=None, name=None, patientID=None):
- """ Load patient from DICOM database fr uid, name, or patient ID.
- Returns list of loaded node ids.
- """
- if uid is not None:
- return loadPatientByUID(uid)
- elif name is not None:
- return loadPatientByName(name)
- elif patientID is not None:
- return loadPatientByPatientID(patientID)
+ """ Load patient from DICOM database fr uid, name, or patient ID.
+ Returns list of loaded node ids.
+ """
+ if uid is not None:
+ return loadPatientByUID(uid)
+ elif name is not None:
+ return loadPatientByName(name)
+ elif patientID is not None:
+ return loadPatientByPatientID(patientID)
- raise ValueError('One of the following arguments needs to be specified: uid, name, patientID')
+ raise ValueError('One of the following arguments needs to be specified: uid, name, patientID')
# ------------------------------------------------------------------------------
def loadSeriesByUID(seriesUIDs):
- """ Load multiple series by UID from DICOM database.
- Returns list of loaded node ids.
- """
- if not isinstance(seriesUIDs, list):
- raise ValueError('SeriesUIDs must contain a list')
- if seriesUIDs is None or len(seriesUIDs) == 0:
- raise ValueError('No series UIDs given')
+ """ Load multiple series by UID from DICOM database.
+ Returns list of loaded node ids.
+ """
+ if not isinstance(seriesUIDs, list):
+ raise ValueError('SeriesUIDs must contain a list')
+ if seriesUIDs is None or len(seriesUIDs) == 0:
+ raise ValueError('No series UIDs given')
- if not slicer.dicomDatabase.isOpen:
- raise OSError('DICOM module or database cannot be accessed')
+ if not slicer.dicomDatabase.isOpen:
+ raise OSError('DICOM module or database cannot be accessed')
- fileLists = []
- for seriesUID in seriesUIDs:
- fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID))
- if len(fileLists) == 0:
- # No files found for DICOM series list
- return []
+ fileLists = []
+ for seriesUID in seriesUIDs:
+ fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID))
+ if len(fileLists) == 0:
+ # No files found for DICOM series list
+ return []
- loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists)
- selectHighestConfidenceLoadables(loadablesByPlugin)
- return loadLoadables(loadablesByPlugin)
+ loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists)
+ selectHighestConfidenceLoadables(loadablesByPlugin)
+ return loadLoadables(loadablesByPlugin)
def selectHighestConfidenceLoadables(loadablesByPlugin):
- """Review the selected state and confidence of the loadables
- across plugins so that the options the user is most likely
- to want are listed at the top of the table and are selected
- by default. Only offer one pre-selected loadable per series
- unless both plugins mark it as selected and they have equal
- confidence."""
-
- # first, get all loadables corresponding to a series
- seriesUIDTag = "0020,000E"
- loadablesBySeries = {}
- for plugin in loadablesByPlugin:
- for loadable in loadablesByPlugin[plugin]:
- seriesUID = slicer.dicomDatabase.fileValue(loadable.files[0], seriesUIDTag)
- if seriesUID not in loadablesBySeries:
- loadablesBySeries[seriesUID] = [loadable]
- else:
- loadablesBySeries[seriesUID].append(loadable)
-
- # now for each series, find the highest confidence selected loadables
- # and set all others to be unselected.
- # If there are several loadables that tie for the
- # highest confidence value, select them all
- # on the assumption that they represent alternate interpretations
- # of the data or subparts of it. The user can either use
- # advanced mode to deselect, or simply delete the
- # unwanted interpretations.
- for series in loadablesBySeries:
- highestConfidenceValue = -1
- for loadable in loadablesBySeries[series]:
- if loadable.confidence > highestConfidenceValue:
- highestConfidenceValue = loadable.confidence
- for loadable in loadablesBySeries[series]:
- loadable.selected = loadable.confidence == highestConfidenceValue
+ """Review the selected state and confidence of the loadables
+ across plugins so that the options the user is most likely
+ to want are listed at the top of the table and are selected
+ by default. Only offer one pre-selected loadable per series
+ unless both plugins mark it as selected and they have equal
+ confidence."""
+
+ # first, get all loadables corresponding to a series
+ seriesUIDTag = "0020,000E"
+ loadablesBySeries = {}
+ for plugin in loadablesByPlugin:
+ for loadable in loadablesByPlugin[plugin]:
+ seriesUID = slicer.dicomDatabase.fileValue(loadable.files[0], seriesUIDTag)
+ if seriesUID not in loadablesBySeries:
+ loadablesBySeries[seriesUID] = [loadable]
+ else:
+ loadablesBySeries[seriesUID].append(loadable)
+
+ # now for each series, find the highest confidence selected loadables
+ # and set all others to be unselected.
+ # If there are several loadables that tie for the
+ # highest confidence value, select them all
+ # on the assumption that they represent alternate interpretations
+ # of the data or subparts of it. The user can either use
+ # advanced mode to deselect, or simply delete the
+ # unwanted interpretations.
+ for series in loadablesBySeries:
+ highestConfidenceValue = -1
+ for loadable in loadablesBySeries[series]:
+ if loadable.confidence > highestConfidenceValue:
+ highestConfidenceValue = loadable.confidence
+ for loadable in loadablesBySeries[series]:
+ loadable.selected = loadable.confidence == highestConfidenceValue
# ------------------------------------------------------------------------------
def loadByInstanceUID(instanceUID):
- """ Load with the most confident loadable that contains the instanceUID from DICOM database.
- This helps in the case where an instance is part of a series which may offer multiple
- loadables, such as when a series has multiple time points where
- each corresponds to a scalar volume and you only want to load the correct one.
- Returns list of loaded node ids (typically one node).
-
- For example:
- >>> uid = '1.3.6.1.4.1.14519.5.2.1.3098.5025.172915611048593327557054469973'
- >>> import DICOMLib
- >>> nodeIDs = DICOMLib.DICOMUtils.loadByInstanceUID(uid)
- """
-
- if not slicer.dicomDatabase.isOpen:
- raise OSError('DICOM module or database cannot be accessed')
-
- # get the loadables corresponding to this instance's series
- filePath = slicer.dicomDatabase.fileForInstance(instanceUID)
- seriesUID = slicer.dicomDatabase.seriesForFile(filePath)
- fileList = slicer.dicomDatabase.filesForSeries(seriesUID)
- loadablesByPlugin, loadEnabled = getLoadablesFromFileLists([fileList])
- # keep only the loadables that include this instance's file and is highest confidence
- highestConfidence = {
- 'confidence': 0,
- 'plugin': None,
- 'loadable': None
- }
- for plugin in loadablesByPlugin.keys():
- loadablesWithInstance = []
- for loadable in loadablesByPlugin[plugin]:
- if filePath in loadable.files:
- if loadable.confidence > highestConfidence['confidence']:
- loadable.selected = True
- highestConfidence = {
- 'confidence': loadable.confidence,
- 'plugin': plugin,
- 'loadable': loadable
- }
- filteredLoadablesByPlugin = {}
- filteredLoadablesByPlugin[highestConfidence['plugin']] = [highestConfidence['loadable'], ]
- # load the results
- return loadLoadables(filteredLoadablesByPlugin)
+ """ Load with the most confident loadable that contains the instanceUID from DICOM database.
+ This helps in the case where an instance is part of a series which may offer multiple
+ loadables, such as when a series has multiple time points where
+ each corresponds to a scalar volume and you only want to load the correct one.
+ Returns list of loaded node ids (typically one node).
+
+ For example:
+ >>> uid = '1.3.6.1.4.1.14519.5.2.1.3098.5025.172915611048593327557054469973'
+ >>> import DICOMLib
+ >>> nodeIDs = DICOMLib.DICOMUtils.loadByInstanceUID(uid)
+ """
+
+ if not slicer.dicomDatabase.isOpen:
+ raise OSError('DICOM module or database cannot be accessed')
+
+ # get the loadables corresponding to this instance's series
+ filePath = slicer.dicomDatabase.fileForInstance(instanceUID)
+ seriesUID = slicer.dicomDatabase.seriesForFile(filePath)
+ fileList = slicer.dicomDatabase.filesForSeries(seriesUID)
+ loadablesByPlugin, loadEnabled = getLoadablesFromFileLists([fileList])
+ # keep only the loadables that include this instance's file and is highest confidence
+ highestConfidence = {
+ 'confidence': 0,
+ 'plugin': None,
+ 'loadable': None
+ }
+ for plugin in loadablesByPlugin.keys():
+ loadablesWithInstance = []
+ for loadable in loadablesByPlugin[plugin]:
+ if filePath in loadable.files:
+ if loadable.confidence > highestConfidence['confidence']:
+ loadable.selected = True
+ highestConfidence = {
+ 'confidence': loadable.confidence,
+ 'plugin': plugin,
+ 'loadable': loadable
+ }
+ filteredLoadablesByPlugin = {}
+ filteredLoadablesByPlugin[highestConfidence['plugin']] = [highestConfidence['loadable'], ]
+ # load the results
+ return loadLoadables(filteredLoadablesByPlugin)
# ------------------------------------------------------------------------------
def openDatabase(databaseDir):
- """Open DICOM database in the specified folder"""
- if not os.access(databaseDir, os.F_OK):
- logging.error('Specified database directory ' + repr(databaseDir) + ' cannot be found')
- return False
- databaseFileName = databaseDir + "/ctkDICOM.sql"
- slicer.dicomDatabase.openDatabase(databaseFileName)
- if not slicer.dicomDatabase.isOpen:
- logging.error('Unable to open DICOM database ' + databaseDir)
- return False
- return True
+ """Open DICOM database in the specified folder"""
+ if not os.access(databaseDir, os.F_OK):
+ logging.error('Specified database directory ' + repr(databaseDir) + ' cannot be found')
+ return False
+ databaseFileName = databaseDir + "/ctkDICOM.sql"
+ slicer.dicomDatabase.openDatabase(databaseFileName)
+ if not slicer.dicomDatabase.isOpen:
+ logging.error('Unable to open DICOM database ' + databaseDir)
+ return False
+ return True
# ------------------------------------------------------------------------------
def clearDatabase(dicomDatabase=None):
- """Delete entire content (index and copied files) of the DICOM database"""
- # Remove files from index and copied files from disk
- if dicomDatabase is None:
- dicomDatabase = slicer.dicomDatabase
- patientIds = dicomDatabase.patients()
- for patientId in patientIds:
- dicomDatabase.removePatient(patientId)
- # Delete empty folders remaining after removing copied files
- removeEmptyDirs(dicomDatabase.databaseDirectory + '/dicom')
- dicomDatabase.databaseChanged()
+ """Delete entire content (index and copied files) of the DICOM database"""
+ # Remove files from index and copied files from disk
+ if dicomDatabase is None:
+ dicomDatabase = slicer.dicomDatabase
+ patientIds = dicomDatabase.patients()
+ for patientId in patientIds:
+ dicomDatabase.removePatient(patientId)
+ # Delete empty folders remaining after removing copied files
+ removeEmptyDirs(dicomDatabase.databaseDirectory + '/dicom')
+ dicomDatabase.databaseChanged()
def removeEmptyDirs(path):
- for root, dirnames, filenames in os.walk(path, topdown=False):
- for dirname in dirnames:
- removeEmptyDirs(os.path.realpath(os.path.join(root, dirname)))
- try:
- os.rmdir(os.path.realpath(os.path.join(root, dirname)))
- except OSError as e:
- logging.error("Removing directory failed: " + str(e))
+ for root, dirnames, filenames in os.walk(path, topdown=False):
+ for dirname in dirnames:
+ removeEmptyDirs(os.path.realpath(os.path.join(root, dirname)))
+ try:
+ os.rmdir(os.path.realpath(os.path.join(root, dirname)))
+ except OSError as e:
+ logging.error("Removing directory failed: " + str(e))
# ------------------------------------------------------------------------------
def openTemporaryDatabase(directory=None):
- """ Temporarily change the main DICOM database folder location,
- return current database directory. Useful for tests and demos.
- Call closeTemporaryDatabase to restore the original database folder.
- """
- # Specify temporary directory
- if not directory or directory == '':
- from time import gmtime, strftime
- directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase'
- if os.path.isabs(directory):
- tempDatabaseDir = directory
- else:
- tempDatabaseDir = slicer.app.temporaryPath + '/' + directory
- logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir)
- if not os.access(tempDatabaseDir, os.F_OK):
- qt.QDir().mkpath(tempDatabaseDir)
-
- # Get original database directory to be able to restore it later
- settings = qt.QSettings()
- originalDatabaseDir = settings.value(slicer.dicomDatabaseDirectorySettingsKey)
- settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, tempDatabaseDir)
-
- openDatabase(tempDatabaseDir)
-
- # Clear the entire database
- slicer.dicomDatabase.initializeDatabase()
-
- return originalDatabaseDir
+ """ Temporarily change the main DICOM database folder location,
+ return current database directory. Useful for tests and demos.
+ Call closeTemporaryDatabase to restore the original database folder.
+ """
+ # Specify temporary directory
+ if not directory or directory == '':
+ from time import gmtime, strftime
+ directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase'
+ if os.path.isabs(directory):
+ tempDatabaseDir = directory
+ else:
+ tempDatabaseDir = slicer.app.temporaryPath + '/' + directory
+ logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir)
+ if not os.access(tempDatabaseDir, os.F_OK):
+ qt.QDir().mkpath(tempDatabaseDir)
+
+ # Get original database directory to be able to restore it later
+ settings = qt.QSettings()
+ originalDatabaseDir = settings.value(slicer.dicomDatabaseDirectorySettingsKey)
+ settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, tempDatabaseDir)
+
+ openDatabase(tempDatabaseDir)
+
+ # Clear the entire database
+ slicer.dicomDatabase.initializeDatabase()
+
+ return originalDatabaseDir
# ------------------------------------------------------------------------------
def closeTemporaryDatabase(originalDatabaseDir, cleanup=True):
- """ Close temporary DICOM database and remove its directory if requested
- """
- if slicer.dicomDatabase.isOpen:
- if cleanup:
- slicer.dicomDatabase.initializeDatabase()
- # TODO: The database files cannot be deleted even if the database is closed.
- # Not critical, as it will be empty, so will not take measurable disk space.
- # import shutil
- # databaseDir = os.path.split(slicer.dicomDatabase.databaseFilename)[0]
- # shutil.rmtree(databaseDir)
- # if os.access(databaseDir, os.F_OK):
- # logging.error('Failed to delete DICOM database ' + databaseDir)
- slicer.dicomDatabase.closeDatabase()
- else:
- logging.error('Unable to close DICOM database ' + slicer.dicomDatabase.databaseFilename)
-
- if originalDatabaseDir is None:
- # Only log debug if there was no original database, as it is a valid use case,
- # see openTemporaryDatabase
- logging.debug('No original database directory was specified')
- return True
+ """ Close temporary DICOM database and remove its directory if requested
+ """
+ if slicer.dicomDatabase.isOpen:
+ if cleanup:
+ slicer.dicomDatabase.initializeDatabase()
+ # TODO: The database files cannot be deleted even if the database is closed.
+ # Not critical, as it will be empty, so will not take measurable disk space.
+ # import shutil
+ # databaseDir = os.path.split(slicer.dicomDatabase.databaseFilename)[0]
+ # shutil.rmtree(databaseDir)
+ # if os.access(databaseDir, os.F_OK):
+ # logging.error('Failed to delete DICOM database ' + databaseDir)
+ slicer.dicomDatabase.closeDatabase()
+ else:
+ logging.error('Unable to close DICOM database ' + slicer.dicomDatabase.databaseFilename)
- settings = qt.QSettings()
- settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, originalDatabaseDir)
+ if originalDatabaseDir is None:
+ # Only log debug if there was no original database, as it is a valid use case,
+ # see openTemporaryDatabase
+ logging.debug('No original database directory was specified')
+ return True
- # Attempt to re-open original database only if it exists
- if os.access(originalDatabaseDir, os.F_OK):
- success = openDatabase(originalDatabaseDir)
- if not success:
- logging.error('Unable to open DICOM database ' + originalDatabaseDir)
- return False
+ settings = qt.QSettings()
+ settings.setValue(slicer.dicomDatabaseDirectorySettingsKey, originalDatabaseDir)
- return True
+ # Attempt to re-open original database only if it exists
+ if os.access(originalDatabaseDir, os.F_OK):
+ success = openDatabase(originalDatabaseDir)
+ if not success:
+ logging.error('Unable to open DICOM database ' + originalDatabaseDir)
+ return False
+
+ return True
# ------------------------------------------------------------------------------
def createTemporaryDatabase(directory=None):
- """ Open temporary DICOM database, return new database object
- """
- # Specify temporary directory
- if not directory or directory == '':
- from time import gmtime, strftime
- directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase'
- if os.path.isabs(directory):
- tempDatabaseDir = directory
- else:
- tempDatabaseDir = slicer.app.temporaryPath + '/' + directory
- logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir)
- if not os.access(tempDatabaseDir, os.F_OK):
- qt.QDir().mkpath(tempDatabaseDir)
-
- databaseFileName = tempDatabaseDir + "/ctkDICOM.sql"
- dicomDatabase = ctk.ctkDICOMDatabase()
- dicomDatabase.openDatabase(databaseFileName)
- if dicomDatabase.isOpen:
- if dicomDatabase.schemaVersionLoaded() != dicomDatabase.schemaVersion():
- dicomDatabase.closeDatabase()
-
- if dicomDatabase.isOpen:
- return dicomDatabase
- else:
- return None
+ """ Open temporary DICOM database, return new database object
+ """
+ # Specify temporary directory
+ if not directory or directory == '':
+ from time import gmtime, strftime
+ directory = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'TempDICOMDatabase'
+ if os.path.isabs(directory):
+ tempDatabaseDir = directory
+ else:
+ tempDatabaseDir = slicer.app.temporaryPath + '/' + directory
+ logging.info('Switching to temporary DICOM database: ' + tempDatabaseDir)
+ if not os.access(tempDatabaseDir, os.F_OK):
+ qt.QDir().mkpath(tempDatabaseDir)
+
+ databaseFileName = tempDatabaseDir + "/ctkDICOM.sql"
+ dicomDatabase = ctk.ctkDICOMDatabase()
+ dicomDatabase.openDatabase(databaseFileName)
+ if dicomDatabase.isOpen:
+ if dicomDatabase.schemaVersionLoaded() != dicomDatabase.schemaVersion():
+ dicomDatabase.closeDatabase()
+
+ if dicomDatabase.isOpen:
+ return dicomDatabase
+ else:
+ return None
# ------------------------------------------------------------------------------
def deleteTemporaryDatabase(dicomDatabase, cleanup=True):
- """ Close temporary DICOM database and remove its directory if requested
- """
- dicomDatabase.closeDatabase()
+ """ Close temporary DICOM database and remove its directory if requested
+ """
+ dicomDatabase.closeDatabase()
- if cleanup:
- import shutil
- databaseDir = os.path.split(dicomDatabase.databaseFilename)[0]
- shutil.rmtree(databaseDir)
- if os.access(databaseDir, os.F_OK):
- logging.error('Failed to delete DICOM database ' + databaseDir)
- # Database is still in use, at least clear its content
- dicomDatabase.initializeDatabase()
+ if cleanup:
+ import shutil
+ databaseDir = os.path.split(dicomDatabase.databaseFilename)[0]
+ shutil.rmtree(databaseDir)
+ if os.access(databaseDir, os.F_OK):
+ logging.error('Failed to delete DICOM database ' + databaseDir)
+ # Database is still in use, at least clear its content
+ dicomDatabase.initializeDatabase()
- return True
+ return True
# ------------------------------------------------------------------------------
class TemporaryDICOMDatabase:
- """Context manager to conveniently use temporary DICOM database.
- It creates a new DICOM database and temporarily sets it as the main
- DICOM database in the application (slicer.dicomDatabase).
- """
+ """Context manager to conveniently use temporary DICOM database.
+ It creates a new DICOM database and temporarily sets it as the main
+ DICOM database in the application (slicer.dicomDatabase).
+ """
- def __init__(self, directory=None):
- self.temporaryDatabaseDir = directory
- self.originalDatabaseDir = None
+ def __init__(self, directory=None):
+ self.temporaryDatabaseDir = directory
+ self.originalDatabaseDir = None
- def __enter__(self):
- self.originalDatabaseDir = openTemporaryDatabase(self.temporaryDatabaseDir)
- return slicer.dicomDatabase
+ def __enter__(self):
+ self.originalDatabaseDir = openTemporaryDatabase(self.temporaryDatabaseDir)
+ return slicer.dicomDatabase
- def __exit__(self, type, value, traceback):
- closeTemporaryDatabase(self.originalDatabaseDir)
+ def __exit__(self, type, value, traceback):
+ closeTemporaryDatabase(self.originalDatabaseDir)
# ------------------------------------------------------------------------------
def importDicom(dicomDataDir, dicomDatabase=None, copyFiles=False):
- """ Import DICOM files from folder into Slicer database
- """
- try:
- indexer = ctk.ctkDICOMIndexer()
- assert indexer is not None
- if dicomDatabase is None:
- dicomDatabase = slicer.dicomDatabase
- indexer.addDirectory(dicomDatabase, dicomDataDir, copyFiles)
- indexer.waitForImportFinished()
- except Exception as e:
- import traceback
- traceback.print_exc()
- logging.error('Failed to import DICOM folder ' + dicomDataDir)
- return False
- return True
+ """ Import DICOM files from folder into Slicer database
+ """
+ try:
+ indexer = ctk.ctkDICOMIndexer()
+ assert indexer is not None
+ if dicomDatabase is None:
+ dicomDatabase = slicer.dicomDatabase
+ indexer.addDirectory(dicomDatabase, dicomDataDir, copyFiles)
+ indexer.waitForImportFinished()
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ logging.error('Failed to import DICOM folder ' + dicomDataDir)
+ return False
+ return True
# ------------------------------------------------------------------------------
def loadSeriesWithVerification(seriesUIDs, expectedSelectedPlugins=None, expectedLoadedNodes=None):
- """ Load series by UID, and verify loadable selection and loaded nodes.
-
- ``selectedPlugins`` example: { 'Scalar Volume':1, 'RT':2 }
- ``expectedLoadedNodes`` example: { 'vtkMRMLScalarVolumeNode':2, 'vtkMRMLSegmentationNode':1 }
- """
- if not slicer.dicomDatabase.isOpen:
- logging.error('DICOM module or database cannot be accessed')
- return False
- if seriesUIDs is None or len(seriesUIDs) == 0:
- logging.error('No series UIDs given')
- return False
-
- fileLists = []
- for seriesUID in seriesUIDs:
- fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID))
-
- if len(fileLists) == 0:
- logging.error('No files found for DICOM series list')
- return False
-
- loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists)
- success = True
-
- # Verify loadables if baseline is given
- if expectedSelectedPlugins is not None and len(expectedSelectedPlugins.keys()) > 0:
- actualSelectedPlugins = {}
- for plugin in loadablesByPlugin:
- for loadable in loadablesByPlugin[plugin]:
- if loadable.selected:
- if plugin.loadType in actualSelectedPlugins:
- count = int(actualSelectedPlugins[plugin.loadType])
- actualSelectedPlugins[plugin.loadType] = count + 1
- else:
- actualSelectedPlugins[plugin.loadType] = 1
- for pluginName in expectedSelectedPlugins.keys():
- if pluginName not in actualSelectedPlugins:
- logging.error("Expected DICOM plugin '%s' was not selected" % (pluginName))
- success = False
- elif actualSelectedPlugins[pluginName] != expectedSelectedPlugins[pluginName]:
- logging.error("DICOM plugin '%s' was expected to be selected in %d loadables, but was selected in %d" % \
- (pluginName, expectedSelectedPlugins[pluginName], actualSelectedPlugins[pluginName]))
- success = False
-
- # Count relevant node types in scene
- actualLoadedNodes = {}
- if expectedLoadedNodes is not None:
- for nodeType in expectedLoadedNodes.keys():
- nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType)
- nodeCollection.UnRegister(None)
- actualLoadedNodes[nodeType] = nodeCollection.GetNumberOfItems()
-
- # Load selected data
- loadedNodeIDs = loadLoadables(loadablesByPlugin)
-
- if expectedLoadedNodes is not None:
- for nodeType in expectedLoadedNodes.keys():
- nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType)
- nodeCollection.UnRegister(None)
- numOfLoadedNodes = nodeCollection.GetNumberOfItems() - actualLoadedNodes[nodeType]
- if numOfLoadedNodes != expectedLoadedNodes[nodeType]:
- logging.error("Number of loaded %s nodes was %d, but %d was expected" % \
- (nodeType, numOfLoadedNodes, expectedLoadedNodes[nodeType]))
- success = False
-
- return success
+ """ Load series by UID, and verify loadable selection and loaded nodes.
+
+ ``selectedPlugins`` example: { 'Scalar Volume':1, 'RT':2 }
+ ``expectedLoadedNodes`` example: { 'vtkMRMLScalarVolumeNode':2, 'vtkMRMLSegmentationNode':1 }
+ """
+ if not slicer.dicomDatabase.isOpen:
+ logging.error('DICOM module or database cannot be accessed')
+ return False
+ if seriesUIDs is None or len(seriesUIDs) == 0:
+ logging.error('No series UIDs given')
+ return False
+
+ fileLists = []
+ for seriesUID in seriesUIDs:
+ fileLists.append(slicer.dicomDatabase.filesForSeries(seriesUID))
+
+ if len(fileLists) == 0:
+ logging.error('No files found for DICOM series list')
+ return False
+
+ loadablesByPlugin, loadEnabled = getLoadablesFromFileLists(fileLists)
+ success = True
+
+ # Verify loadables if baseline is given
+ if expectedSelectedPlugins is not None and len(expectedSelectedPlugins.keys()) > 0:
+ actualSelectedPlugins = {}
+ for plugin in loadablesByPlugin:
+ for loadable in loadablesByPlugin[plugin]:
+ if loadable.selected:
+ if plugin.loadType in actualSelectedPlugins:
+ count = int(actualSelectedPlugins[plugin.loadType])
+ actualSelectedPlugins[plugin.loadType] = count + 1
+ else:
+ actualSelectedPlugins[plugin.loadType] = 1
+ for pluginName in expectedSelectedPlugins.keys():
+ if pluginName not in actualSelectedPlugins:
+ logging.error("Expected DICOM plugin '%s' was not selected" % (pluginName))
+ success = False
+ elif actualSelectedPlugins[pluginName] != expectedSelectedPlugins[pluginName]:
+ logging.error("DICOM plugin '%s' was expected to be selected in %d loadables, but was selected in %d" % \
+ (pluginName, expectedSelectedPlugins[pluginName], actualSelectedPlugins[pluginName]))
+ success = False
+
+ # Count relevant node types in scene
+ actualLoadedNodes = {}
+ if expectedLoadedNodes is not None:
+ for nodeType in expectedLoadedNodes.keys():
+ nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType)
+ nodeCollection.UnRegister(None)
+ actualLoadedNodes[nodeType] = nodeCollection.GetNumberOfItems()
+
+ # Load selected data
+ loadedNodeIDs = loadLoadables(loadablesByPlugin)
+
+ if expectedLoadedNodes is not None:
+ for nodeType in expectedLoadedNodes.keys():
+ nodeCollection = slicer.mrmlScene.GetNodesByClass(nodeType)
+ nodeCollection.UnRegister(None)
+ numOfLoadedNodes = nodeCollection.GetNumberOfItems() - actualLoadedNodes[nodeType]
+ if numOfLoadedNodes != expectedLoadedNodes[nodeType]:
+ logging.error("Number of loaded %s nodes was %d, but %d was expected" % \
+ (nodeType, numOfLoadedNodes, expectedLoadedNodes[nodeType]))
+ success = False
+
+ return success
# ------------------------------------------------------------------------------
def allSeriesUIDsInDatabase(database=None):
- """ Collect all series instance UIDs in a DICOM database (the Slicer one by default)
-
- Useful to get list of just imported series UIDs, for example:
- newSeriesUIDs = [x for x in seriesUIDsAfter if x not in seriesUIDsBefore]
- """
- if database is None:
- database = slicer.dicomDatabase
- dicomWidget = slicer.modules.dicom.widgetRepresentation().self()
- allSeriesUIDs = []
- for patient in database.patients():
- studies = database.studiesForPatient(patient)
- series = [database.seriesForStudy(study) for study in studies]
- seriesUIDs = [uid for uidList in series for uid in uidList]
- allSeriesUIDs.extend(seriesUIDs)
- return allSeriesUIDs
+ """ Collect all series instance UIDs in a DICOM database (the Slicer one by default)
+
+ Useful to get list of just imported series UIDs, for example:
+ newSeriesUIDs = [x for x in seriesUIDsAfter if x not in seriesUIDsBefore]
+ """
+ if database is None:
+ database = slicer.dicomDatabase
+ dicomWidget = slicer.modules.dicom.widgetRepresentation().self()
+ allSeriesUIDs = []
+ for patient in database.patients():
+ studies = database.studiesForPatient(patient)
+ series = [database.seriesForStudy(study) for study in studies]
+ seriesUIDs = [uid for uidList in series for uid in uidList]
+ allSeriesUIDs.extend(seriesUIDs)
+ return allSeriesUIDs
# ------------------------------------------------------------------------------
def seriesUIDsForFiles(files):
- """ Collect series instance UIDs belonging to a list of files
- """
- seriesUIDs = set()
- for file in files:
- seriesUID = slicer.dicomDatabase.seriesForFile(file)
- if seriesUID != '':
- seriesUIDs.add(seriesUID)
- return seriesUIDs
+ """ Collect series instance UIDs belonging to a list of files
+ """
+ seriesUIDs = set()
+ for file in files:
+ seriesUID = slicer.dicomDatabase.seriesForFile(file)
+ if seriesUID != '':
+ seriesUIDs.add(seriesUID)
+ return seriesUIDs
# ------------------------------------------------------------------------------
class LoadDICOMFilesToDatabase:
- """Context manager to conveniently load DICOM files downloaded zipped from the internet
- """
- def __init__(self, url, archiveFilePath=None, dicomDataDir=None, \
- expectedNumberOfFiles=None, selectedPlugins=None, loadedNodes=None, checksum=None):
- from time import gmtime, strftime
- if archiveFilePath is None:
- fileName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase.zip'
- archiveFilePath = slicer.app.temporaryPath + '/' + fileName
- if dicomDataDir is None:
- directoryName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase'
- dicomDataDir = slicer.app.temporaryPath + '/' + directoryName
-
- self.url = url
- self.checksum = checksum
- self.archiveFilePath = archiveFilePath
- self.dicomDataDir = dicomDataDir
- self.expectedNumberOfExtractedFiles = expectedNumberOfFiles
- self.selectedPlugins = selectedPlugins
- self.loadedNodes = loadedNodes
-
- def __enter__(self):
- if slicer.util.downloadAndExtractArchive(self.url, self.archiveFilePath, \
- self.dicomDataDir, self.expectedNumberOfExtractedFiles,
- checksum=self.checksum):
- dicomFiles = slicer.util.getFilesInDirectory(self.dicomDataDir)
- if importDicom(self.dicomDataDir):
- seriesUIDs = seriesUIDsForFiles(dicomFiles)
- return loadSeriesWithVerification(seriesUIDs, self.selectedPlugins, self.loadedNodes)
- return False
-
- def __exit__(self, type, value, traceback):
- pass
+ """Context manager to conveniently load DICOM files downloaded zipped from the internet
+ """
+ def __init__(self, url, archiveFilePath=None, dicomDataDir=None, \
+ expectedNumberOfFiles=None, selectedPlugins=None, loadedNodes=None, checksum=None):
+ from time import gmtime, strftime
+ if archiveFilePath is None:
+ fileName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase.zip'
+ archiveFilePath = slicer.app.temporaryPath + '/' + fileName
+ if dicomDataDir is None:
+ directoryName = strftime("%Y%m%d_%H%M%S_", gmtime()) + 'LoadDICOMFilesToDatabase'
+ dicomDataDir = slicer.app.temporaryPath + '/' + directoryName
+
+ self.url = url
+ self.checksum = checksum
+ self.archiveFilePath = archiveFilePath
+ self.dicomDataDir = dicomDataDir
+ self.expectedNumberOfExtractedFiles = expectedNumberOfFiles
+ self.selectedPlugins = selectedPlugins
+ self.loadedNodes = loadedNodes
+
+ def __enter__(self):
+ if slicer.util.downloadAndExtractArchive(self.url, self.archiveFilePath, \
+ self.dicomDataDir, self.expectedNumberOfExtractedFiles,
+ checksum=self.checksum):
+ dicomFiles = slicer.util.getFilesInDirectory(self.dicomDataDir)
+ if importDicom(self.dicomDataDir):
+ seriesUIDs = seriesUIDsForFiles(dicomFiles)
+ return loadSeriesWithVerification(seriesUIDs, self.selectedPlugins, self.loadedNodes)
+ return False
+
+ def __exit__(self, type, value, traceback):
+ pass
# ------------------------------------------------------------------------------
@@ -595,380 +595,380 @@ def __exit__(self, type, value, traceback):
# - is there gantry tilt?
# - are the orientations the same for all slices?
def getSortedImageFiles(filePaths, epsilon=0.01):
- """ Sort DICOM image files in increasing slice order (IS direction) corresponding to a series
-
- Use the first file to get the ImageOrientationPatient for the
- series and calculate the scan direction (assumed to be perpendicular
- to the acquisition plane)
-
- epsilon: Maximum difference in distance between slices to consider spacing uniform
- """
- warningText = ''
- if len(filePaths) == 0:
- return filePaths, [], warningText
-
- # Define DICOM tags used in this function
- tags = {}
- tags['position'] = "0020,0032"
- tags['orientation'] = "0020,0037"
- tags['numberOfFrames'] = "0028,0008"
- tags['seriesUID'] = "0020,000E"
-
- seriesUID = slicer.dicomDatabase.fileValue(filePaths[0], tags['seriesUID'])
-
- if slicer.dicomDatabase.fileValue(filePaths[0], tags['numberOfFrames']) not in ["", "1"]:
- warningText += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution.\n"
-
- # Make sure first file contains valid geometry
- ref = {}
- for tag in [tags['position'], tags['orientation']]:
- value = slicer.dicomDatabase.fileValue(filePaths[0], tag)
- if not value or value == "":
- warningText += "Reference image in series does not contain geometry information. Please use caution.\n"
- return filePaths, [], warningText
- ref[tag] = value
-
- # Determine out-of-plane direction for first slice
- import numpy as np
- sliceAxes = [float(zz) for zz in ref[tags['orientation']].split('\\')]
- x = np.array(sliceAxes[:3])
- y = np.array(sliceAxes[3:])
- scanAxis = np.cross(x, y)
- scanOrigin = np.array([float(zz) for zz in ref[tags['position']].split('\\')])
-
- # For each file in series, calculate the distance along the scan axis, sort files by this
- sortList = []
- missingGeometry = False
- for file in filePaths:
- positionStr = slicer.dicomDatabase.fileValue(file, tags['position'])
- orientationStr = slicer.dicomDatabase.fileValue(file, tags['orientation'])
- if not positionStr or positionStr == "" or not orientationStr or orientationStr == "":
- missingGeometry = True
- break
- position = np.array([float(zz) for zz in positionStr.split('\\')])
- vec = position - scanOrigin
- dist = vec.dot(scanAxis)
- sortList.append((file, dist))
-
- if missingGeometry:
- warningText += "One or more images is missing geometry information in series. Please use caution.\n"
- return filePaths, [], warningText
-
- # Sort files names by distance from reference slice
- sortedFiles = sorted(sortList, key=lambda x: x[1])
- files = []
- distances = {}
- for file, dist in sortedFiles:
- files.append(file)
- distances[file] = dist
-
- # Get acquisition geometry regularization setting value
- settings = qt.QSettings()
- acquisitionGeometryRegularizationEnabled = (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform")
-
- # Confirm equal spacing between slices
- # - use variable 'epsilon' to determine the tolerance
- spaceWarnings = 0
- if len(files) > 1:
- file0 = files[0]
- file1 = files[1]
- dist0 = distances[file0]
- dist1 = distances[file1]
- spacing0 = dist1 - dist0
- n = 1
- for fileN in files[1:]:
- fileNminus1 = files[n - 1]
- distN = distances[fileN]
- distNminus1 = distances[fileNminus1]
- spacingN = distN - distNminus1
- spaceError = spacingN - spacing0
- if abs(spaceError) > epsilon:
- spaceWarnings += 1
- warningText += f"Images are not equally spaced (a difference of {spaceError:g} vs {spacing0:g} in spacings was detected)."
- if acquisitionGeometryRegularizationEnabled:
- warningText += " Slicer will apply a transform to this series trying to regularize the volume. Please use caution.\n"
- else:
- warningText += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'"
- " in Application settings / DICOM / DICOMScalarVolumePlugin. Please use caution.\n")
- break
- n += 1
+ """ Sort DICOM image files in increasing slice order (IS direction) corresponding to a series
- if spaceWarnings != 0:
- logging.warning("Geometric issues were found with %d of the series. Please use caution.\n" % spaceWarnings)
+ Use the first file to get the ImageOrientationPatient for the
+ series and calculate the scan direction (assumed to be perpendicular
+ to the acquisition plane)
- return files, distances, warningText
+ epsilon: Maximum difference in distance between slices to consider spacing uniform
+ """
+ warningText = ''
+ if len(filePaths) == 0:
+ return filePaths, [], warningText
+
+ # Define DICOM tags used in this function
+ tags = {}
+ tags['position'] = "0020,0032"
+ tags['orientation'] = "0020,0037"
+ tags['numberOfFrames'] = "0028,0008"
+ tags['seriesUID'] = "0020,000E"
+
+ seriesUID = slicer.dicomDatabase.fileValue(filePaths[0], tags['seriesUID'])
+
+ if slicer.dicomDatabase.fileValue(filePaths[0], tags['numberOfFrames']) not in ["", "1"]:
+ warningText += "Multi-frame image. If slice orientation or spacing is non-uniform then the image may be displayed incorrectly. Use with caution.\n"
+
+ # Make sure first file contains valid geometry
+ ref = {}
+ for tag in [tags['position'], tags['orientation']]:
+ value = slicer.dicomDatabase.fileValue(filePaths[0], tag)
+ if not value or value == "":
+ warningText += "Reference image in series does not contain geometry information. Please use caution.\n"
+ return filePaths, [], warningText
+ ref[tag] = value
+
+ # Determine out-of-plane direction for first slice
+ import numpy as np
+ sliceAxes = [float(zz) for zz in ref[tags['orientation']].split('\\')]
+ x = np.array(sliceAxes[:3])
+ y = np.array(sliceAxes[3:])
+ scanAxis = np.cross(x, y)
+ scanOrigin = np.array([float(zz) for zz in ref[tags['position']].split('\\')])
+
+ # For each file in series, calculate the distance along the scan axis, sort files by this
+ sortList = []
+ missingGeometry = False
+ for file in filePaths:
+ positionStr = slicer.dicomDatabase.fileValue(file, tags['position'])
+ orientationStr = slicer.dicomDatabase.fileValue(file, tags['orientation'])
+ if not positionStr or positionStr == "" or not orientationStr or orientationStr == "":
+ missingGeometry = True
+ break
+ position = np.array([float(zz) for zz in positionStr.split('\\')])
+ vec = position - scanOrigin
+ dist = vec.dot(scanAxis)
+ sortList.append((file, dist))
+
+ if missingGeometry:
+ warningText += "One or more images is missing geometry information in series. Please use caution.\n"
+ return filePaths, [], warningText
+
+ # Sort files names by distance from reference slice
+ sortedFiles = sorted(sortList, key=lambda x: x[1])
+ files = []
+ distances = {}
+ for file, dist in sortedFiles:
+ files.append(file)
+ distances[file] = dist
+
+ # Get acquisition geometry regularization setting value
+ settings = qt.QSettings()
+ acquisitionGeometryRegularizationEnabled = (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform")
+
+ # Confirm equal spacing between slices
+ # - use variable 'epsilon' to determine the tolerance
+ spaceWarnings = 0
+ if len(files) > 1:
+ file0 = files[0]
+ file1 = files[1]
+ dist0 = distances[file0]
+ dist1 = distances[file1]
+ spacing0 = dist1 - dist0
+ n = 1
+ for fileN in files[1:]:
+ fileNminus1 = files[n - 1]
+ distN = distances[fileN]
+ distNminus1 = distances[fileNminus1]
+ spacingN = distN - distNminus1
+ spaceError = spacingN - spacing0
+ if abs(spaceError) > epsilon:
+ spaceWarnings += 1
+ warningText += f"Images are not equally spaced (a difference of {spaceError:g} vs {spacing0:g} in spacings was detected)."
+ if acquisitionGeometryRegularizationEnabled:
+ warningText += " Slicer will apply a transform to this series trying to regularize the volume. Please use caution.\n"
+ else:
+ warningText += (" If loaded image appears distorted, enable 'Acquisition geometry regularization'"
+ " in Application settings / DICOM / DICOMScalarVolumePlugin. Please use caution.\n")
+ break
+ n += 1
+
+ if spaceWarnings != 0:
+ logging.warning("Geometric issues were found with %d of the series. Please use caution.\n" % spaceWarnings)
+
+ return files, distances, warningText
# ------------------------------------------------------------------------------
def refreshDICOMWidget():
- """ Refresh DICOM browser from database.
- It is useful when the database is changed via a database object that is
- different from the one stored in the DICOM browser. There may be multiple
- database connection (through different database objects) in the same process.
- """
- try:
- slicer.modules.DICOMInstance.browserWidget.dicomBrowser.dicomTableManager().updateTableViews()
- except AttributeError:
- logging.error('DICOM module or browser cannot be accessed')
- return False
- return True
+ """ Refresh DICOM browser from database.
+ It is useful when the database is changed via a database object that is
+ different from the one stored in the DICOM browser. There may be multiple
+ database connection (through different database objects) in the same process.
+ """
+ try:
+ slicer.modules.DICOMInstance.browserWidget.dicomBrowser.dicomTableManager().updateTableViews()
+ except AttributeError:
+ logging.error('DICOM module or browser cannot be accessed')
+ return False
+ return True
def getLoadablesFromFileLists(fileLists, pluginClassNames=None, messages=None, progressCallback=None, pluginInstances=None):
- """Take list of file lists, return loadables by plugin dictionary
- """
- detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
- loadablesByPlugin = {}
- loadEnabled = False
- if not isinstance(fileLists, list) or len(fileLists) == 0 or not type(fileLists[0]) in [tuple, list]:
- logging.error('File lists must contain a non-empty list of tuples/lists')
+ """Take list of file lists, return loadables by plugin dictionary
+ """
+ detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
+ loadablesByPlugin = {}
+ loadEnabled = False
+ if not isinstance(fileLists, list) or len(fileLists) == 0 or not type(fileLists[0]) in [tuple, list]:
+ logging.error('File lists must contain a non-empty list of tuples/lists')
+ return loadablesByPlugin, loadEnabled
+
+ if pluginClassNames is None:
+ pluginClassNames = list(slicer.modules.dicomPlugins.keys())
+
+ if pluginInstances is None:
+ pluginInstances = {}
+
+ for step, pluginClassName in enumerate(pluginClassNames):
+ if pluginClassName not in pluginInstances:
+ pluginInstances[pluginClassName] = slicer.modules.dicomPlugins[pluginClassName]()
+ plugin = pluginInstances[pluginClassName]
+ if progressCallback:
+ cancelled = progressCallback(pluginClassName, step * 100 / len(pluginClassNames))
+ if cancelled:
+ break
+ try:
+ if detailedLogging:
+ logging.debug("Examine for import using " + pluginClassName)
+ loadablesByPlugin[plugin] = plugin.examineForImport(fileLists)
+ # If regular method is not overridden (so returns empty list), try old function
+ # Ensuring backwards compatibility: examineForImport used to be called examine
+ if not loadablesByPlugin[plugin]:
+ loadablesByPlugin[plugin] = plugin.examine(fileLists)
+ loadEnabled = loadEnabled or loadablesByPlugin[plugin] != []
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ logging.error("DICOM Plugin failed: %s" % str(e))
+ if messages:
+ messages.append("Plugin failed: %s." % pluginClass)
+
return loadablesByPlugin, loadEnabled
- if pluginClassNames is None:
- pluginClassNames = list(slicer.modules.dicomPlugins.keys())
- if pluginInstances is None:
- pluginInstances = {}
+def loadLoadables(loadablesByPlugin, messages=None, progressCallback=None):
+ """Load each DICOM loadable item.
+ Returns loaded node IDs.
+ """
- for step, pluginClassName in enumerate(pluginClassNames):
- if pluginClassName not in pluginInstances:
- pluginInstances[pluginClassName] = slicer.modules.dicomPlugins[pluginClassName]()
- plugin = pluginInstances[pluginClassName]
- if progressCallback:
- cancelled = progressCallback(pluginClassName, step * 100 / len(pluginClassNames))
- if cancelled:
- break
- try:
- if detailedLogging:
- logging.debug("Examine for import using " + pluginClassName)
- loadablesByPlugin[plugin] = plugin.examineForImport(fileLists)
- # If regular method is not overridden (so returns empty list), try old function
- # Ensuring backwards compatibility: examineForImport used to be called examine
- if not loadablesByPlugin[plugin]:
- loadablesByPlugin[plugin] = plugin.examine(fileLists)
- loadEnabled = loadEnabled or loadablesByPlugin[plugin] != []
- except Exception as e:
- import traceback
- traceback.print_exc()
- logging.error("DICOM Plugin failed: %s" % str(e))
- if messages:
- messages.append("Plugin failed: %s." % pluginClass)
+ # Find a plugin for each loadable that will load it
+ # (the last plugin that has that loadable selected wins)
+ selectedLoadables = {}
+ for plugin in loadablesByPlugin:
+ for loadable in loadablesByPlugin[plugin]:
+ if loadable.selected:
+ selectedLoadables[loadable] = plugin
- return loadablesByPlugin, loadEnabled
+ loadedNodeIDs = []
+ @vtk.calldata_type(vtk.VTK_OBJECT)
+ def onNodeAdded(caller, event, calldata):
+ node = calldata
+ if not isinstance(node, slicer.vtkMRMLStorageNode) and not isinstance(node, slicer.vtkMRMLDisplayNode):
+ loadedNodeIDs.append(node.GetID())
-def loadLoadables(loadablesByPlugin, messages=None, progressCallback=None):
- """Load each DICOM loadable item.
- Returns loaded node IDs.
- """
-
- # Find a plugin for each loadable that will load it
- # (the last plugin that has that loadable selected wins)
- selectedLoadables = {}
- for plugin in loadablesByPlugin:
- for loadable in loadablesByPlugin[plugin]:
- if loadable.selected:
- selectedLoadables[loadable] = plugin
-
- loadedNodeIDs = []
-
- @vtk.calldata_type(vtk.VTK_OBJECT)
- def onNodeAdded(caller, event, calldata):
- node = calldata
- if not isinstance(node, slicer.vtkMRMLStorageNode) and not isinstance(node, slicer.vtkMRMLDisplayNode):
- loadedNodeIDs.append(node.GetID())
-
- sceneObserverTag = slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, onNodeAdded)
-
- for step, (loadable, plugin) in enumerate(selectedLoadables.items(), start=1):
- if progressCallback:
- cancelled = progressCallback(loadable.name, step * 100 / len(selectedLoadables))
- if cancelled:
- break
+ sceneObserverTag = slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, onNodeAdded)
- try:
- loadSuccess = plugin.load(loadable)
- except:
- loadSuccess = False
- import traceback
- logging.error("DICOM plugin failed to load '"
- + loadable.name + "' as a '" + plugin.loadType + "'.\n"
- + traceback.format_exc())
- if (not loadSuccess) and (messages is not None):
- messages.append(f'Could not load: {loadable.name} as a {plugin.loadType}')
-
- cancelled = False
- try:
- # DICOM reader plugins (for example, in PETDICOM extension) may generate additional DICOM files
- # during loading. These must be added to the database.
- for derivedItem in loadable.derivedItems:
- indexer = ctk.ctkDICOMIndexer()
+ for step, (loadable, plugin) in enumerate(selectedLoadables.items(), start=1):
if progressCallback:
- cancelled = progressCallback(f"{loadable.name} ({derivedItem})", step * 100 / len(selectedLoadables))
- if cancelled:
+ cancelled = progressCallback(loadable.name, step * 100 / len(selectedLoadables))
+ if cancelled:
+ break
+
+ try:
+ loadSuccess = plugin.load(loadable)
+ except:
+ loadSuccess = False
+ import traceback
+ logging.error("DICOM plugin failed to load '"
+ + loadable.name + "' as a '" + plugin.loadType + "'.\n"
+ + traceback.format_exc())
+ if (not loadSuccess) and (messages is not None):
+ messages.append(f'Could not load: {loadable.name} as a {plugin.loadType}')
+
+ cancelled = False
+ try:
+ # DICOM reader plugins (for example, in PETDICOM extension) may generate additional DICOM files
+ # during loading. These must be added to the database.
+ for derivedItem in loadable.derivedItems:
+ indexer = ctk.ctkDICOMIndexer()
+ if progressCallback:
+ cancelled = progressCallback(f"{loadable.name} ({derivedItem})", step * 100 / len(selectedLoadables))
+ if cancelled:
+ break
+ indexer.addFile(slicer.dicomDatabase, derivedItem)
+ except AttributeError:
+ # no derived items or some other attribute error
+ pass
+ if cancelled:
break
- indexer.addFile(slicer.dicomDatabase, derivedItem)
- except AttributeError:
- # no derived items or some other attribute error
- pass
- if cancelled:
- break
- slicer.mrmlScene.RemoveObserver(sceneObserverTag)
+ slicer.mrmlScene.RemoveObserver(sceneObserverTag)
- return loadedNodeIDs
+ return loadedNodeIDs
def importFromDICOMWeb(dicomWebEndpoint, studyInstanceUID, seriesInstanceUID=None, accessToken=None):
- """
- Downloads and imports DICOM series from a DICOMweb instance.
- Progress is displayed and if errors occur then they are displayed in a popup window in the end.
- If all the instances in a series are already imported then the series will not be retrieved and imported again.
+ """
+ Downloads and imports DICOM series from a DICOMweb instance.
+ Progress is displayed and if errors occur then they are displayed in a popup window in the end.
+ If all the instances in a series are already imported then the series will not be retrieved and imported again.
- :param dicomWebEndpoint: Endpoint URL for retrieving the study/series from DICOMweb
- :param studyInstanceUID: UID for the study to be downloaded
- :param seriesInstanceUID: UID for the series to be downloaded. If not specified, all series will be downloaded from the study
- :param accessToken: Optional access token for the query
- :return: List of imported study UIDs
+ :param dicomWebEndpoint: Endpoint URL for retrieving the study/series from DICOMweb
+ :param studyInstanceUID: UID for the study to be downloaded
+ :param seriesInstanceUID: UID for the series to be downloaded. If not specified, all series will be downloaded from the study
+ :param accessToken: Optional access token for the query
+ :return: List of imported study UIDs
- Example: calling from PythonSlicer console
+ Example: calling from PythonSlicer console
- .. code-block:: python
+ .. code-block:: python
- from DICOMLib import DICOMUtils
- loadedUIDs = DICOMUtils.importFromDICOMWeb(dicomWebEndpoint="https://yourdicomweburl/dicomWebEndpoint",
- studyInstanceUID="2.16.840.1.113669.632.20.1211.10000509338")
- accessToken="YOUR_ACCESS_TOKEN")
+ from DICOMLib import DICOMUtils
+ loadedUIDs = DICOMUtils.importFromDICOMWeb(dicomWebEndpoint="https://yourdicomweburl/dicomWebEndpoint",
+ studyInstanceUID="2.16.840.1.113669.632.20.1211.10000509338")
+ accessToken="YOUR_ACCESS_TOKEN")
- """
+ """
- from dicomweb_client.api import DICOMwebClient
+ from dicomweb_client.api import DICOMwebClient
- seriesImported = []
- errors = []
- clientLogger = logging.getLogger('dicomweb_client.api')
- originalClientLogLevel = clientLogger.level
+ seriesImported = []
+ errors = []
+ clientLogger = logging.getLogger('dicomweb_client.api')
+ originalClientLogLevel = clientLogger.level
- progressDialog = slicer.util.createProgressDialog(parent=slicer.util.mainWindow(), value=0, maximum=100)
- try:
- progressDialog.labelText = f'Retrieving series list...'
- slicer.app.processEvents()
+ progressDialog = slicer.util.createProgressDialog(parent=slicer.util.mainWindow(), value=0, maximum=100)
+ try:
+ progressDialog.labelText = f'Retrieving series list...'
+ slicer.app.processEvents()
- if accessToken is None:
- client = DICOMwebClient(url=dicomWebEndpoint)
- else:
- client = DICOMwebClient(
+ if accessToken is None:
+ client = DICOMwebClient(url=dicomWebEndpoint)
+ else:
+ client = DICOMwebClient(
url=dicomWebEndpoint,
headers={"Authorization": f"Bearer {accessToken}"},
- )
-
- seriesList = client.search_for_series(study_instance_uid=studyInstanceUID)
- seriesInstanceUIDs = []
- if not seriesInstanceUID is None:
- seriesInstanceUIDs = [seriesInstanceUID]
- else:
- for series in seriesList:
- currentSeriesInstanceUID = series['0020000E']['Value'][0]
- seriesInstanceUIDs.append(currentSeriesInstanceUID)
-
- # Turn off detailed logging, because it would slow down the file transfer
- clientLogger.setLevel(logging.WARNING)
-
- fileNumber = 0
- cancelled = False
- for seriesIndex, currentSeriesInstanceUID in enumerate(seriesInstanceUIDs):
- progressDialog.labelText = f'Retrieving series {seriesIndex+1} of {len(seriesInstanceUIDs)}...'
- slicer.app.processEvents()
-
- try:
- seriesInfo = client.retrieve_series_metadata(
- study_instance_uid=studyInstanceUID,
- series_instance_uid=currentSeriesInstanceUID)
- numberOfInstances = len(seriesInfo)
-
- # Skip retrieve and import of this series if it is already imported
- alreadyImportedInstances = slicer.dicomDatabase.instancesForSeries(currentSeriesInstanceUID)
- seriesAlreadyImported = True
- for serieInfo in seriesInfo:
- sopInstanceUID = serieInfo['00080018']['Value'][0]
- if sopInstanceUID not in alreadyImportedInstances:
- seriesAlreadyImported = False
- break
- if seriesAlreadyImported:
- seriesImported.append(currentSeriesInstanceUID)
- continue
-
- instances = client.iter_series(
- study_instance_uid=studyInstanceUID,
- series_instance_uid=currentSeriesInstanceUID)
-
- slicer.app.processEvents()
- cancelled = progressDialog.wasCanceled
- if cancelled:
- break
-
- outputDirectoryBase = slicer.dicomDatabase.databaseDirectory + "/DICOMweb"
- if not os.access(outputDirectoryBase, os.F_OK):
- os.makedirs(outputDirectoryBase)
- outputDirectoryBase += "/" + qt.QDateTime.currentDateTime().toString("yyyyMMdd-hhmmss")
- outputDirectory = qt.QTemporaryDir(outputDirectoryBase) # Add unique substring to directory
- outputDirectory.setAutoRemove(False)
- outputDirectoryPath = outputDirectory.path()
-
- for instanceIndex, instance in enumerate(instances):
- progressDialog.setValue(int(100 * instanceIndex / numberOfInstances))
- slicer.app.processEvents()
- cancelled = progressDialog.wasCanceled
- if cancelled:
- break
- filename = outputDirectoryPath + "/" + str(fileNumber) + ".dcm"
- instance.save_as(filename)
- fileNumber += 1
-
- if cancelled:
- # cancel was requested in instance retrieve loop,
- # stop the entire import process
- break
+ )
- importDicom(outputDirectoryPath)
- seriesImported.append(currentSeriesInstanceUID)
+ seriesList = client.search_for_series(study_instance_uid=studyInstanceUID)
+ seriesInstanceUIDs = []
+ if not seriesInstanceUID is None:
+ seriesInstanceUIDs = [seriesInstanceUID]
+ else:
+ for series in seriesList:
+ currentSeriesInstanceUID = series['0020000E']['Value'][0]
+ seriesInstanceUIDs.append(currentSeriesInstanceUID)
+
+ # Turn off detailed logging, because it would slow down the file transfer
+ clientLogger.setLevel(logging.WARNING)
+
+ fileNumber = 0
+ cancelled = False
+ for seriesIndex, currentSeriesInstanceUID in enumerate(seriesInstanceUIDs):
+ progressDialog.labelText = f'Retrieving series {seriesIndex+1} of {len(seriesInstanceUIDs)}...'
+ slicer.app.processEvents()
+
+ try:
+ seriesInfo = client.retrieve_series_metadata(
+ study_instance_uid=studyInstanceUID,
+ series_instance_uid=currentSeriesInstanceUID)
+ numberOfInstances = len(seriesInfo)
+
+ # Skip retrieve and import of this series if it is already imported
+ alreadyImportedInstances = slicer.dicomDatabase.instancesForSeries(currentSeriesInstanceUID)
+ seriesAlreadyImported = True
+ for serieInfo in seriesInfo:
+ sopInstanceUID = serieInfo['00080018']['Value'][0]
+ if sopInstanceUID not in alreadyImportedInstances:
+ seriesAlreadyImported = False
+ break
+ if seriesAlreadyImported:
+ seriesImported.append(currentSeriesInstanceUID)
+ continue
+
+ instances = client.iter_series(
+ study_instance_uid=studyInstanceUID,
+ series_instance_uid=currentSeriesInstanceUID)
+
+ slicer.app.processEvents()
+ cancelled = progressDialog.wasCanceled
+ if cancelled:
+ break
+
+ outputDirectoryBase = slicer.dicomDatabase.databaseDirectory + "/DICOMweb"
+ if not os.access(outputDirectoryBase, os.F_OK):
+ os.makedirs(outputDirectoryBase)
+ outputDirectoryBase += "/" + qt.QDateTime.currentDateTime().toString("yyyyMMdd-hhmmss")
+ outputDirectory = qt.QTemporaryDir(outputDirectoryBase) # Add unique substring to directory
+ outputDirectory.setAutoRemove(False)
+ outputDirectoryPath = outputDirectory.path()
+
+ for instanceIndex, instance in enumerate(instances):
+ progressDialog.setValue(int(100 * instanceIndex / numberOfInstances))
+ slicer.app.processEvents()
+ cancelled = progressDialog.wasCanceled
+ if cancelled:
+ break
+ filename = outputDirectoryPath + "/" + str(fileNumber) + ".dcm"
+ instance.save_as(filename)
+ fileNumber += 1
+
+ if cancelled:
+ # cancel was requested in instance retrieve loop,
+ # stop the entire import process
+ break
+
+ importDicom(outputDirectoryPath)
+ seriesImported.append(currentSeriesInstanceUID)
+
+ except Exception as e:
+ import traceback
+ errors.append(f"Error importing series {currentSeriesInstanceUID}: {str(e)} ({traceback.format_exc()})")
- except Exception as e:
+ except Exception as e:
import traceback
- errors.append(f"Error importing series {currentSeriesInstanceUID}: {str(e)} ({traceback.format_exc()})")
+ errors.append(f"{str(e)} ({traceback.format_exc()})")
- except Exception as e:
- import traceback
- errors.append(f"{str(e)} ({traceback.format_exc()})")
+ finally:
+ progressDialog.close()
+ clientLogger.setLevel(originalClientLogLevel)
- finally:
- progressDialog.close()
- clientLogger.setLevel(originalClientLogLevel)
+ if errors:
+ slicer.util.errorDisplay(f"Errors occurred during DICOMweb import of {len(errors)} series.", detailedText="\n\n".join(errors))
+ elif cancelled and (len(seriesImported) < len(seriesInstanceUIDs)):
+ slicer.util.infoDisplay(f"DICOMweb import has been interrupted after completing {len(seriesImported)} out of {len(seriesInstanceUIDs)} series.")
- if errors:
- slicer.util.errorDisplay(f"Errors occurred during DICOMweb import of {len(errors)} series.", detailedText="\n\n".join(errors))
- elif cancelled and (len(seriesImported) < len(seriesInstanceUIDs)):
- slicer.util.infoDisplay(f"DICOMweb import has been interrupted after completing {len(seriesImported)} out of {len(seriesInstanceUIDs)} series.")
-
- return seriesImported
+ return seriesImported
def registerSlicerURLHandler():
- """
- Registers file associations and applicationName:// protocol (e.g., Slicer://)
- with this executable. This allows Kheops (https://demo.kheops.online) open
- images selected in the web browser directly in Slicer.
- For now, only implemented on Windows.
- """
- if os.name == 'nt':
- launcherPath = qt.QDir.toNativeSeparators(qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath())
- reg = qt.QSettings(f"HKEY_CURRENT_USER\\Software\\Classes", qt.QSettings.NativeFormat)
- reg.setValue(f"{slicer.app.applicationName}/.", f"{slicer.app.applicationName} supported file")
- reg.setValue(f"{slicer.app.applicationName}/URL protocol", "")
- reg.setValue(f"{slicer.app.applicationName}/shell/open/command/.", f"\"{launcherPath}\" \"%1\"")
- reg.setValue(f"{slicer.app.applicationName}/DefaultIcon/.", f"{slicer.app.applicationName}.exe,0")
- for ext in ['mrml', 'mrb']:
- reg.setValue(f".{ext}/.", f"{slicer.app.applicationName}")
- reg.setValue(f".{ext}/Content Type", f"application/x-{ext}")
- else:
- raise NotImplementedError()
+ """
+ Registers file associations and applicationName:// protocol (e.g., Slicer://)
+ with this executable. This allows Kheops (https://demo.kheops.online) open
+ images selected in the web browser directly in Slicer.
+ For now, only implemented on Windows.
+ """
+ if os.name == 'nt':
+ launcherPath = qt.QDir.toNativeSeparators(qt.QFileInfo(slicer.app.launcherExecutableFilePath).absoluteFilePath())
+ reg = qt.QSettings(f"HKEY_CURRENT_USER\\Software\\Classes", qt.QSettings.NativeFormat)
+ reg.setValue(f"{slicer.app.applicationName}/.", f"{slicer.app.applicationName} supported file")
+ reg.setValue(f"{slicer.app.applicationName}/URL protocol", "")
+ reg.setValue(f"{slicer.app.applicationName}/shell/open/command/.", f"\"{launcherPath}\" \"%1\"")
+ reg.setValue(f"{slicer.app.applicationName}/DefaultIcon/.", f"{slicer.app.applicationName}.exe,0")
+ for ext in ['mrml', 'mrb']:
+ reg.setValue(f".{ext}/.", f"{slicer.app.applicationName}")
+ reg.setValue(f".{ext}/Content Type", f"application/x-{ext}")
+ else:
+ raise NotImplementedError()
diff --git a/Modules/Scripted/DICOMPatcher/DICOMPatcher.py b/Modules/Scripted/DICOMPatcher/DICOMPatcher.py
index 6a33af2c340..82c3e83c13a 100644
--- a/Modules/Scripted/DICOMPatcher/DICOMPatcher.py
+++ b/Modules/Scripted/DICOMPatcher/DICOMPatcher.py
@@ -13,19 +13,19 @@
#
class DICOMPatcher(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "DICOM Patcher"
- self.parent.categories = ["Utilities"]
- self.parent.dependencies = ["DICOM"]
- self.parent.contributors = ["Andras Lasso (PerkLab)"]
- self.parent.helpText = """Fix common issues in DICOM files. This module may help fixing DICOM files that Slicer fails to import."""
- self.parent.helpText += parent.defaultDocumentationLink
- self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab."""
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "DICOM Patcher"
+ self.parent.categories = ["Utilities"]
+ self.parent.dependencies = ["DICOM"]
+ self.parent.contributors = ["Andras Lasso (PerkLab)"]
+ self.parent.helpText = """Fix common issues in DICOM files. This module may help fixing DICOM files that Slicer fails to import."""
+ self.parent.helpText += parent.defaultDocumentationLink
+ self.parent.acknowledgementText = """This file was originally developed by Andras Lasso, PerkLab."""
#
@@ -33,141 +33,141 @@ def __init__(self, parent):
#
class DICOMPatcherWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Instantiate and connect widgets ...
-
- #
- # Parameters Area
- #
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Parameters"
- self.layout.addWidget(parametersCollapsibleButton)
-
- # Layout within the dummy collapsible button
- parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- self.inputDirSelector = ctk.ctkPathLineEdit()
- self.inputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
- self.inputDirSelector.settingKey = 'DICOMPatcherInputDir'
- parametersFormLayout.addRow("Input DICOM directory:", self.inputDirSelector)
-
- self.outputDirSelector = ctk.ctkPathLineEdit()
- self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
- self.outputDirSelector.settingKey = 'DICOMPatcherOutputDir'
- parametersFormLayout.addRow("Output DICOM directory:", self.outputDirSelector)
-
- self.normalizeFileNamesCheckBox = qt.QCheckBox()
- self.normalizeFileNamesCheckBox.checked = True
- self.normalizeFileNamesCheckBox.setToolTip("Replace file and folder names with automatically generated names."
- " Fixes errors caused by file path containins special characters or being too long.")
- parametersFormLayout.addRow("Normalize file names", self.normalizeFileNamesCheckBox)
-
- self.forceSamePatientNameIdInEachDirectoryCheckBox = qt.QCheckBox()
- self.forceSamePatientNameIdInEachDirectoryCheckBox.checked = False
- self.forceSamePatientNameIdInEachDirectoryCheckBox.setToolTip("Generate patient name and ID from the first file in a directory"
- " and force all other files in the same directory to have the same patient name and ID."
- " Enable this option if a separate patient directory is created for each patched file.")
- parametersFormLayout.addRow("Force same patient name and ID in each directory", self.forceSamePatientNameIdInEachDirectoryCheckBox)
-
- self.forceSameSeriesInstanceUidInEachDirectoryCheckBox = qt.QCheckBox()
- self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked = False
- self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.setToolTip("Generate a new series instance UID for each directory"
- " and set it in all files in that same directory."
- " Enable this option to force placing all frames in a folder into a single volume.")
- parametersFormLayout.addRow("Force same series instance UID in each directory", self.forceSameSeriesInstanceUidInEachDirectoryCheckBox)
-
- self.generateMissingIdsCheckBox = qt.QCheckBox()
- self.generateMissingIdsCheckBox.checked = True
- self.generateMissingIdsCheckBox.setToolTip("Generate missing patient, study, series IDs. It is assumed that"
- " all files in a directory belong to the same series. Fixes error caused by too aggressive anonymization"
- " or incorrect DICOM image converters.")
- parametersFormLayout.addRow("Generate missing patient/study/series IDs", self.generateMissingIdsCheckBox)
-
- self.generateImagePositionFromSliceThicknessCheckBox = qt.QCheckBox()
- self.generateImagePositionFromSliceThicknessCheckBox.checked = True
- self.generateImagePositionFromSliceThicknessCheckBox.setToolTip("Generate 'image position sequence' for"
- " multi-frame files that only have 'SliceThickness' field. Fixes error in Dolphin 3D CBCT scanners.")
- parametersFormLayout.addRow("Generate slice position for multi-frame volumes", self.generateImagePositionFromSliceThicknessCheckBox)
-
- self.anonymizeDicomCheckBox = qt.QCheckBox()
- self.anonymizeDicomCheckBox.checked = False
- self.anonymizeDicomCheckBox.setToolTip("If checked, then some patient identifiable information will be removed"
- " from the patched DICOM files. There are many fields that can identify a patient, this function does not remove all of them.")
- parametersFormLayout.addRow("Partially anonymize", self.anonymizeDicomCheckBox)
-
- #
- # Patch Button
- #
- self.patchButton = qt.QPushButton("Patch")
- self.patchButton.toolTip = "Fix DICOM files in input directory and write them to output directory"
- parametersFormLayout.addRow(self.patchButton)
-
- #
- # Import Button
- #
- self.importButton = qt.QPushButton("Import to DICOM database")
- self.importButton.toolTip = "Import DICOM files in output directory into the application's DICOM database"
- parametersFormLayout.addRow(self.importButton)
-
- # connections
- self.patchButton.connect('clicked(bool)', self.onPatchButton)
- self.importButton.connect('clicked(bool)', self.onImportButton)
-
- self.statusLabel = qt.QPlainTextEdit()
- self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
- parametersFormLayout.addRow(self.statusLabel)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- self.logic = DICOMPatcherLogic()
- self.logic.logCallback = self.addLog
-
- def cleanup(self):
- pass
-
- def onPatchButton(self):
- with slicer.util.tryWithErrorDisplay("Unexpected error.", waitCursor=True):
-
- import tempfile
- if not self.outputDirSelector.currentPath:
- self.outputDirSelector.currentPath = tempfile.mkdtemp(prefix="DICOMPatcher-", dir=slicer.app.temporaryPath)
-
- self.inputDirSelector.addCurrentPathToHistory()
- self.outputDirSelector.addCurrentPathToHistory()
- self.statusLabel.plainText = ''
-
- self.logic.clearRules()
- if self.forceSamePatientNameIdInEachDirectoryCheckBox.checked:
- self.logic.addRule("ForceSamePatientNameIdInEachDirectory")
- if self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked:
- self.logic.addRule("ForceSameSeriesInstanceUidInEachDirectory")
- if self.generateMissingIdsCheckBox.checked:
- self.logic.addRule("GenerateMissingIDs")
- self.logic.addRule("RemoveDICOMDIR")
- self.logic.addRule("FixPrivateMediaStorageSOPClassUID")
- if self.generateImagePositionFromSliceThicknessCheckBox.checked:
- self.logic.addRule("AddMissingSliceSpacingToMultiframe")
- if self.anonymizeDicomCheckBox.checked:
- self.logic.addRule("Anonymize")
- if self.normalizeFileNamesCheckBox.checked:
- self.logic.addRule("NormalizeFileNames")
- self.logic.patchDicomDir(self.inputDirSelector.currentPath, self.outputDirSelector.currentPath)
-
- def onImportButton(self):
- self.logic.importDicomDir(self.outputDirSelector.currentPath)
-
- def addLog(self, text):
- """Append text to log window
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.statusLabel.appendPlainText(text)
- slicer.app.processEvents() # force update
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Instantiate and connect widgets ...
+
+ #
+ # Parameters Area
+ #
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Parameters"
+ self.layout.addWidget(parametersCollapsibleButton)
+
+ # Layout within the dummy collapsible button
+ parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ self.inputDirSelector = ctk.ctkPathLineEdit()
+ self.inputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
+ self.inputDirSelector.settingKey = 'DICOMPatcherInputDir'
+ parametersFormLayout.addRow("Input DICOM directory:", self.inputDirSelector)
+
+ self.outputDirSelector = ctk.ctkPathLineEdit()
+ self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
+ self.outputDirSelector.settingKey = 'DICOMPatcherOutputDir'
+ parametersFormLayout.addRow("Output DICOM directory:", self.outputDirSelector)
+
+ self.normalizeFileNamesCheckBox = qt.QCheckBox()
+ self.normalizeFileNamesCheckBox.checked = True
+ self.normalizeFileNamesCheckBox.setToolTip("Replace file and folder names with automatically generated names."
+ " Fixes errors caused by file path containins special characters or being too long.")
+ parametersFormLayout.addRow("Normalize file names", self.normalizeFileNamesCheckBox)
+
+ self.forceSamePatientNameIdInEachDirectoryCheckBox = qt.QCheckBox()
+ self.forceSamePatientNameIdInEachDirectoryCheckBox.checked = False
+ self.forceSamePatientNameIdInEachDirectoryCheckBox.setToolTip("Generate patient name and ID from the first file in a directory"
+ " and force all other files in the same directory to have the same patient name and ID."
+ " Enable this option if a separate patient directory is created for each patched file.")
+ parametersFormLayout.addRow("Force same patient name and ID in each directory", self.forceSamePatientNameIdInEachDirectoryCheckBox)
+
+ self.forceSameSeriesInstanceUidInEachDirectoryCheckBox = qt.QCheckBox()
+ self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked = False
+ self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.setToolTip("Generate a new series instance UID for each directory"
+ " and set it in all files in that same directory."
+ " Enable this option to force placing all frames in a folder into a single volume.")
+ parametersFormLayout.addRow("Force same series instance UID in each directory", self.forceSameSeriesInstanceUidInEachDirectoryCheckBox)
+
+ self.generateMissingIdsCheckBox = qt.QCheckBox()
+ self.generateMissingIdsCheckBox.checked = True
+ self.generateMissingIdsCheckBox.setToolTip("Generate missing patient, study, series IDs. It is assumed that"
+ " all files in a directory belong to the same series. Fixes error caused by too aggressive anonymization"
+ " or incorrect DICOM image converters.")
+ parametersFormLayout.addRow("Generate missing patient/study/series IDs", self.generateMissingIdsCheckBox)
+
+ self.generateImagePositionFromSliceThicknessCheckBox = qt.QCheckBox()
+ self.generateImagePositionFromSliceThicknessCheckBox.checked = True
+ self.generateImagePositionFromSliceThicknessCheckBox.setToolTip("Generate 'image position sequence' for"
+ " multi-frame files that only have 'SliceThickness' field. Fixes error in Dolphin 3D CBCT scanners.")
+ parametersFormLayout.addRow("Generate slice position for multi-frame volumes", self.generateImagePositionFromSliceThicknessCheckBox)
+
+ self.anonymizeDicomCheckBox = qt.QCheckBox()
+ self.anonymizeDicomCheckBox.checked = False
+ self.anonymizeDicomCheckBox.setToolTip("If checked, then some patient identifiable information will be removed"
+ " from the patched DICOM files. There are many fields that can identify a patient, this function does not remove all of them.")
+ parametersFormLayout.addRow("Partially anonymize", self.anonymizeDicomCheckBox)
+
+ #
+ # Patch Button
+ #
+ self.patchButton = qt.QPushButton("Patch")
+ self.patchButton.toolTip = "Fix DICOM files in input directory and write them to output directory"
+ parametersFormLayout.addRow(self.patchButton)
+
+ #
+ # Import Button
+ #
+ self.importButton = qt.QPushButton("Import to DICOM database")
+ self.importButton.toolTip = "Import DICOM files in output directory into the application's DICOM database"
+ parametersFormLayout.addRow(self.importButton)
+
+ # connections
+ self.patchButton.connect('clicked(bool)', self.onPatchButton)
+ self.importButton.connect('clicked(bool)', self.onImportButton)
+
+ self.statusLabel = qt.QPlainTextEdit()
+ self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
+ parametersFormLayout.addRow(self.statusLabel)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ self.logic = DICOMPatcherLogic()
+ self.logic.logCallback = self.addLog
+
+ def cleanup(self):
+ pass
+
+ def onPatchButton(self):
+ with slicer.util.tryWithErrorDisplay("Unexpected error.", waitCursor=True):
+
+ import tempfile
+ if not self.outputDirSelector.currentPath:
+ self.outputDirSelector.currentPath = tempfile.mkdtemp(prefix="DICOMPatcher-", dir=slicer.app.temporaryPath)
+
+ self.inputDirSelector.addCurrentPathToHistory()
+ self.outputDirSelector.addCurrentPathToHistory()
+ self.statusLabel.plainText = ''
+
+ self.logic.clearRules()
+ if self.forceSamePatientNameIdInEachDirectoryCheckBox.checked:
+ self.logic.addRule("ForceSamePatientNameIdInEachDirectory")
+ if self.forceSameSeriesInstanceUidInEachDirectoryCheckBox.checked:
+ self.logic.addRule("ForceSameSeriesInstanceUidInEachDirectory")
+ if self.generateMissingIdsCheckBox.checked:
+ self.logic.addRule("GenerateMissingIDs")
+ self.logic.addRule("RemoveDICOMDIR")
+ self.logic.addRule("FixPrivateMediaStorageSOPClassUID")
+ if self.generateImagePositionFromSliceThicknessCheckBox.checked:
+ self.logic.addRule("AddMissingSliceSpacingToMultiframe")
+ if self.anonymizeDicomCheckBox.checked:
+ self.logic.addRule("Anonymize")
+ if self.normalizeFileNamesCheckBox.checked:
+ self.logic.addRule("NormalizeFileNames")
+ self.logic.patchDicomDir(self.inputDirSelector.currentPath, self.outputDirSelector.currentPath)
+
+ def onImportButton(self):
+ self.logic.importDicomDir(self.outputDirSelector.currentPath)
+
+ def addLog(self, text):
+ """Append text to log window
+ """
+ self.statusLabel.appendPlainText(text)
+ slicer.app.processEvents() # force update
#
@@ -175,28 +175,28 @@ def addLog(self, text):
#
class DICOMPatcherRule:
- def __init__(self):
- self.logCallback = None
+ def __init__(self):
+ self.logCallback = None
- def addLog(self, text):
- logging.info(text)
- if self.logCallback:
- self.logCallback(text)
+ def addLog(self, text):
+ logging.info(text)
+ if self.logCallback:
+ self.logCallback(text)
- def processStart(self, inputRootDir, outputRootDir):
- pass
+ def processStart(self, inputRootDir, outputRootDir):
+ pass
- def processDirectory(self, currentSubDir):
- pass
+ def processDirectory(self, currentSubDir):
+ pass
- def skipFile(self, filepath):
- return False
+ def skipFile(self, filepath):
+ return False
- def processDataSet(self, ds):
- pass
+ def processDataSet(self, ds):
+ pass
- def generateOutputFilePath(self, ds, filepath):
- return filepath
+ def generateOutputFilePath(self, ds, filepath):
+ return filepath
#
@@ -204,105 +204,105 @@ def generateOutputFilePath(self, ds, filepath):
#
class ForceSamePatientNameIdInEachDirectory(DICOMPatcherRule):
- def __init__(self):
- self.requiredTags = ['PatientName', 'PatientID']
- self.eachFileIsSeparateSeries = False
-
- def processStart(self, inputRootDir, outputRootDir):
- self.patientIndex = 0
-
- def processDirectory(self, currentSubDir):
- self.firstFileInDirectory = True
- self.patientIndex += 1
-
- def processDataSet(self, ds):
- import pydicom
- if self.firstFileInDirectory:
- # Get patient name and ID for this folder and save it
- self.firstFileInDirectory = False
- if ds.PatientName:
- self.patientName = ds.PatientName
- else:
- self.patientName = "Unspecified Patient " + str(self.patientIndex)
- if ds.PatientID:
- self.patientID = ds.PatientID
- else:
- self.patientID = pydicom.uid.generate_uid(None)
- # Set the same patient name and ID as the first file in the directory
- ds.PatientName = self.patientName
- ds.PatientID = self.patientID
+ def __init__(self):
+ self.requiredTags = ['PatientName', 'PatientID']
+ self.eachFileIsSeparateSeries = False
+
+ def processStart(self, inputRootDir, outputRootDir):
+ self.patientIndex = 0
+
+ def processDirectory(self, currentSubDir):
+ self.firstFileInDirectory = True
+ self.patientIndex += 1
+
+ def processDataSet(self, ds):
+ import pydicom
+ if self.firstFileInDirectory:
+ # Get patient name and ID for this folder and save it
+ self.firstFileInDirectory = False
+ if ds.PatientName:
+ self.patientName = ds.PatientName
+ else:
+ self.patientName = "Unspecified Patient " + str(self.patientIndex)
+ if ds.PatientID:
+ self.patientID = ds.PatientID
+ else:
+ self.patientID = pydicom.uid.generate_uid(None)
+ # Set the same patient name and ID as the first file in the directory
+ ds.PatientName = self.patientName
+ ds.PatientID = self.patientID
class ForceSameSeriesInstanceUidInEachDirectory(DICOMPatcherRule):
- def __init__(self):
- self.requiredTags = ['SeriesInstanceUID']
+ def __init__(self):
+ self.requiredTags = ['SeriesInstanceUID']
- def processStart(self, inputRootDir, outputRootDir):
- self.seriesIndex = 0
+ def processStart(self, inputRootDir, outputRootDir):
+ self.seriesIndex = 0
- def processDirectory(self, currentSubDir):
- self.firstFileInDirectory = True
- self.seriesIndex += 1
+ def processDirectory(self, currentSubDir):
+ self.firstFileInDirectory = True
+ self.seriesIndex += 1
- def processDataSet(self, ds):
- import pydicom
- if self.firstFileInDirectory:
- # Get seriesInstanceUID for this folder and save it
- self.firstFileInDirectory = False
- self.seriesInstanceUID = pydicom.uid.generate_uid(None)
- # Set the same patient name and ID as the first file in the directory
- ds.SeriesInstanceUID = self.seriesInstanceUID
+ def processDataSet(self, ds):
+ import pydicom
+ if self.firstFileInDirectory:
+ # Get seriesInstanceUID for this folder and save it
+ self.firstFileInDirectory = False
+ self.seriesInstanceUID = pydicom.uid.generate_uid(None)
+ # Set the same patient name and ID as the first file in the directory
+ ds.SeriesInstanceUID = self.seriesInstanceUID
class GenerateMissingIDs(DICOMPatcherRule):
- def __init__(self):
- self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber']
- self.eachFileIsSeparateSeries = False
-
- def processStart(self, inputRootDir, outputRootDir):
- import pydicom
- self.patientIDToRandomIDMap = {}
- self.studyUIDToRandomUIDMap = {}
- self.seriesUIDToRandomUIDMap = {}
- self.numberOfSeriesInStudyMap = {}
- # All files without a patient ID will be assigned to the same patient
- self.randomPatientID = pydicom.uid.generate_uid(None)
-
- def processDirectory(self, currentSubDir):
- import pydicom
- # Assume that all files in a directory belongs to the same study
- self.randomStudyUID = pydicom.uid.generate_uid(None)
- # Assume that all files in a directory belongs to the same series
- self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None)
-
- def processDataSet(self, ds):
- import pydicom
-
- for tag in self.requiredTags:
- if not hasattr(ds, tag):
- setattr(ds, tag, '')
-
- # Generate a new SOPInstanceUID to avoid different files having the same SOPInstanceUID
- ds.SOPInstanceUID = pydicom.uid.generate_uid(None)
-
- if ds.PatientName == '':
- ds.PatientName = "Unspecified Patient"
- if ds.PatientID == '':
- ds.PatientID = self.randomPatientID
- if ds.StudyInstanceUID == '':
- ds.StudyInstanceUID = self.randomStudyUID
- if ds.SeriesInstanceUID == '':
- if self.eachFileIsSeparateSeries:
- ds.SeriesInstanceUID = pydicom.uid.generate_uid(None)
- else:
- ds.SeriesInstanceUID = self.randomSeriesInstanceUID
-
- # Generate series number to make it easier to identify a sequence within a study
- if ds.SeriesNumber == '':
- if ds.StudyInstanceUID not in self.numberOfSeriesInStudyMap:
- self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = 0
- self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] + 1
- ds.SeriesNumber = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID]
+ def __init__(self):
+ self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber']
+ self.eachFileIsSeparateSeries = False
+
+ def processStart(self, inputRootDir, outputRootDir):
+ import pydicom
+ self.patientIDToRandomIDMap = {}
+ self.studyUIDToRandomUIDMap = {}
+ self.seriesUIDToRandomUIDMap = {}
+ self.numberOfSeriesInStudyMap = {}
+ # All files without a patient ID will be assigned to the same patient
+ self.randomPatientID = pydicom.uid.generate_uid(None)
+
+ def processDirectory(self, currentSubDir):
+ import pydicom
+ # Assume that all files in a directory belongs to the same study
+ self.randomStudyUID = pydicom.uid.generate_uid(None)
+ # Assume that all files in a directory belongs to the same series
+ self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None)
+
+ def processDataSet(self, ds):
+ import pydicom
+
+ for tag in self.requiredTags:
+ if not hasattr(ds, tag):
+ setattr(ds, tag, '')
+
+ # Generate a new SOPInstanceUID to avoid different files having the same SOPInstanceUID
+ ds.SOPInstanceUID = pydicom.uid.generate_uid(None)
+
+ if ds.PatientName == '':
+ ds.PatientName = "Unspecified Patient"
+ if ds.PatientID == '':
+ ds.PatientID = self.randomPatientID
+ if ds.StudyInstanceUID == '':
+ ds.StudyInstanceUID = self.randomStudyUID
+ if ds.SeriesInstanceUID == '':
+ if self.eachFileIsSeparateSeries:
+ ds.SeriesInstanceUID = pydicom.uid.generate_uid(None)
+ else:
+ ds.SeriesInstanceUID = self.randomSeriesInstanceUID
+
+ # Generate series number to make it easier to identify a sequence within a study
+ if ds.SeriesNumber == '':
+ if ds.StudyInstanceUID not in self.numberOfSeriesInStudyMap:
+ self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = 0
+ self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID] + 1
+ ds.SeriesNumber = self.numberOfSeriesInStudyMap[ds.StudyInstanceUID]
#
@@ -310,11 +310,11 @@ def processDataSet(self, ds):
#
class RemoveDICOMDIR(DICOMPatcherRule):
- def skipFile(self, filepath):
- if os.path.basename(filepath) != 'DICOMDIR':
- return False
- self.addLog('DICOMDIR file is ignored (its contents may be inconsistent with the contents of the indexed DICOM files, therefore it is safer not to use it)')
- return True
+ def skipFile(self, filepath):
+ if os.path.basename(filepath) != 'DICOMDIR':
+ return False
+ self.addLog('DICOMDIR file is ignored (its contents may be inconsistent with the contents of the indexed DICOM files, therefore it is safer not to use it)')
+ return True
#
@@ -322,19 +322,19 @@ def skipFile(self, filepath):
#
class FixPrivateMediaStorageSOPClassUID(DICOMPatcherRule):
- def processDataSet(self, ds):
- # DCMTK uses a specific UID for if storage SOP class UID is not specified.
- # GDCM refuses to load images with a private SOP class UID, so we change it to CT storage
- # (as that is the most commonly used imaging modality).
- # We could make things nicer by allowing the user to specify a modality.
- DCMTKPrivateMediaStorageSOPClassUID = "1.2.276.0.7230010.3.1.0.1"
- CTImageStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
- if not hasattr(ds.file_meta, 'MediaStorageSOPClassUID') or ds.file_meta.MediaStorageSOPClassUID == DCMTKPrivateMediaStorageSOPClassUID:
- self.addLog("DCMTK private MediaStorageSOPClassUID found. Replace it with CT media storage SOP class UID.")
- ds.file_meta.MediaStorageSOPClassUID = CTImageStorageSOPClassUID
+ def processDataSet(self, ds):
+ # DCMTK uses a specific UID for if storage SOP class UID is not specified.
+ # GDCM refuses to load images with a private SOP class UID, so we change it to CT storage
+ # (as that is the most commonly used imaging modality).
+ # We could make things nicer by allowing the user to specify a modality.
+ DCMTKPrivateMediaStorageSOPClassUID = "1.2.276.0.7230010.3.1.0.1"
+ CTImageStorageSOPClassUID = "1.2.840.10008.5.1.4.1.1.2"
+ if not hasattr(ds.file_meta, 'MediaStorageSOPClassUID') or ds.file_meta.MediaStorageSOPClassUID == DCMTKPrivateMediaStorageSOPClassUID:
+ self.addLog("DCMTK private MediaStorageSOPClassUID found. Replace it with CT media storage SOP class UID.")
+ ds.file_meta.MediaStorageSOPClassUID = CTImageStorageSOPClassUID
- if hasattr(ds, 'SOPClassUID') and ds.SOPClassUID == DCMTKPrivateMediaStorageSOPClassUID:
- ds.SOPClassUID = CTImageStorageSOPClassUID
+ if hasattr(ds, 'SOPClassUID') and ds.SOPClassUID == DCMTKPrivateMediaStorageSOPClassUID:
+ ds.SOPClassUID = CTImageStorageSOPClassUID
#
@@ -342,83 +342,83 @@ def processDataSet(self, ds):
#
class AddMissingSliceSpacingToMultiframe(DICOMPatcherRule):
- """Add missing slice spacing info to multiframe files"""
-
- def processDataSet(self, ds):
- import pydicom
-
- if not hasattr(ds, 'NumberOfFrames'):
- return
- numberOfFrames = ds.NumberOfFrames
- if numberOfFrames <= 1:
- return
-
- # Multi-frame sequence, we may need to add slice positions
-
- # Error in Dolphin 3D CBCT scanners, they store multiple frames but they keep using CTImageStorage as storage class
- if ds.SOPClassUID == '1.2.840.10008.5.1.4.1.1.2': # Computed Tomography Image IOD
- ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.2.1' # Enhanced CT Image IOD
-
- sliceStartPosition = ds.ImagePositionPatient if hasattr(ds, 'ImagePositionPatient') else [0, 0, 0]
- sliceAxes = ds.ImageOrientationPatient if hasattr(ds, 'ImageOrientationPatient') else [1, 0, 0, 0, 1, 0]
- x = sliceAxes[:3]
- y = sliceAxes[3:]
- z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]] # cross(x,y)
- sliceSpacing = ds.SliceThickness if hasattr(ds, 'SliceThickness') else 1.0
- pixelSpacing = ds.PixelSpacing if hasattr(ds, 'PixelSpacing') else [1.0, 1.0]
-
- if not (pydicom.tag.Tag(0x5200, 0x9229) in ds):
-
- # (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence
- # (0020,9116) SQ (Sequence with undefined length #=1) # u/l, 1 PlaneOrientationSequence
- # (0020,0037) DS [1.00000\0.00000\0.00000\0.00000\1.00000\0.00000] # 48, 6 ImageOrientationPatient
- # (0028,9110) SQ (Sequence with undefined length #=1) # u/l, 1 PixelMeasuresSequence
- # (0018,0050) DS [3.00000] # 8, 1 SliceThickness
- # (0028,0030) DS [0.597656\0.597656] # 18, 2 PixelSpacing
-
- planeOrientationDataSet = pydicom.dataset.Dataset()
- planeOrientationDataSet.ImageOrientationPatient = sliceAxes
- planeOrientationSequence = pydicom.sequence.Sequence()
- planeOrientationSequence.insert(pydicom.tag.Tag(0x0020, 0x9116), planeOrientationDataSet)
-
- pixelMeasuresDataSet = pydicom.dataset.Dataset()
- pixelMeasuresDataSet.SliceThickness = sliceSpacing
- pixelMeasuresDataSet.PixelSpacing = pixelSpacing
- pixelMeasuresSequence = pydicom.sequence.Sequence()
- pixelMeasuresSequence.insert(pydicom.tag.Tag(0x0028, 0x9110), pixelMeasuresDataSet)
-
- sharedFunctionalGroupsDataSet = pydicom.dataset.Dataset()
- sharedFunctionalGroupsDataSet.PlaneOrientationSequence = planeOrientationSequence
- sharedFunctionalGroupsDataSet.PixelMeasuresSequence = pixelMeasuresSequence
- sharedFunctionalGroupsSequence = pydicom.sequence.Sequence()
- sharedFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9229), sharedFunctionalGroupsDataSet)
- ds.SharedFunctionalGroupsSequence = sharedFunctionalGroupsSequence
-
- if not (pydicom.tag.Tag(0x5200, 0x9230) in ds):
-
- # (5200,9230) SQ (Sequence with undefined length #=54) # u/l, 1 PerFrameFunctionalGroupsSequence
- # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence
- # (0020,0032) DS [-94.7012\-312.701\-806.500] # 26, 3 ImagePositionPatient
- # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence
- # (0020,0032) DS [-94.7012\-312.701\-809.500] # 26, 3 ImagePositionPatient
- # ...
-
- perFrameFunctionalGroupsSequence = pydicom.sequence.Sequence()
-
- for frameIndex in range(numberOfFrames):
- planePositionDataSet = pydicom.dataset.Dataset()
- slicePosition = [
- sliceStartPosition[0] + frameIndex * z[0] * sliceSpacing,
- sliceStartPosition[1] + frameIndex * z[1] * sliceSpacing,
- sliceStartPosition[2] + frameIndex * z[2] * sliceSpacing]
- planePositionDataSet.ImagePositionPatient = slicePosition
- planePositionSequence = pydicom.sequence.Sequence()
- planePositionSequence.insert(pydicom.tag.Tag(0x0020, 0x9113), planePositionDataSet)
- perFrameFunctionalGroupsDataSet = pydicom.dataset.Dataset()
- perFrameFunctionalGroupsDataSet.PlanePositionSequence = planePositionSequence
- perFrameFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9230), perFrameFunctionalGroupsDataSet)
-
- ds.PerFrameFunctionalGroupsSequence = perFrameFunctionalGroupsSequence
+ """Add missing slice spacing info to multiframe files"""
+
+ def processDataSet(self, ds):
+ import pydicom
+
+ if not hasattr(ds, 'NumberOfFrames'):
+ return
+ numberOfFrames = ds.NumberOfFrames
+ if numberOfFrames <= 1:
+ return
+
+ # Multi-frame sequence, we may need to add slice positions
+
+ # Error in Dolphin 3D CBCT scanners, they store multiple frames but they keep using CTImageStorage as storage class
+ if ds.SOPClassUID == '1.2.840.10008.5.1.4.1.1.2': # Computed Tomography Image IOD
+ ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.2.1' # Enhanced CT Image IOD
+
+ sliceStartPosition = ds.ImagePositionPatient if hasattr(ds, 'ImagePositionPatient') else [0, 0, 0]
+ sliceAxes = ds.ImageOrientationPatient if hasattr(ds, 'ImageOrientationPatient') else [1, 0, 0, 0, 1, 0]
+ x = sliceAxes[:3]
+ y = sliceAxes[3:]
+ z = [x[1] * y[2] - x[2] * y[1], x[2] * y[0] - x[0] * y[2], x[0] * y[1] - x[1] * y[0]] # cross(x,y)
+ sliceSpacing = ds.SliceThickness if hasattr(ds, 'SliceThickness') else 1.0
+ pixelSpacing = ds.PixelSpacing if hasattr(ds, 'PixelSpacing') else [1.0, 1.0]
+
+ if not (pydicom.tag.Tag(0x5200, 0x9229) in ds):
+
+ # (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence
+ # (0020,9116) SQ (Sequence with undefined length #=1) # u/l, 1 PlaneOrientationSequence
+ # (0020,0037) DS [1.00000\0.00000\0.00000\0.00000\1.00000\0.00000] # 48, 6 ImageOrientationPatient
+ # (0028,9110) SQ (Sequence with undefined length #=1) # u/l, 1 PixelMeasuresSequence
+ # (0018,0050) DS [3.00000] # 8, 1 SliceThickness
+ # (0028,0030) DS [0.597656\0.597656] # 18, 2 PixelSpacing
+
+ planeOrientationDataSet = pydicom.dataset.Dataset()
+ planeOrientationDataSet.ImageOrientationPatient = sliceAxes
+ planeOrientationSequence = pydicom.sequence.Sequence()
+ planeOrientationSequence.insert(pydicom.tag.Tag(0x0020, 0x9116), planeOrientationDataSet)
+
+ pixelMeasuresDataSet = pydicom.dataset.Dataset()
+ pixelMeasuresDataSet.SliceThickness = sliceSpacing
+ pixelMeasuresDataSet.PixelSpacing = pixelSpacing
+ pixelMeasuresSequence = pydicom.sequence.Sequence()
+ pixelMeasuresSequence.insert(pydicom.tag.Tag(0x0028, 0x9110), pixelMeasuresDataSet)
+
+ sharedFunctionalGroupsDataSet = pydicom.dataset.Dataset()
+ sharedFunctionalGroupsDataSet.PlaneOrientationSequence = planeOrientationSequence
+ sharedFunctionalGroupsDataSet.PixelMeasuresSequence = pixelMeasuresSequence
+ sharedFunctionalGroupsSequence = pydicom.sequence.Sequence()
+ sharedFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9229), sharedFunctionalGroupsDataSet)
+ ds.SharedFunctionalGroupsSequence = sharedFunctionalGroupsSequence
+
+ if not (pydicom.tag.Tag(0x5200, 0x9230) in ds):
+
+ # (5200,9230) SQ (Sequence with undefined length #=54) # u/l, 1 PerFrameFunctionalGroupsSequence
+ # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence
+ # (0020,0032) DS [-94.7012\-312.701\-806.500] # 26, 3 ImagePositionPatient
+ # (0020,9113) SQ (Sequence with undefined length #=1) # u/l, 1 PlanePositionSequence
+ # (0020,0032) DS [-94.7012\-312.701\-809.500] # 26, 3 ImagePositionPatient
+ # ...
+
+ perFrameFunctionalGroupsSequence = pydicom.sequence.Sequence()
+
+ for frameIndex in range(numberOfFrames):
+ planePositionDataSet = pydicom.dataset.Dataset()
+ slicePosition = [
+ sliceStartPosition[0] + frameIndex * z[0] * sliceSpacing,
+ sliceStartPosition[1] + frameIndex * z[1] * sliceSpacing,
+ sliceStartPosition[2] + frameIndex * z[2] * sliceSpacing]
+ planePositionDataSet.ImagePositionPatient = slicePosition
+ planePositionSequence = pydicom.sequence.Sequence()
+ planePositionSequence.insert(pydicom.tag.Tag(0x0020, 0x9113), planePositionDataSet)
+ perFrameFunctionalGroupsDataSet = pydicom.dataset.Dataset()
+ perFrameFunctionalGroupsDataSet.PlanePositionSequence = planePositionSequence
+ perFrameFunctionalGroupsSequence.insert(pydicom.tag.Tag(0x5200, 0x9230), perFrameFunctionalGroupsDataSet)
+
+ ds.PerFrameFunctionalGroupsSequence = perFrameFunctionalGroupsSequence
#
@@ -426,49 +426,49 @@ def processDataSet(self, ds):
#
class Anonymize(DICOMPatcherRule):
- def __init__(self):
- self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber']
-
- def processStart(self, inputRootDir, outputRootDir):
- import pydicom
- self.patientIDToRandomIDMap = {}
- self.studyUIDToRandomUIDMap = {}
- self.seriesUIDToRandomUIDMap = {}
- self.numberOfSeriesInStudyMap = {}
- # All files without a patient ID will be assigned to the same patient
- self.randomPatientID = pydicom.uid.generate_uid(None)
-
- def processDirectory(self, currentSubDir):
- import pydicom
- # Assume that all files in a directory belongs to the same study
- self.randomStudyUID = pydicom.uid.generate_uid(None)
- # Assume that all files in a directory belongs to the same series
- self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None)
-
- def processDataSet(self, ds):
- import pydicom
-
- ds.StudyDate = ''
- ds.StudyTime = ''
- ds.ContentDate = ''
- ds.ContentTime = ''
- ds.AccessionNumber = ''
- ds.ReferringPhysiciansName = ''
- ds.PatientsBirthDate = ''
- ds.PatientsSex = ''
- ds.StudyID = ''
- ds.PatientName = "Unspecified Patient"
-
- # replace ids with random values - re-use if we have seen them before
- if ds.PatientID not in self.patientIDToRandomIDMap:
- self.patientIDToRandomIDMap[ds.PatientID] = pydicom.uid.generate_uid(None)
- ds.PatientID = self.patientIDToRandomIDMap[ds.PatientID]
- if ds.StudyInstanceUID not in self.studyUIDToRandomUIDMap:
- self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] = pydicom.uid.generate_uid(None)
- ds.StudyInstanceUID = self.studyUIDToRandomUIDMap[ds.StudyInstanceUID]
- if ds.SeriesInstanceUID not in self.seriesUIDToRandomUIDMap:
- self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] = pydicom.uid.generate_uid(None)
- ds.SeriesInstanceUID = self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID]
+ def __init__(self):
+ self.requiredTags = ['PatientName', 'PatientID', 'StudyInstanceUID', 'SeriesInstanceUID', 'SeriesNumber']
+
+ def processStart(self, inputRootDir, outputRootDir):
+ import pydicom
+ self.patientIDToRandomIDMap = {}
+ self.studyUIDToRandomUIDMap = {}
+ self.seriesUIDToRandomUIDMap = {}
+ self.numberOfSeriesInStudyMap = {}
+ # All files without a patient ID will be assigned to the same patient
+ self.randomPatientID = pydicom.uid.generate_uid(None)
+
+ def processDirectory(self, currentSubDir):
+ import pydicom
+ # Assume that all files in a directory belongs to the same study
+ self.randomStudyUID = pydicom.uid.generate_uid(None)
+ # Assume that all files in a directory belongs to the same series
+ self.randomSeriesInstanceUID = pydicom.uid.generate_uid(None)
+
+ def processDataSet(self, ds):
+ import pydicom
+
+ ds.StudyDate = ''
+ ds.StudyTime = ''
+ ds.ContentDate = ''
+ ds.ContentTime = ''
+ ds.AccessionNumber = ''
+ ds.ReferringPhysiciansName = ''
+ ds.PatientsBirthDate = ''
+ ds.PatientsSex = ''
+ ds.StudyID = ''
+ ds.PatientName = "Unspecified Patient"
+
+ # replace ids with random values - re-use if we have seen them before
+ if ds.PatientID not in self.patientIDToRandomIDMap:
+ self.patientIDToRandomIDMap[ds.PatientID] = pydicom.uid.generate_uid(None)
+ ds.PatientID = self.patientIDToRandomIDMap[ds.PatientID]
+ if ds.StudyInstanceUID not in self.studyUIDToRandomUIDMap:
+ self.studyUIDToRandomUIDMap[ds.StudyInstanceUID] = pydicom.uid.generate_uid(None)
+ ds.StudyInstanceUID = self.studyUIDToRandomUIDMap[ds.StudyInstanceUID]
+ if ds.SeriesInstanceUID not in self.seriesUIDToRandomUIDMap:
+ self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID] = pydicom.uid.generate_uid(None)
+ ds.SeriesInstanceUID = self.seriesUIDToRandomUIDMap[ds.SeriesInstanceUID]
#
@@ -476,35 +476,35 @@ def processDataSet(self, ds):
#
class NormalizeFileNames(DICOMPatcherRule):
- def processStart(self, inputRootDir, outputRootDir):
- self.inputRootDir = inputRootDir
- self.outputRootDir = outputRootDir
- self.patientNameIDToFolderMap = {}
- self.studyUIDToFolderMap = {}
- self.seriesUIDToFolderMap = {}
- # Number of files or folder in the specified folder
- self.numberOfItemsInFolderMap = {}
-
- def getNextItemName(self, prefix, root):
- numberOfFilesInFolder = self.numberOfItemsInFolderMap[root] if root in self.numberOfItemsInFolderMap else 0
- self.numberOfItemsInFolderMap[root] = numberOfFilesInFolder + 1
- return f"{prefix}{numberOfFilesInFolder:03d}"
-
- def generateOutputFilePath(self, ds, filepath):
- folderName = ""
- patientNameID = str(ds.PatientName) + "*" + ds.PatientID
- if patientNameID not in self.patientNameIDToFolderMap:
- self.patientNameIDToFolderMap[patientNameID] = self.getNextItemName("pa", folderName)
- folderName += self.patientNameIDToFolderMap[patientNameID]
- if ds.StudyInstanceUID not in self.studyUIDToFolderMap:
- self.studyUIDToFolderMap[ds.StudyInstanceUID] = self.getNextItemName("st", folderName)
- folderName += "/" + self.studyUIDToFolderMap[ds.StudyInstanceUID]
- if ds.SeriesInstanceUID not in self.seriesUIDToFolderMap:
- self.seriesUIDToFolderMap[ds.SeriesInstanceUID] = self.getNextItemName("se", folderName)
- folderName += "/" + self.seriesUIDToFolderMap[ds.SeriesInstanceUID]
- prefix = ds.Modality.lower() if hasattr(ds, 'Modality') else ""
- filePath = self.outputRootDir + "/" + folderName + "/" + self.getNextItemName(prefix, folderName) + ".dcm"
- return filePath
+ def processStart(self, inputRootDir, outputRootDir):
+ self.inputRootDir = inputRootDir
+ self.outputRootDir = outputRootDir
+ self.patientNameIDToFolderMap = {}
+ self.studyUIDToFolderMap = {}
+ self.seriesUIDToFolderMap = {}
+ # Number of files or folder in the specified folder
+ self.numberOfItemsInFolderMap = {}
+
+ def getNextItemName(self, prefix, root):
+ numberOfFilesInFolder = self.numberOfItemsInFolderMap[root] if root in self.numberOfItemsInFolderMap else 0
+ self.numberOfItemsInFolderMap[root] = numberOfFilesInFolder + 1
+ return f"{prefix}{numberOfFilesInFolder:03d}"
+
+ def generateOutputFilePath(self, ds, filepath):
+ folderName = ""
+ patientNameID = str(ds.PatientName) + "*" + ds.PatientID
+ if patientNameID not in self.patientNameIDToFolderMap:
+ self.patientNameIDToFolderMap[patientNameID] = self.getNextItemName("pa", folderName)
+ folderName += self.patientNameIDToFolderMap[patientNameID]
+ if ds.StudyInstanceUID not in self.studyUIDToFolderMap:
+ self.studyUIDToFolderMap[ds.StudyInstanceUID] = self.getNextItemName("st", folderName)
+ folderName += "/" + self.studyUIDToFolderMap[ds.StudyInstanceUID]
+ if ds.SeriesInstanceUID not in self.seriesUIDToFolderMap:
+ self.seriesUIDToFolderMap[ds.SeriesInstanceUID] = self.getNextItemName("se", folderName)
+ folderName += "/" + self.seriesUIDToFolderMap[ds.SeriesInstanceUID]
+ prefix = ds.Modality.lower() if hasattr(ds, 'Modality') else ""
+ filePath = self.outputRootDir + "/" + folderName + "/" + self.getNextItemName(prefix, folderName) + ".dcm"
+ return filePath
#
@@ -512,119 +512,119 @@ def generateOutputFilePath(self, ds, filepath):
#
class DICOMPatcherLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self):
- ScriptedLoadableModuleLogic.__init__(self)
- self.logCallback = None
- self.patchingRules = []
-
- def clearRules(self):
- self.patchingRules = []
-
- def addRule(self, ruleName):
- import importlib
- ruleModule = importlib.import_module("DICOMPatcher")
- ruleClass = getattr(ruleModule, ruleName)
- ruleInstance = ruleClass()
- self.patchingRules.append(ruleInstance)
-
- def addLog(self, text):
- logging.info(text)
- if self.logCallback:
- self.logCallback(text)
-
- def patchDicomDir(self, inputDirPath, outputDirPath):
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- Since CTK (rightly) requires certain basic information [1] before it can import
- data files that purport to be dicom, this code patches the files in a directory
- with some needed fields.
- Calling this function with a directory path will make a patched copy of each file.
- Importing the old files to CTK should still fail, but the new ones should work.
+ def __init__(self):
+ ScriptedLoadableModuleLogic.__init__(self)
+ self.logCallback = None
+ self.patchingRules = []
- The directory is assumed to have a set of instances that are all from the
- same study of the same patient. Also that each instance (file) is an
- independent (multiframe) series.
+ def clearRules(self):
+ self.patchingRules = []
- [1] https://github.com/commontk/CTK/blob/16aa09540dcb59c6eafde4d9a88dfee1f0948edc/Libs/DICOM/Core/ctkDICOMDatabase.cpp#L1283-L1287
- """
+ def addRule(self, ruleName):
+ import importlib
+ ruleModule = importlib.import_module("DICOMPatcher")
+ ruleClass = getattr(ruleModule, ruleName)
+ ruleInstance = ruleClass()
+ self.patchingRules.append(ruleInstance)
- import pydicom
+ def addLog(self, text):
+ logging.info(text)
+ if self.logCallback:
+ self.logCallback(text)
- self.addLog('DICOM patching started...')
- logging.debug('DICOM patch input directory: ' + inputDirPath)
- logging.debug('DICOM patch output directory: ' + outputDirPath)
+ def patchDicomDir(self, inputDirPath, outputDirPath):
+ """
+ Since CTK (rightly) requires certain basic information [1] before it can import
+ data files that purport to be dicom, this code patches the files in a directory
+ with some needed fields.
- for rule in self.patchingRules:
- rule.logCallback = self.addLog
- rule.processStart(inputDirPath, outputDirPath)
+ Calling this function with a directory path will make a patched copy of each file.
+ Importing the old files to CTK should still fail, but the new ones should work.
- for root, subFolders, files in os.walk(inputDirPath):
+ The directory is assumed to have a set of instances that are all from the
+ same study of the same patient. Also that each instance (file) is an
+ independent (multiframe) series.
- currentSubDir = os.path.relpath(root, inputDirPath)
- rootOutput = os.path.join(outputDirPath, currentSubDir)
+ [1] https://github.com/commontk/CTK/blob/16aa09540dcb59c6eafde4d9a88dfee1f0948edc/Libs/DICOM/Core/ctkDICOMDatabase.cpp#L1283-L1287
+ """
- # Notify rules that processing of a new subdirectory started
- for rule in self.patchingRules:
- rule.processDirectory(currentSubDir)
+ import pydicom
- for file in files:
- filePath = os.path.join(root, file)
- self.addLog('Examining %s...' % os.path.join(currentSubDir, file))
+ self.addLog('DICOM patching started...')
+ logging.debug('DICOM patch input directory: ' + inputDirPath)
+ logging.debug('DICOM patch output directory: ' + outputDirPath)
- skipFileRequestingRule = None
for rule in self.patchingRules:
- if rule.skipFile(currentSubDir):
- skipFileRequestingRule = rule
- break
- if skipFileRequestingRule:
- self.addLog(' Rule ' + rule.__class__.__name__ + ' requested to skip this file.')
- continue
+ rule.logCallback = self.addLog
+ rule.processStart(inputDirPath, outputDirPath)
- try:
- ds = pydicom.read_file(filePath)
- except (OSError, pydicom.filereader.InvalidDicomError):
- self.addLog(' Not DICOM file. Skipped.')
- continue
+ for root, subFolders, files in os.walk(inputDirPath):
- self.addLog(' Patching...')
+ currentSubDir = os.path.relpath(root, inputDirPath)
+ rootOutput = os.path.join(outputDirPath, currentSubDir)
- for rule in self.patchingRules:
- rule.processDataSet(ds)
+ # Notify rules that processing of a new subdirectory started
+ for rule in self.patchingRules:
+ rule.processDirectory(currentSubDir)
- patchedFilePath = os.path.abspath(os.path.join(rootOutput, file))
- for rule in self.patchingRules:
- patchedFilePath = rule.generateOutputFilePath(ds, patchedFilePath)
+ for file in files:
+ filePath = os.path.join(root, file)
+ self.addLog('Examining %s...' % os.path.join(currentSubDir, file))
- ######################################################
- # Write
+ skipFileRequestingRule = None
+ for rule in self.patchingRules:
+ if rule.skipFile(currentSubDir):
+ skipFileRequestingRule = rule
+ break
+ if skipFileRequestingRule:
+ self.addLog(' Rule ' + rule.__class__.__name__ + ' requested to skip this file.')
+ continue
- dirName = os.path.dirname(patchedFilePath)
- if not os.path.exists(dirName):
- os.makedirs(dirName)
+ try:
+ ds = pydicom.read_file(filePath)
+ except (OSError, pydicom.filereader.InvalidDicomError):
+ self.addLog(' Not DICOM file. Skipped.')
+ continue
- self.addLog(' Writing DICOM...')
- pydicom.write_file(patchedFilePath, ds)
- self.addLog(' Created DICOM file: %s' % patchedFilePath)
+ self.addLog(' Patching...')
- self.addLog(f'DICOM patching completed. Patched files are written to:\n{outputDirPath}')
+ for rule in self.patchingRules:
+ rule.processDataSet(ds)
- def importDicomDir(self, outputDirPath):
- """
- Utility function to import DICOM files from a directory
- """
- self.addLog('Initiate DICOM importing from folder ' + outputDirPath)
- slicer.util.selectModule('DICOM')
- dicomBrowser = slicer.modules.dicom.widgetRepresentation().self().browserWidget.dicomBrowser
- dicomBrowser.importDirectory(outputDirPath)
+ patchedFilePath = os.path.abspath(os.path.join(rootOutput, file))
+ for rule in self.patchingRules:
+ patchedFilePath = rule.generateOutputFilePath(ds, patchedFilePath)
+
+ ######################################################
+ # Write
+
+ dirName = os.path.dirname(patchedFilePath)
+ if not os.path.exists(dirName):
+ os.makedirs(dirName)
+
+ self.addLog(' Writing DICOM...')
+ pydicom.write_file(patchedFilePath, ds)
+ self.addLog(' Created DICOM file: %s' % patchedFilePath)
+
+ self.addLog(f'DICOM patching completed. Patched files are written to:\n{outputDirPath}')
+
+ def importDicomDir(self, outputDirPath):
+ """
+ Utility function to import DICOM files from a directory
+ """
+ self.addLog('Initiate DICOM importing from folder ' + outputDirPath)
+ slicer.util.selectModule('DICOM')
+ dicomBrowser = slicer.modules.dicom.widgetRepresentation().self().browserWidget.dicomBrowser
+ dicomBrowser.importDirectory(outputDirPath)
#
@@ -632,92 +632,92 @@ def importDicomDir(self, outputDirPath):
#
class DICOMPatcherTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Run as few or as many tests as needed here.
"""
- self.setUp()
- self.test_DICOMPatcher1()
-
- def test_DICOMPatcher1(self):
- """ Ideally you should have several levels of tests. At the lowest level
- tests should exercise the functionality of the logic with different inputs
- (both valid and invalid). At higher levels your tests should emulate the
- way the user would interact with your code and confirm that it still works
- the way you intended.
- One of the most important features of the tests is that it should alert other
- developers when their changes will have an impact on the behavior of your
- module. For example, if a developer removes a feature that you depend on,
- your test should break so they know that the feature is needed.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- import tempfile
- testDir = tempfile.mkdtemp(prefix="DICOMPatcherTest-", dir=slicer.app.temporaryPath)
- self.assertTrue(os.path.isdir(testDir))
-
- inputTestDir = testDir + "/input"
- os.makedirs(inputTestDir)
- outputTestDir = testDir + "/output"
- self.delayDisplay('Created test directory: ' + testDir)
-
- self.delayDisplay("Generate test files")
-
- testFileNonDICOM = open(inputTestDir + "/NonDICOMFile.txt", "w")
- testFileNonDICOM.write("This is not a DICOM file")
- testFileNonDICOM.close()
-
- testFileDICOMFilename = inputTestDir + "/DICOMFile.dcm"
- self.delayDisplay('Writing test file: ' + testFileDICOMFilename)
- import pydicom
- file_meta = pydicom.dataset.Dataset()
- file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage
- file_meta.MediaStorageSOPInstanceUID = "1.2.3" # !! Need valid UID here for real work
- file_meta.ImplementationClassUID = "1.2.3.4" # !!! Need valid UIDs here
- ds = pydicom.dataset.FileDataset(testFileDICOMFilename, {}, file_meta=file_meta, preamble=b"\0" * 128)
- ds.PatientName = "Test^Firstname"
- ds.PatientID = "123456"
- # Set the transfer syntax
- ds.is_little_endian = True
- ds.is_implicit_VR = True
- ds.save_as(testFileDICOMFilename)
-
- self.delayDisplay("Patch input files")
-
- logic = DICOMPatcherLogic()
- logic.addRule("GenerateMissingIDs")
- logic.addRule("RemoveDICOMDIR")
- logic.addRule("FixPrivateMediaStorageSOPClassUID")
- logic.addRule("AddMissingSliceSpacingToMultiframe")
- logic.addRule("Anonymize")
- logic.addRule("NormalizeFileNames")
- logic.patchDicomDir(inputTestDir, outputTestDir)
-
- self.delayDisplay("Verify generated files")
-
- expectedWalk = []
- expectedWalk.append([['pa000'], []])
- expectedWalk.append([['st000'], []])
- expectedWalk.append([['se000'], []])
- expectedWalk.append([[], ['000.dcm']])
- step = 0
- for root, subFolders, files in os.walk(outputTestDir):
- self.assertEqual(subFolders, expectedWalk[step][0])
- self.assertEqual(files, expectedWalk[step][1])
- step += 1
-
- # TODO: test rule ForceSamePatientNameIdInEachDirectory
-
- self.delayDisplay("Clean up")
-
- import shutil
- shutil.rmtree(testDir)
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_DICOMPatcher1()
+
+ def test_DICOMPatcher1(self):
+ """ Ideally you should have several levels of tests. At the lowest level
+ tests should exercise the functionality of the logic with different inputs
+ (both valid and invalid). At higher levels your tests should emulate the
+ way the user would interact with your code and confirm that it still works
+ the way you intended.
+ One of the most important features of the tests is that it should alert other
+ developers when their changes will have an impact on the behavior of your
+ module. For example, if a developer removes a feature that you depend on,
+ your test should break so they know that the feature is needed.
+ """
+
+ import tempfile
+ testDir = tempfile.mkdtemp(prefix="DICOMPatcherTest-", dir=slicer.app.temporaryPath)
+ self.assertTrue(os.path.isdir(testDir))
+
+ inputTestDir = testDir + "/input"
+ os.makedirs(inputTestDir)
+ outputTestDir = testDir + "/output"
+ self.delayDisplay('Created test directory: ' + testDir)
+
+ self.delayDisplay("Generate test files")
+
+ testFileNonDICOM = open(inputTestDir + "/NonDICOMFile.txt", "w")
+ testFileNonDICOM.write("This is not a DICOM file")
+ testFileNonDICOM.close()
+
+ testFileDICOMFilename = inputTestDir + "/DICOMFile.dcm"
+ self.delayDisplay('Writing test file: ' + testFileDICOMFilename)
+ import pydicom
+ file_meta = pydicom.dataset.Dataset()
+ file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2' # CT Image Storage
+ file_meta.MediaStorageSOPInstanceUID = "1.2.3" # !! Need valid UID here for real work
+ file_meta.ImplementationClassUID = "1.2.3.4" # !!! Need valid UIDs here
+ ds = pydicom.dataset.FileDataset(testFileDICOMFilename, {}, file_meta=file_meta, preamble=b"\0" * 128)
+ ds.PatientName = "Test^Firstname"
+ ds.PatientID = "123456"
+ # Set the transfer syntax
+ ds.is_little_endian = True
+ ds.is_implicit_VR = True
+ ds.save_as(testFileDICOMFilename)
+
+ self.delayDisplay("Patch input files")
+
+ logic = DICOMPatcherLogic()
+ logic.addRule("GenerateMissingIDs")
+ logic.addRule("RemoveDICOMDIR")
+ logic.addRule("FixPrivateMediaStorageSOPClassUID")
+ logic.addRule("AddMissingSliceSpacingToMultiframe")
+ logic.addRule("Anonymize")
+ logic.addRule("NormalizeFileNames")
+ logic.patchDicomDir(inputTestDir, outputTestDir)
+
+ self.delayDisplay("Verify generated files")
+
+ expectedWalk = []
+ expectedWalk.append([['pa000'], []])
+ expectedWalk.append([['st000'], []])
+ expectedWalk.append([['se000'], []])
+ expectedWalk.append([[], ['000.dcm']])
+ step = 0
+ for root, subFolders, files in os.walk(outputTestDir):
+ self.assertEqual(subFolders, expectedWalk[step][0])
+ self.assertEqual(files, expectedWalk[step][1])
+ step += 1
+
+ # TODO: test rule ForceSamePatientNameIdInEachDirectory
+
+ self.delayDisplay("Clean up")
+
+ import shutil
+ shutil.rmtree(testDir)
diff --git a/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py
index 631368f66f3..2dd263cf0a2 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMEnhancedUSVolumePlugin.py
@@ -13,171 +13,171 @@
#
class DICOMEnhancedUSVolumePluginClass(DICOMPlugin):
- """ 3D ultrasound loader plugin.
- Limitation: ultrasound calibrated regions are not supported (each calibrated region
- would need to be split out to its own volume sequence).
- """
-
- def __init__(self):
- super().__init__()
- self.loadType = "Enhanced US volume"
-
- self.tags['sopClassUID'] = "0008,0016"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['seriesDescription'] = "0008,103E"
- self.tags['instanceNumber'] = "0020,0013"
- self.tags['modality'] = "0008,0060"
- self.tags['photometricInterpretation'] = "0028,0004"
-
- self.detailedLogging = False
-
- def examine(self, fileLists):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- fileLists parameter.
+ """ 3D ultrasound loader plugin.
+ Limitation: ultrasound calibrated regions are not supported (each calibrated region
+ would need to be split out to its own volume sequence).
"""
- loadables = []
- for files in fileLists:
- loadables += self.examineFiles(files)
- return loadables
-
- def examineFiles(self, files):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- files parameter.
- """
-
- self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
-
- supportedSOPClassUIDs = [
- '1.2.840.10008.5.1.4.1.1.6.2', # Enhanced US Volume Storage
- ]
-
- # The only sample data set that we received from GE LOGIQE10 (software version R1.5.1).
- # It added all volumes into a single series, even though they were acquired minutes apart.
- # Therefore, instead of loading the volumes into a sequence, we load each as a separate volume.
-
- loadables = []
-
- for filePath in files:
- # Quick check of SOP class UID without parsing the file...
- sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
- if not (sopClassUID in supportedSOPClassUIDs):
- # Unsupported class
- continue
-
- instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber'])
- modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality'])
- seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber'])
- seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription'])
- photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation'])
- name = ''
- if seriesNumber:
- name = f'{seriesNumber}:'
- if modality:
- name = f'{name} {modality}'
- if seriesDescription:
- name = f'{name} {seriesDescription}'
- else:
- name = f'{name} volume'
- if instanceNumber:
- name = f'{name} [{instanceNumber}]'
-
- loadable = DICOMLoadable()
- loadable.singleSequence = False # put each instance in a separate sequence
- loadable.files = [filePath]
- loadable.name = name.strip() # remove leading and trailing spaces, if any
- loadable.warning = "Loading of this image type is experimental. Please verify image geometry and report any problem is found."
- loadable.tooltip = f"Ultrasound volume"
- loadable.selected = True
- # Confidence is slightly larger than default scalar volume plugin's (0.5)
- # and DICOMVolumeSequencePlugin (0.7)
- # but still leaving room for more specialized plugins.
- loadable.confidence = 0.8
- loadable.grayscale = ('MONOCHROME' in photometricInterpretation)
- loadables.append(loadable)
-
- return loadables
-
- def load(self, loadable):
- """Load the selection
- """
-
- if loadable.grayscale:
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", loadable.name)
- else:
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", loadable.name)
-
- import vtkITK
- if loadable.grayscale:
- reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
- else:
- reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
- filePath = loadable.files[0]
- reader.SetArchetype(filePath)
- reader.AddFileName(filePath)
- reader.SetSingleFile(True)
- reader.SetOutputScalarTypeToNative()
- reader.SetDesiredCoordinateOrientationToNative()
- reader.SetUseNativeOriginOn()
- # GDCM is not particularly better in this than DCMTK, we just select one explicitly
- # so that we know which one is used
- reader.SetDICOMImageIOApproachToGDCM()
- reader.Update()
- if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
- errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
- raise ValueError(
- f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
-
- rasToIjk = reader.GetRasToIjkMatrix()
- ijkToRas = vtk.vtkMatrix4x4()
- vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas)
-
- imageData = reader.GetOutput()
- imageData.SetSpacing(1.0, 1.0, 1.0)
- imageData.SetOrigin(0.0, 0.0, 0.0)
- volumeNode.SetIJKToRASMatrix(ijkToRas)
- volumeNode.SetAndObserveImageData(imageData)
-
- # show volume
- appLogic = slicer.app.applicationLogic()
- selNode = appLogic.GetSelectionNode()
- selNode.SetActiveVolumeID(volumeNode.GetID())
- appLogic.PropagateVolumeSelection()
-
- return volumeNode
+ def __init__(self):
+ super().__init__()
+ self.loadType = "Enhanced US volume"
+
+ self.tags['sopClassUID'] = "0008,0016"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['seriesDescription'] = "0008,103E"
+ self.tags['instanceNumber'] = "0020,0013"
+ self.tags['modality'] = "0008,0060"
+ self.tags['photometricInterpretation'] = "0028,0004"
+
+ self.detailedLogging = False
+
+ def examine(self, fileLists):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ fileLists parameter.
+ """
+ loadables = []
+ for files in fileLists:
+ loadables += self.examineFiles(files)
+
+ return loadables
+
+ def examineFiles(self, files):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ files parameter.
+ """
+
+ self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
+
+ supportedSOPClassUIDs = [
+ '1.2.840.10008.5.1.4.1.1.6.2', # Enhanced US Volume Storage
+ ]
+
+ # The only sample data set that we received from GE LOGIQE10 (software version R1.5.1).
+ # It added all volumes into a single series, even though they were acquired minutes apart.
+ # Therefore, instead of loading the volumes into a sequence, we load each as a separate volume.
+
+ loadables = []
+
+ for filePath in files:
+ # Quick check of SOP class UID without parsing the file...
+ sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
+ if not (sopClassUID in supportedSOPClassUIDs):
+ # Unsupported class
+ continue
+
+ instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber'])
+ modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality'])
+ seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber'])
+ seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription'])
+ photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation'])
+ name = ''
+ if seriesNumber:
+ name = f'{seriesNumber}:'
+ if modality:
+ name = f'{name} {modality}'
+ if seriesDescription:
+ name = f'{name} {seriesDescription}'
+ else:
+ name = f'{name} volume'
+ if instanceNumber:
+ name = f'{name} [{instanceNumber}]'
+
+ loadable = DICOMLoadable()
+ loadable.singleSequence = False # put each instance in a separate sequence
+ loadable.files = [filePath]
+ loadable.name = name.strip() # remove leading and trailing spaces, if any
+ loadable.warning = "Loading of this image type is experimental. Please verify image geometry and report any problem is found."
+ loadable.tooltip = f"Ultrasound volume"
+ loadable.selected = True
+ # Confidence is slightly larger than default scalar volume plugin's (0.5)
+ # and DICOMVolumeSequencePlugin (0.7)
+ # but still leaving room for more specialized plugins.
+ loadable.confidence = 0.8
+ loadable.grayscale = ('MONOCHROME' in photometricInterpretation)
+ loadables.append(loadable)
+
+ return loadables
+
+ def load(self, loadable):
+ """Load the selection
+ """
+
+ if loadable.grayscale:
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", loadable.name)
+ else:
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", loadable.name)
+
+ import vtkITK
+ if loadable.grayscale:
+ reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
+ else:
+ reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
+ filePath = loadable.files[0]
+ reader.SetArchetype(filePath)
+ reader.AddFileName(filePath)
+ reader.SetSingleFile(True)
+ reader.SetOutputScalarTypeToNative()
+ reader.SetDesiredCoordinateOrientationToNative()
+ reader.SetUseNativeOriginOn()
+ # GDCM is not particularly better in this than DCMTK, we just select one explicitly
+ # so that we know which one is used
+ reader.SetDICOMImageIOApproachToGDCM()
+ reader.Update()
+ if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
+ errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
+ raise ValueError(
+ f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
+
+ rasToIjk = reader.GetRasToIjkMatrix()
+ ijkToRas = vtk.vtkMatrix4x4()
+ vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas)
+
+ imageData = reader.GetOutput()
+ imageData.SetSpacing(1.0, 1.0, 1.0)
+ imageData.SetOrigin(0.0, 0.0, 0.0)
+ volumeNode.SetIJKToRASMatrix(ijkToRas)
+ volumeNode.SetAndObserveImageData(imageData)
+
+ # show volume
+ appLogic = slicer.app.applicationLogic()
+ selNode = appLogic.GetSelectionNode()
+ selNode.SetActiveVolumeID(volumeNode.GetID())
+ appLogic.PropagateVolumeSelection()
+
+ return volumeNode
#
# DICOMEnhancedUSVolumePlugin
#
class DICOMEnhancedUSVolumePlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM Enhanced US volume Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Andras Lasso (PerkLab)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM Enhanced US volume Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Andras Lasso (PerkLab)"]
+ parent.helpText = """
Plugin to the DICOM Module to parse and load 3D enhanced US volumes.
No module interface here, only in the DICOM module.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
The file was originally developed by Andras Lasso (PerkLab).
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
-
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMEnhancedUSVolumePlugin'] = DICOMEnhancedUSVolumePluginClass
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
+
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMEnhancedUSVolumePlugin'] = DICOMEnhancedUSVolumePluginClass
diff --git a/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py b/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py
index c426ff03191..4f363842703 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMGeAbusPlugin.py
@@ -19,286 +19,286 @@
#
class DICOMGeAbusPluginClass(DICOMPlugin):
- """ Image loader plugin for GE Invenia
- ABUS (automated breast ultrasound) images.
- """
-
- def __init__(self):
- super().__init__()
- self.loadType = "GE ABUS"
-
- self.tags['sopClassUID'] = "0008,0016"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['seriesDescription'] = "0008,103E"
- self.tags['instanceNumber'] = "0020,0013"
- self.tags['manufacturerModelName'] = "0008,1090"
-
- # Accepted private creator identifications
- self.privateCreators = ["U-Systems", "General Electric Company 01"]
-
- def examine(self, fileLists):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- fileLists parameter.
+ """ Image loader plugin for GE Invenia
+ ABUS (automated breast ultrasound) images.
"""
- loadables = []
- for files in fileLists:
- loadables += self.examineFiles(files)
- return loadables
-
- def examineFiles(self, files):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- files parameter.
- """
-
- detailedLogging = self.isDetailedLogging()
-
- supportedSOPClassUIDs = [
- '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage
- ]
-
- loadables = []
-
- for filePath in files:
- # Quick check of SOP class UID without parsing the file...
- try:
- sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
- if not (sopClassUID in supportedSOPClassUIDs):
- # Unsupported class
- continue
-
- manufacturerModelName = slicer.dicomDatabase.fileValue(filePath, self.tags['manufacturerModelName'])
- if manufacturerModelName != "Invenia":
- if detailedLogging:
- logging.debug("ManufacturerModelName is not Invenia, the series will not be considered as an ABUS image")
- continue
-
- except Exception as e:
- # Quick check could not be completed (probably Slicer DICOM database is not initialized).
- # No problem, we'll try to parse the file and check the SOP class UID then.
- pass
-
- try:
- ds = dicom.read_file(filePath, stop_before_pixels=True)
- except Exception as e:
- logging.debug(f"Failed to parse DICOM file: {str(e)}")
- continue
-
- # check if probeCurvatureRadius is available
- probeCurvatureRadiusFound = False
- for privateCreator in self.privateCreators:
- if self.findPrivateTag(ds, 0x0021, 0x40, privateCreator):
- probeCurvatureRadiusFound = True
- break
-
- if not probeCurvatureRadiusFound:
- if detailedLogging:
- logging.debug("Probe curvature radius is not found, the series will not be considered as an ABUS image")
- continue
-
- name = ''
- if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber:
- name = f'{ds.SeriesNumber}:'
- if hasattr(ds, 'Modality') and ds.Modality:
- name = f'{name} {ds.Modality}'
- if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription:
- name = f'{name} {ds.SeriesDescription}'
- if hasattr(ds, 'InstanceNumber') and ds.InstanceNumber:
- name = f'{name} [{ds.InstanceNumber}]'
-
- loadable = DICOMLoadable()
- loadable.files = [filePath]
- loadable.name = name.strip() # remove leading and trailing spaces, if any
- loadable.tooltip = "GE Invenia ABUS"
- loadable.warning = "Loading of this image type is experimental. Please verify image size and orientation and report any problem is found."
- loadable.selected = True
- loadable.confidence = 0.9 # this has to be higher than 0.7 (ultrasound sequence)
-
- # Add to loadables list
- loadables.append(loadable)
-
- return loadables
-
- def getMetadata(self, filePath):
- try:
- ds = dicom.read_file(filePath, stop_before_pixels=True)
- except Exception as e:
- raise ValueError(f"Failed to parse DICOM file: {str(e)}")
-
- fieldsInfo = {
- 'NipplePosition': {'group': 0x0021, 'element': 0x20, 'private': True, 'required': False},
- 'FirstElementPosition': {'group': 0x0021, 'element': 0x21, 'private': True, 'required': False},
- 'CurvatureRadiusProbe': {'group': 0x0021, 'element': 0x40, 'private': True, 'required': True},
- 'CurvatureRadiusTrack': {'group': 0x0021, 'element': 0x41, 'private': True, 'required': True},
- 'LineDensity': {'group': 0x0021, 'element': 0x62, 'private': True, 'required': False},
- 'ScanDepthCm': {'group': 0x0021, 'element': 0x63, 'private': True, 'required': True},
- 'SpacingBetweenSlices': {'group': 0x0018, 'element': 0x0088, 'private': False, 'required': True},
- 'PixelSpacing': {'group': 0x0028, 'element': 0x0030, 'private': False, 'required': True},
- }
-
- fieldValues = {}
- for fieldName in fieldsInfo:
- fieldInfo = fieldsInfo[fieldName]
- if fieldInfo['private']:
- for privateCreator in self.privateCreators:
- tag = self.findPrivateTag(ds, fieldInfo['group'], fieldInfo['element'], privateCreator)
- if tag:
- break
- else:
- tag = dicom.tag.Tag(fieldInfo['group'], fieldInfo['element'])
- if tag:
- fieldValues[fieldName] = ds[tag].value
-
- # Make sure all mandatory fields are found
- for fieldName in fieldsInfo:
- fieldInfo = fieldsInfo[fieldName]
- if not fieldInfo['required']:
- continue
- if not fieldName in fieldValues:
- raise ValueError(f"Mandatory field {fieldName} was not found")
-
- return fieldValues
-
- def load(self, loadable):
- """Load the selection
- """
-
- filePath = loadable.files[0]
- metadata = self.getMetadata(filePath)
-
- import vtkITK
- reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
- reader.SetArchetype(filePath)
- reader.AddFileName(filePath)
- reader.SetSingleFile(True)
- reader.SetOutputScalarTypeToNative()
- reader.SetDesiredCoordinateOrientationToNative()
- reader.SetUseNativeOriginOn()
- # GDCM is not particularly better in this than DCMTK, we just select one explicitly
- # so that we know which one is used
- reader.SetDICOMImageIOApproachToGDCM()
- reader.Update()
- imageData = reader.GetOutput()
-
- if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
- errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
- raise ValueError(
- f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
-
- # Image origin and spacing is stored in IJK to RAS matrix
- imageData.SetSpacing(1.0, 1.0, 1.0)
- imageData.SetOrigin(0.0, 0.0, 0.0)
-
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", slicer.mrmlScene.GenerateUniqueName(loadable.name))
-
- # I axis: scanline index (lateralSpacing)
- # J axis: sound propagation (axialSpacing)
- # K axis: slice (sliceSpacing)
- lateralSpacing = metadata['PixelSpacing'][1]
- axialSpacing = metadata['PixelSpacing'][0]
- sliceSpacing = metadata['SpacingBetweenSlices']
-
- ijkToRas = vtk.vtkMatrix4x4()
- ijkToRas.SetElement(0, 0, -1)
- ijkToRas.SetElement(1, 1, -1) # so that J axis points toward posterior
- volumeNode.SetIJKToRASMatrix(ijkToRas)
- volumeNode.SetSpacing(lateralSpacing, axialSpacing, sliceSpacing)
- extent = imageData.GetExtent()
- volumeNode.SetOrigin((extent[1] - extent[0] + 1) * 0.5 * lateralSpacing, 0, -(extent[5] - extent[2] + 1) * 0.5 * sliceSpacing)
- volumeNode.SetAndObserveImageData(imageData)
-
- # Apply scan conversion transform
- acquisitionTransform = self.createAcquisitionTransform(volumeNode, metadata)
- volumeNode.SetAndObserveTransformNodeID(acquisitionTransform.GetID())
-
- # Create Subject hierarchy nodes for the loaded series
- self.addSeriesInSubjectHierarchy(loadable, volumeNode)
-
- # Place transform in the same subject hierarchy folder as the volume node
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode))
- shNode.SetItemParent(shNode.GetItemByDataNode(acquisitionTransform), volumeParentItemId)
-
- # Show in slice views
- selectionNode = slicer.app.applicationLogic().GetSelectionNode()
- selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID())
- slicer.app.applicationLogic().PropagateVolumeSelection(1)
-
- return volumeNode
-
- def createAcquisitionTransform(self, volumeNode, metadata):
-
- # Creates transform that applies scan conversion transform
- probeRadius = metadata['CurvatureRadiusProbe']
- trackRadius = metadata['CurvatureRadiusTrack']
- if trackRadius != 0.0:
- raise ValueError(f"Curvature Radius (Track) is {trackRadius}. Currently, only volume with zero radius can be imported.")
-
- # Create a sampling grid for the transform
- import numpy as np
- spacing = np.array(volumeNode.GetSpacing())
- averageSpacing = (spacing[0] + spacing[1] + spacing[2]) / 3.0
- voxelsPerTransformControlPoint = 20 # the transform is changing smoothly, so we don't need to add too many control points
- gridSpacingMm = averageSpacing * voxelsPerTransformControlPoint
- gridSpacingVoxel = np.floor(gridSpacingMm / spacing).astype(int)
- gridAxesIJK = []
- imageData = volumeNode.GetImageData()
- extent = imageData.GetExtent()
- for axis in range(3):
- gridAxesIJK.append(list(range(extent[axis * 2], extent[axis * 2 + 1] + gridSpacingVoxel[axis], gridSpacingVoxel[axis])))
- samplingPoints_shape = [len(gridAxesIJK[0]), len(gridAxesIJK[1]), len(gridAxesIJK[2]), 3]
-
- # create a grid transform with one vector at the corner of each slice
- # the transform is in the same space and orientation as the volume node
- import vtk
- gridImage = vtk.vtkImageData()
- gridImage.SetOrigin(*volumeNode.GetOrigin())
- gridImage.SetDimensions(samplingPoints_shape[:3])
- gridImage.SetSpacing(gridSpacingVoxel[0] * spacing[0], gridSpacingVoxel[1] * spacing[1], gridSpacingVoxel[2] * spacing[2])
- gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3)
- transform = slicer.vtkOrientedGridTransform()
- directionMatrix = vtk.vtkMatrix4x4()
- volumeNode.GetIJKToRASDirectionMatrix(directionMatrix)
- transform.SetGridDirectionMatrix(directionMatrix)
- transform.SetDisplacementGridData(gridImage)
-
- # create the grid transform node
- gridTransform = slicer.vtkMRMLGridTransformNode()
- gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform'))
- slicer.mrmlScene.AddNode(gridTransform)
- gridTransform.SetAndObserveTransformToParent(transform)
-
- # populate the grid so that each corner of each slice
- # is mapped from the source corner to the target corner
-
- nshape = tuple(reversed(gridImage.GetDimensions()))
- nshape = nshape + (3,)
- displacements = vtk.util.numpy_support.vtk_to_numpy(gridImage.GetPointData().GetScalars()).reshape(nshape)
-
- # Get displacements
- from math import sin, cos
- ijkToRas = vtk.vtkMatrix4x4()
- volumeNode.GetIJKToRASMatrix(ijkToRas)
- spacing = volumeNode.GetSpacing()
- center_IJK = [(extent[0] + extent[1]) / 2.0, extent[2], (extent[4] + extent[5]) / 2.0]
- sourcePoints_RAS = numpy.zeros(shape=samplingPoints_shape)
- targetPoints_RAS = numpy.zeros(shape=samplingPoints_shape)
- for k in range(samplingPoints_shape[2]):
- for j in range(samplingPoints_shape[1]):
- for i in range(samplingPoints_shape[0]):
- samplingPoint_IJK = [gridAxesIJK[0][i], gridAxesIJK[1][j], gridAxesIJK[2][k], 1]
- sourcePoint_RAS = np.array(ijkToRas.MultiplyPoint(samplingPoint_IJK)[:3])
- radius = probeRadius - (samplingPoint_IJK[1] - center_IJK[1]) * spacing[1]
- angleRad = (samplingPoint_IJK[0] - center_IJK[0]) * spacing[0] / probeRadius
- targetPoint_RAS = np.array([
- -radius * sin(angleRad),
- radius * cos(angleRad) - probeRadius,
- spacing[2] * (samplingPoint_IJK[2] - center_IJK[2])])
- displacements[k][j][i] = targetPoint_RAS - sourcePoint_RAS
-
- return gridTransform
+ def __init__(self):
+ super().__init__()
+ self.loadType = "GE ABUS"
+
+ self.tags['sopClassUID'] = "0008,0016"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['seriesDescription'] = "0008,103E"
+ self.tags['instanceNumber'] = "0020,0013"
+ self.tags['manufacturerModelName'] = "0008,1090"
+
+ # Accepted private creator identifications
+ self.privateCreators = ["U-Systems", "General Electric Company 01"]
+
+ def examine(self, fileLists):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ fileLists parameter.
+ """
+ loadables = []
+ for files in fileLists:
+ loadables += self.examineFiles(files)
+
+ return loadables
+
+ def examineFiles(self, files):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ files parameter.
+ """
+
+ detailedLogging = self.isDetailedLogging()
+
+ supportedSOPClassUIDs = [
+ '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage
+ ]
+
+ loadables = []
+
+ for filePath in files:
+ # Quick check of SOP class UID without parsing the file...
+ try:
+ sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
+ if not (sopClassUID in supportedSOPClassUIDs):
+ # Unsupported class
+ continue
+
+ manufacturerModelName = slicer.dicomDatabase.fileValue(filePath, self.tags['manufacturerModelName'])
+ if manufacturerModelName != "Invenia":
+ if detailedLogging:
+ logging.debug("ManufacturerModelName is not Invenia, the series will not be considered as an ABUS image")
+ continue
+
+ except Exception as e:
+ # Quick check could not be completed (probably Slicer DICOM database is not initialized).
+ # No problem, we'll try to parse the file and check the SOP class UID then.
+ pass
+
+ try:
+ ds = dicom.read_file(filePath, stop_before_pixels=True)
+ except Exception as e:
+ logging.debug(f"Failed to parse DICOM file: {str(e)}")
+ continue
+
+ # check if probeCurvatureRadius is available
+ probeCurvatureRadiusFound = False
+ for privateCreator in self.privateCreators:
+ if self.findPrivateTag(ds, 0x0021, 0x40, privateCreator):
+ probeCurvatureRadiusFound = True
+ break
+
+ if not probeCurvatureRadiusFound:
+ if detailedLogging:
+ logging.debug("Probe curvature radius is not found, the series will not be considered as an ABUS image")
+ continue
+
+ name = ''
+ if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber:
+ name = f'{ds.SeriesNumber}:'
+ if hasattr(ds, 'Modality') and ds.Modality:
+ name = f'{name} {ds.Modality}'
+ if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription:
+ name = f'{name} {ds.SeriesDescription}'
+ if hasattr(ds, 'InstanceNumber') and ds.InstanceNumber:
+ name = f'{name} [{ds.InstanceNumber}]'
+
+ loadable = DICOMLoadable()
+ loadable.files = [filePath]
+ loadable.name = name.strip() # remove leading and trailing spaces, if any
+ loadable.tooltip = "GE Invenia ABUS"
+ loadable.warning = "Loading of this image type is experimental. Please verify image size and orientation and report any problem is found."
+ loadable.selected = True
+ loadable.confidence = 0.9 # this has to be higher than 0.7 (ultrasound sequence)
+
+ # Add to loadables list
+ loadables.append(loadable)
+
+ return loadables
+
+ def getMetadata(self, filePath):
+ try:
+ ds = dicom.read_file(filePath, stop_before_pixels=True)
+ except Exception as e:
+ raise ValueError(f"Failed to parse DICOM file: {str(e)}")
+
+ fieldsInfo = {
+ 'NipplePosition': {'group': 0x0021, 'element': 0x20, 'private': True, 'required': False},
+ 'FirstElementPosition': {'group': 0x0021, 'element': 0x21, 'private': True, 'required': False},
+ 'CurvatureRadiusProbe': {'group': 0x0021, 'element': 0x40, 'private': True, 'required': True},
+ 'CurvatureRadiusTrack': {'group': 0x0021, 'element': 0x41, 'private': True, 'required': True},
+ 'LineDensity': {'group': 0x0021, 'element': 0x62, 'private': True, 'required': False},
+ 'ScanDepthCm': {'group': 0x0021, 'element': 0x63, 'private': True, 'required': True},
+ 'SpacingBetweenSlices': {'group': 0x0018, 'element': 0x0088, 'private': False, 'required': True},
+ 'PixelSpacing': {'group': 0x0028, 'element': 0x0030, 'private': False, 'required': True},
+ }
+
+ fieldValues = {}
+ for fieldName in fieldsInfo:
+ fieldInfo = fieldsInfo[fieldName]
+ if fieldInfo['private']:
+ for privateCreator in self.privateCreators:
+ tag = self.findPrivateTag(ds, fieldInfo['group'], fieldInfo['element'], privateCreator)
+ if tag:
+ break
+ else:
+ tag = dicom.tag.Tag(fieldInfo['group'], fieldInfo['element'])
+ if tag:
+ fieldValues[fieldName] = ds[tag].value
+
+ # Make sure all mandatory fields are found
+ for fieldName in fieldsInfo:
+ fieldInfo = fieldsInfo[fieldName]
+ if not fieldInfo['required']:
+ continue
+ if not fieldName in fieldValues:
+ raise ValueError(f"Mandatory field {fieldName} was not found")
+
+ return fieldValues
+
+ def load(self, loadable):
+ """Load the selection
+ """
+
+ filePath = loadable.files[0]
+ metadata = self.getMetadata(filePath)
+
+ import vtkITK
+ reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
+ reader.SetArchetype(filePath)
+ reader.AddFileName(filePath)
+ reader.SetSingleFile(True)
+ reader.SetOutputScalarTypeToNative()
+ reader.SetDesiredCoordinateOrientationToNative()
+ reader.SetUseNativeOriginOn()
+ # GDCM is not particularly better in this than DCMTK, we just select one explicitly
+ # so that we know which one is used
+ reader.SetDICOMImageIOApproachToGDCM()
+ reader.Update()
+ imageData = reader.GetOutput()
+
+ if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
+ errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
+ raise ValueError(
+ f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
+
+ # Image origin and spacing is stored in IJK to RAS matrix
+ imageData.SetSpacing(1.0, 1.0, 1.0)
+ imageData.SetOrigin(0.0, 0.0, 0.0)
+
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", slicer.mrmlScene.GenerateUniqueName(loadable.name))
+
+ # I axis: scanline index (lateralSpacing)
+ # J axis: sound propagation (axialSpacing)
+ # K axis: slice (sliceSpacing)
+ lateralSpacing = metadata['PixelSpacing'][1]
+ axialSpacing = metadata['PixelSpacing'][0]
+ sliceSpacing = metadata['SpacingBetweenSlices']
+
+ ijkToRas = vtk.vtkMatrix4x4()
+ ijkToRas.SetElement(0, 0, -1)
+ ijkToRas.SetElement(1, 1, -1) # so that J axis points toward posterior
+ volumeNode.SetIJKToRASMatrix(ijkToRas)
+ volumeNode.SetSpacing(lateralSpacing, axialSpacing, sliceSpacing)
+ extent = imageData.GetExtent()
+ volumeNode.SetOrigin((extent[1] - extent[0] + 1) * 0.5 * lateralSpacing, 0, -(extent[5] - extent[2] + 1) * 0.5 * sliceSpacing)
+ volumeNode.SetAndObserveImageData(imageData)
+
+ # Apply scan conversion transform
+ acquisitionTransform = self.createAcquisitionTransform(volumeNode, metadata)
+ volumeNode.SetAndObserveTransformNodeID(acquisitionTransform.GetID())
+
+ # Create Subject hierarchy nodes for the loaded series
+ self.addSeriesInSubjectHierarchy(loadable, volumeNode)
+
+ # Place transform in the same subject hierarchy folder as the volume node
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode))
+ shNode.SetItemParent(shNode.GetItemByDataNode(acquisitionTransform), volumeParentItemId)
+
+ # Show in slice views
+ selectionNode = slicer.app.applicationLogic().GetSelectionNode()
+ selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID())
+ slicer.app.applicationLogic().PropagateVolumeSelection(1)
+
+ return volumeNode
+
+ def createAcquisitionTransform(self, volumeNode, metadata):
+
+ # Creates transform that applies scan conversion transform
+ probeRadius = metadata['CurvatureRadiusProbe']
+ trackRadius = metadata['CurvatureRadiusTrack']
+ if trackRadius != 0.0:
+ raise ValueError(f"Curvature Radius (Track) is {trackRadius}. Currently, only volume with zero radius can be imported.")
+
+ # Create a sampling grid for the transform
+ import numpy as np
+ spacing = np.array(volumeNode.GetSpacing())
+ averageSpacing = (spacing[0] + spacing[1] + spacing[2]) / 3.0
+ voxelsPerTransformControlPoint = 20 # the transform is changing smoothly, so we don't need to add too many control points
+ gridSpacingMm = averageSpacing * voxelsPerTransformControlPoint
+ gridSpacingVoxel = np.floor(gridSpacingMm / spacing).astype(int)
+ gridAxesIJK = []
+ imageData = volumeNode.GetImageData()
+ extent = imageData.GetExtent()
+ for axis in range(3):
+ gridAxesIJK.append(list(range(extent[axis * 2], extent[axis * 2 + 1] + gridSpacingVoxel[axis], gridSpacingVoxel[axis])))
+ samplingPoints_shape = [len(gridAxesIJK[0]), len(gridAxesIJK[1]), len(gridAxesIJK[2]), 3]
+
+ # create a grid transform with one vector at the corner of each slice
+ # the transform is in the same space and orientation as the volume node
+ import vtk
+ gridImage = vtk.vtkImageData()
+ gridImage.SetOrigin(*volumeNode.GetOrigin())
+ gridImage.SetDimensions(samplingPoints_shape[:3])
+ gridImage.SetSpacing(gridSpacingVoxel[0] * spacing[0], gridSpacingVoxel[1] * spacing[1], gridSpacingVoxel[2] * spacing[2])
+ gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3)
+ transform = slicer.vtkOrientedGridTransform()
+ directionMatrix = vtk.vtkMatrix4x4()
+ volumeNode.GetIJKToRASDirectionMatrix(directionMatrix)
+ transform.SetGridDirectionMatrix(directionMatrix)
+ transform.SetDisplacementGridData(gridImage)
+
+ # create the grid transform node
+ gridTransform = slicer.vtkMRMLGridTransformNode()
+ gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform'))
+ slicer.mrmlScene.AddNode(gridTransform)
+ gridTransform.SetAndObserveTransformToParent(transform)
+
+ # populate the grid so that each corner of each slice
+ # is mapped from the source corner to the target corner
+
+ nshape = tuple(reversed(gridImage.GetDimensions()))
+ nshape = nshape + (3,)
+ displacements = vtk.util.numpy_support.vtk_to_numpy(gridImage.GetPointData().GetScalars()).reshape(nshape)
+
+ # Get displacements
+ from math import sin, cos
+ ijkToRas = vtk.vtkMatrix4x4()
+ volumeNode.GetIJKToRASMatrix(ijkToRas)
+ spacing = volumeNode.GetSpacing()
+ center_IJK = [(extent[0] + extent[1]) / 2.0, extent[2], (extent[4] + extent[5]) / 2.0]
+ sourcePoints_RAS = numpy.zeros(shape=samplingPoints_shape)
+ targetPoints_RAS = numpy.zeros(shape=samplingPoints_shape)
+ for k in range(samplingPoints_shape[2]):
+ for j in range(samplingPoints_shape[1]):
+ for i in range(samplingPoints_shape[0]):
+ samplingPoint_IJK = [gridAxesIJK[0][i], gridAxesIJK[1][j], gridAxesIJK[2][k], 1]
+ sourcePoint_RAS = np.array(ijkToRas.MultiplyPoint(samplingPoint_IJK)[:3])
+ radius = probeRadius - (samplingPoint_IJK[1] - center_IJK[1]) * spacing[1]
+ angleRad = (samplingPoint_IJK[0] - center_IJK[0]) * spacing[0] / probeRadius
+ targetPoint_RAS = np.array([
+ -radius * sin(angleRad),
+ radius * cos(angleRad) - probeRadius,
+ spacing[2] * (samplingPoint_IJK[2] - center_IJK[2])])
+ displacements[k][j][i] = targetPoint_RAS - sourcePoint_RAS
+
+ return gridTransform
#
@@ -306,31 +306,31 @@ def createAcquisitionTransform(self, volumeNode, metadata):
#
class DICOMGeAbusPlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM GE ABUS Import Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Andras Lasso (PerkLab)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM GE ABUS Import Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Andras Lasso (PerkLab)"]
+ parent.helpText = """
Plugin to the DICOM Module to parse and load GE Invenia ABUS images.
No module interface here, only in the DICOM module.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
The file was originally developed by Andras Lasso (PerkLab).
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
-
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMGeAbusPlugin'] = DICOMGeAbusPluginClass
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
+
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMGeAbusPlugin'] = DICOMGeAbusPluginClass
diff --git a/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py
index 96425c1f792..06e1efc633b 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMImageSequencePlugin.py
@@ -17,369 +17,369 @@
#
class DICOMImageSequencePluginClass(DICOMPlugin):
- """ 2D image sequence loader plugin.
- It supports X-ray angiography and ultrasound images.
- The main difference compared to plain scalar volume plugin is that it
- loads frames as a single-slice-volume sequence (and not as a 3D volume),
- it accepts color images, and handles multiple instances within a series
- (e.g., multiple independent acquisitions and synchronized biplane acquisitions).
- Limitation: ultrasound calibrated regions are not supported (each calibrated region
- would need to be split out to its own volume sequence).
- """
-
- def __init__(self):
- super().__init__()
- self.loadType = "Image sequence"
-
- self.tags['sopClassUID'] = "0008,0016"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['seriesDescription'] = "0008,103E"
- self.tags['instanceNumber'] = "0020,0013"
- self.tags['triggerTime'] = "0018,1060"
- self.tags['modality'] = "0008,0060"
- self.tags['photometricInterpretation'] = "0028,0004"
- self.tags['orientation'] = "0020,0037"
-
- self.detailedLogging = False
-
- def examine(self, fileLists):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- fileLists parameter.
+ """ 2D image sequence loader plugin.
+ It supports X-ray angiography and ultrasound images.
+ The main difference compared to plain scalar volume plugin is that it
+ loads frames as a single-slice-volume sequence (and not as a 3D volume),
+ it accepts color images, and handles multiple instances within a series
+ (e.g., multiple independent acquisitions and synchronized biplane acquisitions).
+ Limitation: ultrasound calibrated regions are not supported (each calibrated region
+ would need to be split out to its own volume sequence).
"""
- loadables = []
- for files in fileLists:
- loadables += self.examineFiles(files)
- return loadables
+ def __init__(self):
+ super().__init__()
+ self.loadType = "Image sequence"
+
+ self.tags['sopClassUID'] = "0008,0016"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['seriesDescription'] = "0008,103E"
+ self.tags['instanceNumber'] = "0020,0013"
+ self.tags['triggerTime'] = "0018,1060"
+ self.tags['modality'] = "0008,0060"
+ self.tags['photometricInterpretation'] = "0028,0004"
+ self.tags['orientation'] = "0020,0037"
+
+ self.detailedLogging = False
+
+ def examine(self, fileLists):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ fileLists parameter.
+ """
+ loadables = []
+ for files in fileLists:
+ loadables += self.examineFiles(files)
+
+ return loadables
+
+ def examineFiles(self, files):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ files parameter.
+ """
+
+ self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
+
+ supportedSOPClassUIDs = [
+ '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage
+ '1.2.840.10008.5.1.4.1.1.12.2', # X-Ray Fluoroscopy Image Storage
+ '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage
+ '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage
+ '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage (only accepted for modalities that typically acquire 2D image sequences)
+ '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage (will be only accepted if cine-MRI)
+ ]
+
+ # Modalities that typically acquire 2D image sequences:
+ suppportedSecondaryCaptureModalities = ['US', 'XA', 'RF', 'ES']
+
+ # Each instance will be a loadable, that will result in one sequence browser node
+ # and usually one sequence (except simultaneous biplane acquisition, which will
+ # result in two sequences).
+ # Each pedal press on the XA/RF acquisition device creates a new instance number,
+ # but if the device has two imaging planes (biplane) then two sequences
+ # will be acquired, which have the same instance number. These two sequences
+ # are synchronized in time, therefore they have to be assigned to the same
+ # browser node.
+ instanceNumberToLoadableIndex = {}
+
+ loadables = []
+
+ canBeCineMri = True
+ cineMriTriggerTimes = set()
+ cineMriImageOrientations = set()
+ cineMriInstanceNumberToFilenameIndex = {}
+
+ for filePath in files:
+ # Quick check of SOP class UID without parsing the file...
+ try:
+ sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
+ if not (sopClassUID in supportedSOPClassUIDs):
+ # Unsupported class
+ continue
+
+ # Only accept MRI if it looks like cine-MRI
+ if sopClassUID != '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage (will be only accepted if cine-MRI)
+ canBeCineMri = False
+ if not canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage
+ continue
+
+ except Exception as e:
+ # Quick check could not be completed (probably Slicer DICOM database is not initialized).
+ # No problem, we'll try to parse the file and check the SOP class UID then.
+ pass
+
+ instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber'])
+ if canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage
+ if not instanceNumber:
+ # no instance number, probably not cine-MRI
+ canBeCineMri = False
+ if self.detailedLogging:
+ logging.debug("No instance number attribute found, the series will not be considered as a cine MRI")
+ continue
+ cineMriInstanceNumberToFilenameIndex[int(instanceNumber)] = filePath
+ cineMriTriggerTimes.add(slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime']))
+ cineMriImageOrientations.add(slicer.dicomDatabase.fileValue(filePath, self.tags['orientation']))
+
+ else:
+ modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality'])
+ if sopClassUID == '1.2.840.10008.5.1.4.1.1.7': # Secondary Capture Image Storage
+ if modality not in suppportedSecondaryCaptureModalities:
+ # practice of dumping secondary capture images into the same series
+ # is only prevalent in US and XA/RF modalities
+ continue
+
+ if not (instanceNumber in instanceNumberToLoadableIndex.keys()):
+ # new instance number
+ seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber'])
+ seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription'])
+ photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation'])
+ name = ''
+ if seriesNumber:
+ name = f'{seriesNumber}:'
+ if modality:
+ name = f'{name} {modality}'
+ if seriesDescription:
+ name = f'{name} {seriesDescription}'
+ if instanceNumber:
+ name = f'{name} [{instanceNumber}]'
+
+ loadable = DICOMLoadable()
+ loadable.singleSequence = False # put each instance in a separate sequence
+ loadable.files = [filePath]
+ loadable.name = name.strip() # remove leading and trailing spaces, if any
+ loadable.warning = "Image spacing may need to be calibrated for accurate size measurements."
+ loadable.tooltip = f"{modality} image sequence"
+ loadable.selected = True
+ # Confidence is slightly larger than default scalar volume plugin's (0.5)
+ # but still leaving room for more specialized plugins.
+ loadable.confidence = 0.7
+ loadable.grayscale = ('MONOCHROME' in photometricInterpretation)
+
+ # Add to loadables list
+ loadables.append(loadable)
+ instanceNumberToLoadableIndex[instanceNumber] = len(loadables) - 1
+ else:
+ # existing instance number, add this file
+ loadableIndex = instanceNumberToLoadableIndex[instanceNumber]
+ loadables[loadableIndex].files.append(filePath)
+ loadable.tooltip = f"{modality} image sequence ({len(loadables[loadableIndex].files)} planes)"
+
+ if canBeCineMri and len(cineMriInstanceNumberToFilenameIndex) > 1:
+ # Get description from first
+ ds = dicom.read_file(cineMriInstanceNumberToFilenameIndex[next(iter(cineMriInstanceNumberToFilenameIndex))], stop_before_pixels=True)
+ name = ''
+ if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber:
+ name = f'{ds.SeriesNumber}:'
+ if hasattr(ds, 'Modality') and ds.Modality:
+ name = f'{name} {ds.Modality}'
+ if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription:
+ name = f'{name} {ds.SeriesDescription}'
+
+ loadable = DICOMLoadable()
+ loadable.singleSequence = True # put all instances in a single sequence
+ loadable.instanceNumbers = sorted(cineMriInstanceNumberToFilenameIndex)
+ loadable.files = [cineMriInstanceNumberToFilenameIndex[instanceNumber] for instanceNumber in loadable.instanceNumbers]
+ loadable.name = name.strip() # remove leading and trailing spaces, if any
+ loadable.tooltip = f"{ds.Modality} image sequence"
+ loadable.selected = True
+ if len(cineMriTriggerTimes) > 3:
+ if self.detailedLogging:
+ logging.debug("Several different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is a cine MRI")
+ # This is likely a cardiac cine acquisition.
+ if len(cineMriImageOrientations) > 1:
+ if self.detailedLogging:
+ logging.debug("Several different image orientations found (" + repr(cineMriImageOrientations) + ") - assuming this series is a rotational cine MRI")
+ # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit higher confidence to be selected by default
+ loadable.confidence = 1.05
+ else:
+ if self.detailedLogging:
+ logging.debug("All image orientations are the same (" + repr(cineMriImageOrientations) + ") - probably the MultiVolume plugin should load this")
+ # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit lower confidence to allow multivolume selected by default
+ loadable.confidence = 0.85
+ else:
+ # This may be a 3D acquisition,so set lower confidence than scalar volume's default (0.5)
+ if self.detailedLogging:
+ logging.debug("Only one or few different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is not a cine MRI")
+ loadable.confidence = 0.4
+ loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation)
+
+ # Add to loadables list
+ loadables.append(loadable)
+
+ return loadables
+
+ def loadImageData(self, filePath, grayscale, volumeNode):
+ import vtkITK
+ if grayscale:
+ reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
+ else:
+ reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
+ reader.SetArchetype(filePath)
+ reader.AddFileName(filePath)
+ reader.SetSingleFile(True)
+ reader.SetOutputScalarTypeToNative()
+ reader.SetDesiredCoordinateOrientationToNative()
+ reader.SetUseNativeOriginOn()
+ # GDCM is not particularly better in this than DCMTK, we just select one explicitly
+ # so that we know which one is used
+ reader.SetDICOMImageIOApproachToGDCM()
+ reader.Update()
+ if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
+ errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
+ raise ValueError(
+ f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
+
+ rasToIjk = reader.GetRasToIjkMatrix()
+ ijkToRas = vtk.vtkMatrix4x4()
+ vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas)
+ return reader.GetOutput(), ijkToRas
+
+ def addSequenceBrowserNode(self, name, outputSequenceNodes, playbackRateFps, loadable):
+ # Add a browser node and show the volume in the slice viewer for user convenience
+ outputSequenceBrowserNode = slicer.vtkMRMLSequenceBrowserNode()
+ outputSequenceBrowserNode.SetName(slicer.mrmlScene.GenerateUniqueName(name + ' browser'))
+ outputSequenceBrowserNode.SetPlaybackRateFps(playbackRateFps)
+ slicer.mrmlScene.AddNode(outputSequenceBrowserNode)
+
+ # Add all sequences to the sequence browser
+ first = True
+ for outputSequenceNode in outputSequenceNodes:
+ outputSequenceBrowserNode.AddSynchronizedSequenceNode(outputSequenceNode)
+ proxyVolumeNode = outputSequenceBrowserNode.GetProxyNode(outputSequenceNode)
+ # create Subject hierarchy nodes for the loaded series
+ self.addSeriesInSubjectHierarchy(loadable, proxyVolumeNode)
+
+ if first:
+ first = False
+ # Automatically select the volume to display
+ appLogic = slicer.app.applicationLogic()
+ selNode = appLogic.GetSelectionNode()
+ selNode.SetReferenceActiveVolumeID(proxyVolumeNode.GetID())
+ appLogic.PropagateVolumeSelection()
+ appLogic.FitSliceToAll()
+ slicer.modules.sequences.setToolBarActiveBrowserNode(outputSequenceBrowserNode)
+
+ # Show sequence browser toolbar
+ slicer.modules.sequences.showSequenceBrowser(outputSequenceBrowserNode)
+
+ def addSequenceFromImageData(self, imageData, tempFrameVolume, filePath, name, singleFileInLoadable):
+
+ # Rotate 180deg, otherwise the image would appear upside down
+ ijkToRas = vtk.vtkMatrix4x4()
+ ijkToRas.SetElement(0, 0, -1.0)
+ ijkToRas.SetElement(1, 1, -1.0)
+ tempFrameVolume.SetIJKToRASMatrix(ijkToRas)
+ # z axis is time
+ [spacingX, spacingY, frameTimeMsec] = imageData.GetSpacing()
+ imageData.SetSpacing(1.0, 1.0, 1.0)
+ tempFrameVolume.SetSpacing(spacingX, spacingY, 1.0)
- def examineFiles(self, files):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- files parameter.
- """
+ # Create new sequence
+ outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode")
- self.detailedLogging = slicer.util.settingsValue('DICOM/detailedLogging', False, converter=slicer.util.toBool)
-
- supportedSOPClassUIDs = [
- '1.2.840.10008.5.1.4.1.1.12.1', # X-Ray Angiographic Image Storage
- '1.2.840.10008.5.1.4.1.1.12.2', # X-Ray Fluoroscopy Image Storage
- '1.2.840.10008.5.1.4.1.1.3.1', # Ultrasound Multiframe Image Storage
- '1.2.840.10008.5.1.4.1.1.6.1', # Ultrasound Image Storage
- '1.2.840.10008.5.1.4.1.1.7', # Secondary Capture Image Storage (only accepted for modalities that typically acquire 2D image sequences)
- '1.2.840.10008.5.1.4.1.1.4', # MR Image Storage (will be only accepted if cine-MRI)
- ]
-
- # Modalities that typically acquire 2D image sequences:
- suppportedSecondaryCaptureModalities = ['US', 'XA', 'RF', 'ES']
-
- # Each instance will be a loadable, that will result in one sequence browser node
- # and usually one sequence (except simultaneous biplane acquisition, which will
- # result in two sequences).
- # Each pedal press on the XA/RF acquisition device creates a new instance number,
- # but if the device has two imaging planes (biplane) then two sequences
- # will be acquired, which have the same instance number. These two sequences
- # are synchronized in time, therefore they have to be assigned to the same
- # browser node.
- instanceNumberToLoadableIndex = {}
-
- loadables = []
-
- canBeCineMri = True
- cineMriTriggerTimes = set()
- cineMriImageOrientations = set()
- cineMriInstanceNumberToFilenameIndex = {}
-
- for filePath in files:
- # Quick check of SOP class UID without parsing the file...
- try:
- sopClassUID = slicer.dicomDatabase.fileValue(filePath, self.tags['sopClassUID'])
- if not (sopClassUID in supportedSOPClassUIDs):
- # Unsupported class
- continue
-
- # Only accept MRI if it looks like cine-MRI
- if sopClassUID != '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage (will be only accepted if cine-MRI)
- canBeCineMri = False
- if not canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage
- continue
-
- except Exception as e:
- # Quick check could not be completed (probably Slicer DICOM database is not initialized).
- # No problem, we'll try to parse the file and check the SOP class UID then.
- pass
-
- instanceNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['instanceNumber'])
- if canBeCineMri and sopClassUID == '1.2.840.10008.5.1.4.1.1.4': # MR Image Storage
- if not instanceNumber:
- # no instance number, probably not cine-MRI
- canBeCineMri = False
- if self.detailedLogging:
- logging.debug("No instance number attribute found, the series will not be considered as a cine MRI")
- continue
- cineMriInstanceNumberToFilenameIndex[int(instanceNumber)] = filePath
- cineMriTriggerTimes.add(slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime']))
- cineMriImageOrientations.add(slicer.dicomDatabase.fileValue(filePath, self.tags['orientation']))
-
- else:
- modality = slicer.dicomDatabase.fileValue(filePath, self.tags['modality'])
- if sopClassUID == '1.2.840.10008.5.1.4.1.1.7': # Secondary Capture Image Storage
- if modality not in suppportedSecondaryCaptureModalities:
- # practice of dumping secondary capture images into the same series
- # is only prevalent in US and XA/RF modalities
- continue
-
- if not (instanceNumber in instanceNumberToLoadableIndex.keys()):
- # new instance number
- seriesNumber = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesNumber'])
- seriesDescription = slicer.dicomDatabase.fileValue(filePath, self.tags['seriesDescription'])
- photometricInterpretation = slicer.dicomDatabase.fileValue(filePath, self.tags['photometricInterpretation'])
- name = ''
- if seriesNumber:
- name = f'{seriesNumber}:'
- if modality:
- name = f'{name} {modality}'
- if seriesDescription:
- name = f'{name} {seriesDescription}'
- if instanceNumber:
- name = f'{name} [{instanceNumber}]'
-
- loadable = DICOMLoadable()
- loadable.singleSequence = False # put each instance in a separate sequence
- loadable.files = [filePath]
- loadable.name = name.strip() # remove leading and trailing spaces, if any
- loadable.warning = "Image spacing may need to be calibrated for accurate size measurements."
- loadable.tooltip = f"{modality} image sequence"
- loadable.selected = True
- # Confidence is slightly larger than default scalar volume plugin's (0.5)
- # but still leaving room for more specialized plugins.
- loadable.confidence = 0.7
- loadable.grayscale = ('MONOCHROME' in photometricInterpretation)
-
- # Add to loadables list
- loadables.append(loadable)
- instanceNumberToLoadableIndex[instanceNumber] = len(loadables) - 1
+ # Get sequence name
+ if singleFileInLoadable:
+ outputSequenceNode.SetName(name)
else:
- # existing instance number, add this file
- loadableIndex = instanceNumberToLoadableIndex[instanceNumber]
- loadables[loadableIndex].files.append(filePath)
- loadable.tooltip = f"{modality} image sequence ({len(loadables[loadableIndex].files)} planes)"
-
- if canBeCineMri and len(cineMriInstanceNumberToFilenameIndex) > 1:
- # Get description from first
- ds = dicom.read_file(cineMriInstanceNumberToFilenameIndex[next(iter(cineMriInstanceNumberToFilenameIndex))], stop_before_pixels=True)
- name = ''
- if hasattr(ds, 'SeriesNumber') and ds.SeriesNumber:
- name = f'{ds.SeriesNumber}:'
- if hasattr(ds, 'Modality') and ds.Modality:
- name = f'{name} {ds.Modality}'
- if hasattr(ds, 'SeriesDescription') and ds.SeriesDescription:
- name = f'{name} {ds.SeriesDescription}'
-
- loadable = DICOMLoadable()
- loadable.singleSequence = True # put all instances in a single sequence
- loadable.instanceNumbers = sorted(cineMriInstanceNumberToFilenameIndex)
- loadable.files = [cineMriInstanceNumberToFilenameIndex[instanceNumber] for instanceNumber in loadable.instanceNumbers]
- loadable.name = name.strip() # remove leading and trailing spaces, if any
- loadable.tooltip = f"{ds.Modality} image sequence"
- loadable.selected = True
- if len(cineMriTriggerTimes) > 3:
- if self.detailedLogging:
- logging.debug("Several different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is a cine MRI")
- # This is likely a cardiac cine acquisition.
- if len(cineMriImageOrientations) > 1:
- if self.detailedLogging:
- logging.debug("Several different image orientations found (" + repr(cineMriImageOrientations) + ") - assuming this series is a rotational cine MRI")
- # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit higher confidence to be selected by default
- loadable.confidence = 1.05
+ ds = dicom.read_file(filePath, stop_before_pixels=True)
+ if hasattr(ds, 'PositionerPrimaryAngle') and hasattr(ds, 'PositionerSecondaryAngle'):
+ outputSequenceNode.SetName(f'{name} ({ds.PositionerPrimaryAngle}/{ds.PositionerSecondaryAngle})')
+ else:
+ outputSequenceNode.SetName(name)
+
+ if frameTimeMsec == 1.0:
+ # frame time is not found, set it to 1.0fps
+ frameTime = 1
+ outputSequenceNode.SetIndexName("frame")
+ outputSequenceNode.SetIndexUnit("")
+ playbackRateFps = 10
else:
- if self.detailedLogging:
- logging.debug("All image orientations are the same (" + repr(cineMriImageOrientations) + ") - probably the MultiVolume plugin should load this")
- # Multivolume importer sets confidence=0.9-1.0, so we need to set a bit lower confidence to allow multivolume selected by default
- loadable.confidence = 0.85
- else:
- # This may be a 3D acquisition,so set lower confidence than scalar volume's default (0.5)
- if self.detailedLogging:
- logging.debug("Only one or few different trigger times found (" + repr(cineMriTriggerTimes) + ") - assuming this series is not a cine MRI")
- loadable.confidence = 0.4
- loadable.grayscale = ('MONOCHROME' in ds.PhotometricInterpretation)
-
- # Add to loadables list
- loadables.append(loadable)
-
- return loadables
-
- def loadImageData(self, filePath, grayscale, volumeNode):
- import vtkITK
- if grayscale:
- reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
- else:
- reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
- reader.SetArchetype(filePath)
- reader.AddFileName(filePath)
- reader.SetSingleFile(True)
- reader.SetOutputScalarTypeToNative()
- reader.SetDesiredCoordinateOrientationToNative()
- reader.SetUseNativeOriginOn()
- # GDCM is not particularly better in this than DCMTK, we just select one explicitly
- # so that we know which one is used
- reader.SetDICOMImageIOApproachToGDCM()
- reader.Update()
- if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
- errorString = vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode())
- raise ValueError(
- f"Could not read image {loadable.name} from file {filePath}. Error is: {errorString}")
-
- rasToIjk = reader.GetRasToIjkMatrix()
- ijkToRas = vtk.vtkMatrix4x4()
- vtk.vtkMatrix4x4.Invert(rasToIjk, ijkToRas)
- return reader.GetOutput(), ijkToRas
-
- def addSequenceBrowserNode(self, name, outputSequenceNodes, playbackRateFps, loadable):
- # Add a browser node and show the volume in the slice viewer for user convenience
- outputSequenceBrowserNode = slicer.vtkMRMLSequenceBrowserNode()
- outputSequenceBrowserNode.SetName(slicer.mrmlScene.GenerateUniqueName(name + ' browser'))
- outputSequenceBrowserNode.SetPlaybackRateFps(playbackRateFps)
- slicer.mrmlScene.AddNode(outputSequenceBrowserNode)
-
- # Add all sequences to the sequence browser
- first = True
- for outputSequenceNode in outputSequenceNodes:
- outputSequenceBrowserNode.AddSynchronizedSequenceNode(outputSequenceNode)
- proxyVolumeNode = outputSequenceBrowserNode.GetProxyNode(outputSequenceNode)
- # create Subject hierarchy nodes for the loaded series
- self.addSeriesInSubjectHierarchy(loadable, proxyVolumeNode)
-
- if first:
- first = False
- # Automatically select the volume to display
- appLogic = slicer.app.applicationLogic()
- selNode = appLogic.GetSelectionNode()
- selNode.SetReferenceActiveVolumeID(proxyVolumeNode.GetID())
- appLogic.PropagateVolumeSelection()
- appLogic.FitSliceToAll()
- slicer.modules.sequences.setToolBarActiveBrowserNode(outputSequenceBrowserNode)
-
- # Show sequence browser toolbar
- slicer.modules.sequences.showSequenceBrowser(outputSequenceBrowserNode)
-
- def addSequenceFromImageData(self, imageData, tempFrameVolume, filePath, name, singleFileInLoadable):
-
- # Rotate 180deg, otherwise the image would appear upside down
- ijkToRas = vtk.vtkMatrix4x4()
- ijkToRas.SetElement(0, 0, -1.0)
- ijkToRas.SetElement(1, 1, -1.0)
- tempFrameVolume.SetIJKToRASMatrix(ijkToRas)
- # z axis is time
- [spacingX, spacingY, frameTimeMsec] = imageData.GetSpacing()
- imageData.SetSpacing(1.0, 1.0, 1.0)
- tempFrameVolume.SetSpacing(spacingX, spacingY, 1.0)
-
- # Create new sequence
- outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode")
-
- # Get sequence name
- if singleFileInLoadable:
- outputSequenceNode.SetName(name)
- else:
- ds = dicom.read_file(filePath, stop_before_pixels=True)
- if hasattr(ds, 'PositionerPrimaryAngle') and hasattr(ds, 'PositionerSecondaryAngle'):
- outputSequenceNode.SetName(f'{name} ({ds.PositionerPrimaryAngle}/{ds.PositionerSecondaryAngle})')
- else:
- outputSequenceNode.SetName(name)
-
- if frameTimeMsec == 1.0:
- # frame time is not found, set it to 1.0fps
- frameTime = 1
- outputSequenceNode.SetIndexName("frame")
- outputSequenceNode.SetIndexUnit("")
- playbackRateFps = 10
- else:
- # frame time is set, use it
- frameTime = frameTimeMsec * 0.001
- outputSequenceNode.SetIndexName("time")
- outputSequenceNode.SetIndexUnit("s")
- playbackRateFps = 1.0 / frameTime
-
- # Add frames to the sequence
- numberOfFrames = imageData.GetDimensions()[2]
- extent = imageData.GetExtent()
- numberOfFrames = extent[5] - extent[4] + 1
- for frame in range(numberOfFrames):
- # get current frame from multiframe
- crop = vtk.vtkImageClip()
- crop.SetInputData(imageData)
- crop.SetOutputWholeExtent(extent[0], extent[1], extent[2], extent[3], extent[4] + frame, extent[4] + frame)
- crop.ClipDataOn()
- crop.Update()
- croppedOutput = crop.GetOutput()
- croppedOutput.SetExtent(extent[0], extent[1], extent[2], extent[3], 0, 0)
- croppedOutput.SetOrigin(0.0, 0.0, 0.0)
- tempFrameVolume.SetAndObserveImageData(croppedOutput)
- # get timestamp
- if type(frameTime) == int:
- timeStampSec = str(frame * frameTime)
- else:
- timeStampSec = f"{frame * frameTime:.3f}"
- outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, timeStampSec)
-
- # Create storage node that allows saving node as nrrd
- outputSequenceStorageNode = slicer.vtkMRMLVolumeSequenceStorageNode()
- slicer.mrmlScene.AddNode(outputSequenceStorageNode)
- outputSequenceNode.SetAndObserveStorageNodeID(outputSequenceStorageNode.GetID())
-
- return outputSequenceNode, playbackRateFps
-
- def load(self, loadable):
- """Load the selection
- """
-
- outputSequenceNodes = []
-
- if loadable.singleSequence:
- outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode")
- outputSequenceNode.SetName(loadable.name)
- outputSequenceNode.SetIndexName("instance number")
- outputSequenceNode.SetIndexUnit("")
- playbackRateFps = 10
- outputSequenceNodes.append(outputSequenceNode)
-
- # Create a temporary volume node that will be used to insert volume nodes in the sequence
- if loadable.grayscale:
- tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode")
- else:
- tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode")
-
- for fileIndex, filePath in enumerate(loadable.files):
- imageData, ijkToRas = self.loadImageData(filePath, loadable.grayscale, tempFrameVolume)
- if loadable.singleSequence:
- # each file is a frame (cine-MRI)
- imageData.SetSpacing(1.0, 1.0, 1.0)
- imageData.SetOrigin(0.0, 0.0, 0.0)
- tempFrameVolume.SetIJKToRASMatrix(ijkToRas)
- tempFrameVolume.SetAndObserveImageData(imageData)
- instanceNumber = loadable.instanceNumbers[fileIndex]
- # Save DICOM SOP instance UID into the sequence so DICOM metadata can be retrieved later if needed
- tempFrameVolume.SetAttribute('DICOM.instanceUIDs', slicer.dicomDatabase.instanceForFile(filePath))
- # Save trigger time, because it may be needed for 4D cine-MRI volume reconstruction
- triggerTime = slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime'])
- if triggerTime:
- tempFrameVolume.SetAttribute('DICOM.triggerTime', triggerTime)
- outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, str(instanceNumber))
- else:
- # each file is a new sequence
- outputSequenceNode, playbackRateFps = self.addSequenceFromImageData(
- imageData, tempFrameVolume, filePath, loadable.name, (len(loadable.files) == 1))
- outputSequenceNodes.append(outputSequenceNode)
-
- # Delete temporary volume node
- slicer.mrmlScene.RemoveNode(tempFrameVolume)
-
- if not hasattr(loadable, 'createBrowserNode') or loadable.createBrowserNode:
- self.addSequenceBrowserNode(loadable.name, outputSequenceNodes, playbackRateFps, loadable)
-
- # Return the last loaded sequence node (that is the one currently displayed in slice views)
- return outputSequenceNodes[-1]
+ # frame time is set, use it
+ frameTime = frameTimeMsec * 0.001
+ outputSequenceNode.SetIndexName("time")
+ outputSequenceNode.SetIndexUnit("s")
+ playbackRateFps = 1.0 / frameTime
+
+ # Add frames to the sequence
+ numberOfFrames = imageData.GetDimensions()[2]
+ extent = imageData.GetExtent()
+ numberOfFrames = extent[5] - extent[4] + 1
+ for frame in range(numberOfFrames):
+ # get current frame from multiframe
+ crop = vtk.vtkImageClip()
+ crop.SetInputData(imageData)
+ crop.SetOutputWholeExtent(extent[0], extent[1], extent[2], extent[3], extent[4] + frame, extent[4] + frame)
+ crop.ClipDataOn()
+ crop.Update()
+ croppedOutput = crop.GetOutput()
+ croppedOutput.SetExtent(extent[0], extent[1], extent[2], extent[3], 0, 0)
+ croppedOutput.SetOrigin(0.0, 0.0, 0.0)
+ tempFrameVolume.SetAndObserveImageData(croppedOutput)
+ # get timestamp
+ if type(frameTime) == int:
+ timeStampSec = str(frame * frameTime)
+ else:
+ timeStampSec = f"{frame * frameTime:.3f}"
+ outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, timeStampSec)
+
+ # Create storage node that allows saving node as nrrd
+ outputSequenceStorageNode = slicer.vtkMRMLVolumeSequenceStorageNode()
+ slicer.mrmlScene.AddNode(outputSequenceStorageNode)
+ outputSequenceNode.SetAndObserveStorageNodeID(outputSequenceStorageNode.GetID())
+
+ return outputSequenceNode, playbackRateFps
+
+ def load(self, loadable):
+ """Load the selection
+ """
+
+ outputSequenceNodes = []
+
+ if loadable.singleSequence:
+ outputSequenceNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSequenceNode")
+ outputSequenceNode.SetName(loadable.name)
+ outputSequenceNode.SetIndexName("instance number")
+ outputSequenceNode.SetIndexUnit("")
+ playbackRateFps = 10
+ outputSequenceNodes.append(outputSequenceNode)
+
+ # Create a temporary volume node that will be used to insert volume nodes in the sequence
+ if loadable.grayscale:
+ tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode")
+ else:
+ tempFrameVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode")
+
+ for fileIndex, filePath in enumerate(loadable.files):
+ imageData, ijkToRas = self.loadImageData(filePath, loadable.grayscale, tempFrameVolume)
+ if loadable.singleSequence:
+ # each file is a frame (cine-MRI)
+ imageData.SetSpacing(1.0, 1.0, 1.0)
+ imageData.SetOrigin(0.0, 0.0, 0.0)
+ tempFrameVolume.SetIJKToRASMatrix(ijkToRas)
+ tempFrameVolume.SetAndObserveImageData(imageData)
+ instanceNumber = loadable.instanceNumbers[fileIndex]
+ # Save DICOM SOP instance UID into the sequence so DICOM metadata can be retrieved later if needed
+ tempFrameVolume.SetAttribute('DICOM.instanceUIDs', slicer.dicomDatabase.instanceForFile(filePath))
+ # Save trigger time, because it may be needed for 4D cine-MRI volume reconstruction
+ triggerTime = slicer.dicomDatabase.fileValue(filePath, self.tags['triggerTime'])
+ if triggerTime:
+ tempFrameVolume.SetAttribute('DICOM.triggerTime', triggerTime)
+ outputSequenceNode.SetDataNodeAtValue(tempFrameVolume, str(instanceNumber))
+ else:
+ # each file is a new sequence
+ outputSequenceNode, playbackRateFps = self.addSequenceFromImageData(
+ imageData, tempFrameVolume, filePath, loadable.name, (len(loadable.files) == 1))
+ outputSequenceNodes.append(outputSequenceNode)
+
+ # Delete temporary volume node
+ slicer.mrmlScene.RemoveNode(tempFrameVolume)
+
+ if not hasattr(loadable, 'createBrowserNode') or loadable.createBrowserNode:
+ self.addSequenceBrowserNode(loadable.name, outputSequenceNodes, playbackRateFps, loadable)
+
+ # Return the last loaded sequence node (that is the one currently displayed in slice views)
+ return outputSequenceNodes[-1]
#
@@ -387,31 +387,31 @@ def load(self, loadable):
#
class DICOMImageSequencePlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM Image Sequence Import Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Andras Lasso (PerkLab)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM Image Sequence Import Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Andras Lasso (PerkLab)"]
+ parent.helpText = """
Plugin to the DICOM Module to parse and load 2D image sequences.
No module interface here, only in the DICOM module.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
The file was originally developed by Andras Lasso (PerkLab).
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
-
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMImageSequencePlugin'] = DICOMImageSequencePluginClass
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
+
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMImageSequencePlugin'] = DICOMImageSequencePluginClass
diff --git a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py
index 01de496c5ff..48e351ffcd5 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMScalarVolumePlugin.py
@@ -22,826 +22,826 @@
#
class DICOMScalarVolumePluginClass(DICOMPlugin):
- """ ScalarVolume specific interpretation code
- """
-
- def __init__(self, epsilon=0.01):
- super().__init__()
- self.loadType = "Scalar Volume"
- self.epsilon = epsilon
- self.acquisitionModeling = None
- self.defaultStudyID = 'SLICER10001' # TODO: What should be the new study ID?
-
- self.tags['sopClassUID'] = "0008,0016"
- self.tags['photometricInterpretation'] = "0028,0004"
- self.tags['seriesDescription'] = "0008,103e"
- self.tags['seriesUID'] = "0020,000E"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['position'] = "0020,0032"
- self.tags['orientation'] = "0020,0037"
- self.tags['pixelData'] = "7fe0,0010"
- self.tags['seriesInstanceUID'] = "0020,000E"
- self.tags['acquisitionNumber'] = "0020,0012"
- self.tags['imageType'] = "0008,0008"
- self.tags['contentTime'] = "0008,0033"
- self.tags['triggerTime'] = "0018,1060"
- self.tags['diffusionGradientOrientation'] = "0018,9089"
- self.tags['imageOrientationPatient'] = "0020,0037"
- self.tags['numberOfFrames'] = "0028,0008"
- self.tags['instanceUID'] = "0008,0018"
- self.tags['windowCenter'] = "0028,1050"
- self.tags['windowWidth'] = "0028,1051"
- self.tags['rows'] = "0028,0010"
- self.tags['columns'] = "0028,0011"
-
- @staticmethod
- def readerApproaches():
- """Available reader implementations. First entry is initial default.
- Note: the settings file stores the index of the user's selected reader
- approach, so if new approaches are added the should go at the
- end of the list.
+ """ ScalarVolume specific interpretation code
"""
- return ["GDCM with DCMTK fallback", "DCMTK", "GDCM", "Archetype"]
- @staticmethod
- def settingsPanelEntry(panel, parent):
- """Create a settings panel entry for this plugin class.
- It is added to the DICOM panel of the application settings
- by the DICOM module.
- """
- formLayout = qt.QFormLayout(parent)
-
- readersComboBox = qt.QComboBox()
- for approach in DICOMScalarVolumePluginClass.readerApproaches():
- readersComboBox.addItem(approach)
- readersComboBox.toolTip = ("Preferred back end. Archetype was used by default in Slicer before June of 2017."
- "Change this setting if data that previously loaded stops working (and report an issue).")
- formLayout.addRow("DICOM reader approach:", readersComboBox)
- panel.registerProperty(
- "DICOM/ScalarVolume/ReaderApproach", readersComboBox,
- "currentIndex", str(qt.SIGNAL("currentIndexChanged(int)")))
-
- importFormatsComboBox = ctk.ctkComboBox()
- importFormatsComboBox.toolTip = ("Enable adding non-linear transform to regularize images acquired irregular geometry:"
- " non-rectilinear grid (such as tilted gantry CT acquisitions) and non-uniform slice spacing."
- " If no regularization is applied then image may appear distorted if it was acquired with irregular geometry.")
- importFormatsComboBox.addItem("default (none)", "default")
- importFormatsComboBox.addItem("none", "none")
- importFormatsComboBox.addItem("apply regularization transform", "transform")
- # in the future additional option, such as "resample" may be added
- importFormatsComboBox.currentIndex = 0
- formLayout.addRow("Acquisition geometry regularization:", importFormatsComboBox)
- panel.registerProperty(
- "DICOM/ScalarVolume/AcquisitionGeometryRegularization", importFormatsComboBox,
- "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")),
- "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart)
- # DICOM examination settings are cached so we need to restart to make sure changes take effect
-
- allowLoadingByTimeCheckBox = qt.QCheckBox()
- allowLoadingByTimeCheckBox.toolTip = ("Offer loading of individual slices or group of slices"
- " that were acquired at a specific time (content or trigger time)."
- " If this option is enabled then a large number of loadable items may be displayed in the Advanced section of DICOM browser.")
- formLayout.addRow("Allow loading subseries by time:", allowLoadingByTimeCheckBox)
- allowLoadingByTimeMapper = ctk.ctkBooleanMapper(allowLoadingByTimeCheckBox, "checked", str(qt.SIGNAL("toggled(bool)")))
- panel.registerProperty(
- "DICOM/ScalarVolume/AllowLoadingByTime", allowLoadingByTimeMapper,
- "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")),
- "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart)
- # DICOM examination settings are cached so we need to restart to make sure changes take effect
-
- @staticmethod
- def compareVolumeNodes(volumeNode1, volumeNode2):
- """
- Given two mrml volume nodes, return true of the numpy arrays have identical data
- and other metadata matches. Returns empty string on match, otherwise
- a string with a list of differences separated by newlines.
- """
- volumesLogic = slicer.modules.volumes.logic()
- comparison = ""
- comparison += volumesLogic.CompareVolumeGeometry(volumeNode1, volumeNode2)
- image1 = volumeNode1.GetImageData()
- image2 = volumeNode2.GetImageData()
- if image1.GetScalarType() != image2.GetScalarType():
- comparison += f"First volume is {image1.GetScalarTypeAsString()}, but second is {image2.GetScalarTypeAsString()}"
- array1 = slicer.util.array(volumeNode1.GetID())
- array2 = slicer.util.array(volumeNode2.GetID())
- if not numpy.all(array1 == array2):
- comparison += "Pixel data mismatch\n"
- return comparison
-
- def acquisitionGeometryRegularizationEnabled(self):
- settings = qt.QSettings()
- return (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform")
-
- def allowLoadingByTime(self):
- settings = qt.QSettings()
- return (int(settings.value("DICOM/ScalarVolume/AllowLoadingByTime", "0")) != 0)
-
- def examineForImport(self, fileLists):
- """ Returns a sorted list of DICOMLoadable instances
- corresponding to ways of interpreting the
- fileLists parameter (list of file lists).
- """
- loadables = []
- for files in fileLists:
- cachedLoadables = self.getCachedLoadables(files)
- if cachedLoadables:
- loadables += cachedLoadables
- else:
- loadablesForFiles = self.examineFiles(files)
- loadables += loadablesForFiles
- self.cacheLoadables(files, loadablesForFiles)
-
- # sort the loadables by series number if possible
- loadables.sort(key=cmp_to_key(lambda x, y: self.seriesSorter(x, y)))
-
- return loadables
-
- def cleanNodeName(self, value):
- cleanValue = value
- cleanValue = cleanValue.replace("|", "-")
- cleanValue = cleanValue.replace("/", "-")
- cleanValue = cleanValue.replace("\\", "-")
- cleanValue = cleanValue.replace("*", "(star)")
- cleanValue = cleanValue.replace("\\", "-")
- return cleanValue
-
- def examineFiles(self, files):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- files parameter.
- """
-
- seriesUID = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesUID'])
- seriesName = self.defaultSeriesNodeName(seriesUID)
-
- # default loadable includes all files for series
- allFilesLoadable = DICOMLoadable()
- allFilesLoadable.files = files
- allFilesLoadable.name = self.cleanNodeName(seriesName)
- allFilesLoadable.tooltip = "%d files, first file: %s" % (len(allFilesLoadable.files), allFilesLoadable.files[0])
- allFilesLoadable.selected = True
- # add it to the list of loadables later, if pixel data is available in at least one file
-
- # make subseries volumes based on tag differences
- subseriesTags = [
- "seriesInstanceUID",
- "acquisitionNumber",
- # GE volume viewer and Siemens Axiom CBCT systems put an overview (localizer) slice and all the reconstructed slices
- # in one series, using two different image types. Splitting based on image type allows loading of these volumes
- # (loading the series without localizer).
- "imageType",
- "imageOrientationPatient",
- "diffusionGradientOrientation",
- ]
-
- if self.allowLoadingByTime():
- subseriesTags.append("contentTime")
- subseriesTags.append("triggerTime")
-
- # Values for these tags will only be enumerated (value itself will not be part of the loadable name)
- # because the vale itself is usually too long and complicated to be displayed to users
- subseriesTagsToEnumerateValues = [
- "seriesInstanceUID",
- "imageOrientationPatient",
- "diffusionGradientOrientation",
- ]
-
- #
- # first, look for subseries within this series
- # - build a list of files for each unique value
- # of each tag
- #
- subseriesFiles = {}
- subseriesValues = {}
- for file in allFilesLoadable.files:
- # check for subseries values
- for tag in subseriesTags:
- value = slicer.dicomDatabase.fileValue(file, self.tags[tag])
- value = value.replace(",", "_") # remove commas so it can be used as an index
- if tag not in subseriesValues:
- subseriesValues[tag] = []
- if not subseriesValues[tag].__contains__(value):
- subseriesValues[tag].append(value)
- if (tag, value) not in subseriesFiles:
- subseriesFiles[tag, value] = []
- subseriesFiles[tag, value].append(file)
-
- loadables = []
-
- # Pixel data is available, so add the default loadable to the output
- loadables.append(allFilesLoadable)
-
- #
- # second, for any tags that have more than one value, create a new
- # virtual series
- #
- subseriesCount = 0
- # List of loadables that look like subseries that contain the full series except a single frame
- probableLocalizerFreeLoadables = []
- for tag in subseriesTags:
- if len(subseriesValues[tag]) > 1:
- subseriesCount += 1
- for valueIndex, value in enumerate(subseriesValues[tag]):
- # default loadable includes all files for series
- loadable = DICOMLoadable()
- loadable.files = subseriesFiles[tag, value]
- # value can be a long string (and it will be used for generating node name)
- # therefore use just an index instead
- if tag in subseriesTagsToEnumerateValues:
- loadable.name = seriesName + " - %s %d" % (tag, valueIndex + 1)
- else:
- loadable.name = seriesName + f" - {tag} {value}"
- loadable.name = self.cleanNodeName(loadable.name)
- loadable.tooltip = "%d files, grouped by %s = %s. First file: %s. %s = %s" % (len(loadable.files), tag, value, loadable.files[0], tag, value)
- loadable.selected = False
- loadables.append(loadable)
- if len(subseriesValues[tag]) == 2:
- otherValue = subseriesValues[tag][1 - valueIndex]
- if len(subseriesFiles[tag, value]) > 1 and len(subseriesFiles[tag, otherValue]) == 1:
- # this looks like a subseries without a localizer image
- probableLocalizerFreeLoadables.append(loadable)
-
- # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading)
- # also remove DICOM SEG, since it is not handled by ITK readers
- newLoadables = []
- for loadable in loadables:
- newFiles = []
- excludedLoadable = False
- for file in loadable.files:
- if slicer.dicomDatabase.fileValueExists(file, self.tags['pixelData']):
- newFiles.append(file)
- if slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.66.4':
- excludedLoadable = True
- if 'DICOMSegmentationPlugin' not in slicer.modules.dicomPlugins:
- logging.warning('Please install Quantitative Reporting extension to enable loading of DICOM Segmentation objects')
- elif slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.481.3':
- excludedLoadable = True
- if 'DicomRtImportExportPlugin' not in slicer.modules.dicomPlugins:
- logging.warning('Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects')
- if len(newFiles) > 0 and not excludedLoadable:
- loadable.files = newFiles
- loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(newFiles[0], self.tags['photometricInterpretation']))
- newLoadables.append(loadable)
- elif excludedLoadable:
- continue
- else:
- # here all files in have no pixel data, so they might be
- # secondary capture images which will read, so let's pass
- # them through with a warning and low confidence
- loadable.warning += "There is no pixel data attribute for the DICOM objects, but they might be readable as secondary capture images. "
- loadable.confidence = 0.2
- loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['photometricInterpretation']))
- newLoadables.append(loadable)
- loadables = newLoadables
+ def __init__(self, epsilon=0.01):
+ super().__init__()
+ self.loadType = "Scalar Volume"
+ self.epsilon = epsilon
+ self.acquisitionModeling = None
+ self.defaultStudyID = 'SLICER10001' # TODO: What should be the new study ID?
+
+ self.tags['sopClassUID'] = "0008,0016"
+ self.tags['photometricInterpretation'] = "0028,0004"
+ self.tags['seriesDescription'] = "0008,103e"
+ self.tags['seriesUID'] = "0020,000E"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['position'] = "0020,0032"
+ self.tags['orientation'] = "0020,0037"
+ self.tags['pixelData'] = "7fe0,0010"
+ self.tags['seriesInstanceUID'] = "0020,000E"
+ self.tags['acquisitionNumber'] = "0020,0012"
+ self.tags['imageType'] = "0008,0008"
+ self.tags['contentTime'] = "0008,0033"
+ self.tags['triggerTime'] = "0018,1060"
+ self.tags['diffusionGradientOrientation'] = "0018,9089"
+ self.tags['imageOrientationPatient'] = "0020,0037"
+ self.tags['numberOfFrames'] = "0028,0008"
+ self.tags['instanceUID'] = "0008,0018"
+ self.tags['windowCenter'] = "0028,1050"
+ self.tags['windowWidth'] = "0028,1051"
+ self.tags['rows'] = "0028,0010"
+ self.tags['columns'] = "0028,0011"
+
+ @staticmethod
+ def readerApproaches():
+ """Available reader implementations. First entry is initial default.
+ Note: the settings file stores the index of the user's selected reader
+ approach, so if new approaches are added the should go at the
+ end of the list.
+ """
+ return ["GDCM with DCMTK fallback", "DCMTK", "GDCM", "Archetype"]
+
+ @staticmethod
+ def settingsPanelEntry(panel, parent):
+ """Create a settings panel entry for this plugin class.
+ It is added to the DICOM panel of the application settings
+ by the DICOM module.
+ """
+ formLayout = qt.QFormLayout(parent)
+
+ readersComboBox = qt.QComboBox()
+ for approach in DICOMScalarVolumePluginClass.readerApproaches():
+ readersComboBox.addItem(approach)
+ readersComboBox.toolTip = ("Preferred back end. Archetype was used by default in Slicer before June of 2017."
+ "Change this setting if data that previously loaded stops working (and report an issue).")
+ formLayout.addRow("DICOM reader approach:", readersComboBox)
+ panel.registerProperty(
+ "DICOM/ScalarVolume/ReaderApproach", readersComboBox,
+ "currentIndex", str(qt.SIGNAL("currentIndexChanged(int)")))
+
+ importFormatsComboBox = ctk.ctkComboBox()
+ importFormatsComboBox.toolTip = ("Enable adding non-linear transform to regularize images acquired irregular geometry:"
+ " non-rectilinear grid (such as tilted gantry CT acquisitions) and non-uniform slice spacing."
+ " If no regularization is applied then image may appear distorted if it was acquired with irregular geometry.")
+ importFormatsComboBox.addItem("default (none)", "default")
+ importFormatsComboBox.addItem("none", "none")
+ importFormatsComboBox.addItem("apply regularization transform", "transform")
+ # in the future additional option, such as "resample" may be added
+ importFormatsComboBox.currentIndex = 0
+ formLayout.addRow("Acquisition geometry regularization:", importFormatsComboBox)
+ panel.registerProperty(
+ "DICOM/ScalarVolume/AcquisitionGeometryRegularization", importFormatsComboBox,
+ "currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")),
+ "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart)
+ # DICOM examination settings are cached so we need to restart to make sure changes take effect
+
+ allowLoadingByTimeCheckBox = qt.QCheckBox()
+ allowLoadingByTimeCheckBox.toolTip = ("Offer loading of individual slices or group of slices"
+ " that were acquired at a specific time (content or trigger time)."
+ " If this option is enabled then a large number of loadable items may be displayed in the Advanced section of DICOM browser.")
+ formLayout.addRow("Allow loading subseries by time:", allowLoadingByTimeCheckBox)
+ allowLoadingByTimeMapper = ctk.ctkBooleanMapper(allowLoadingByTimeCheckBox, "checked", str(qt.SIGNAL("toggled(bool)")))
+ panel.registerProperty(
+ "DICOM/ScalarVolume/AllowLoadingByTime", allowLoadingByTimeMapper,
+ "valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")),
+ "DICOM examination settings", ctk.ctkSettingsPanel.OptionRequireRestart)
+ # DICOM examination settings are cached so we need to restart to make sure changes take effect
+
+ @staticmethod
+ def compareVolumeNodes(volumeNode1, volumeNode2):
+ """
+ Given two mrml volume nodes, return true of the numpy arrays have identical data
+ and other metadata matches. Returns empty string on match, otherwise
+ a string with a list of differences separated by newlines.
+ """
+ volumesLogic = slicer.modules.volumes.logic()
+ comparison = ""
+ comparison += volumesLogic.CompareVolumeGeometry(volumeNode1, volumeNode2)
+ image1 = volumeNode1.GetImageData()
+ image2 = volumeNode2.GetImageData()
+ if image1.GetScalarType() != image2.GetScalarType():
+ comparison += f"First volume is {image1.GetScalarTypeAsString()}, but second is {image2.GetScalarTypeAsString()}"
+ array1 = slicer.util.array(volumeNode1.GetID())
+ array2 = slicer.util.array(volumeNode2.GetID())
+ if not numpy.all(array1 == array2):
+ comparison += "Pixel data mismatch\n"
+ return comparison
+
+ def acquisitionGeometryRegularizationEnabled(self):
+ settings = qt.QSettings()
+ return (settings.value("DICOM/ScalarVolume/AcquisitionGeometryRegularization", "default") == "transform")
+
+ def allowLoadingByTime(self):
+ settings = qt.QSettings()
+ return (int(settings.value("DICOM/ScalarVolume/AllowLoadingByTime", "0")) != 0)
+
+ def examineForImport(self, fileLists):
+ """ Returns a sorted list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ fileLists parameter (list of file lists).
+ """
+ loadables = []
+ for files in fileLists:
+ cachedLoadables = self.getCachedLoadables(files)
+ if cachedLoadables:
+ loadables += cachedLoadables
+ else:
+ loadablesForFiles = self.examineFiles(files)
+ loadables += loadablesForFiles
+ self.cacheLoadables(files, loadablesForFiles)
+
+ # sort the loadables by series number if possible
+ loadables.sort(key=cmp_to_key(lambda x, y: self.seriesSorter(x, y)))
+
+ return loadables
+
+ def cleanNodeName(self, value):
+ cleanValue = value
+ cleanValue = cleanValue.replace("|", "-")
+ cleanValue = cleanValue.replace("/", "-")
+ cleanValue = cleanValue.replace("\\", "-")
+ cleanValue = cleanValue.replace("*", "(star)")
+ cleanValue = cleanValue.replace("\\", "-")
+ return cleanValue
+
+ def examineFiles(self, files):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ files parameter.
+ """
+
+ seriesUID = slicer.dicomDatabase.fileValue(files[0], self.tags['seriesUID'])
+ seriesName = self.defaultSeriesNodeName(seriesUID)
+
+ # default loadable includes all files for series
+ allFilesLoadable = DICOMLoadable()
+ allFilesLoadable.files = files
+ allFilesLoadable.name = self.cleanNodeName(seriesName)
+ allFilesLoadable.tooltip = "%d files, first file: %s" % (len(allFilesLoadable.files), allFilesLoadable.files[0])
+ allFilesLoadable.selected = True
+ # add it to the list of loadables later, if pixel data is available in at least one file
+
+ # make subseries volumes based on tag differences
+ subseriesTags = [
+ "seriesInstanceUID",
+ "acquisitionNumber",
+ # GE volume viewer and Siemens Axiom CBCT systems put an overview (localizer) slice and all the reconstructed slices
+ # in one series, using two different image types. Splitting based on image type allows loading of these volumes
+ # (loading the series without localizer).
+ "imageType",
+ "imageOrientationPatient",
+ "diffusionGradientOrientation",
+ ]
+
+ if self.allowLoadingByTime():
+ subseriesTags.append("contentTime")
+ subseriesTags.append("triggerTime")
+
+ # Values for these tags will only be enumerated (value itself will not be part of the loadable name)
+ # because the vale itself is usually too long and complicated to be displayed to users
+ subseriesTagsToEnumerateValues = [
+ "seriesInstanceUID",
+ "imageOrientationPatient",
+ "diffusionGradientOrientation",
+ ]
+
+ #
+ # first, look for subseries within this series
+ # - build a list of files for each unique value
+ # of each tag
+ #
+ subseriesFiles = {}
+ subseriesValues = {}
+ for file in allFilesLoadable.files:
+ # check for subseries values
+ for tag in subseriesTags:
+ value = slicer.dicomDatabase.fileValue(file, self.tags[tag])
+ value = value.replace(",", "_") # remove commas so it can be used as an index
+ if tag not in subseriesValues:
+ subseriesValues[tag] = []
+ if not subseriesValues[tag].__contains__(value):
+ subseriesValues[tag].append(value)
+ if (tag, value) not in subseriesFiles:
+ subseriesFiles[tag, value] = []
+ subseriesFiles[tag, value].append(file)
+
+ loadables = []
+
+ # Pixel data is available, so add the default loadable to the output
+ loadables.append(allFilesLoadable)
+
+ #
+ # second, for any tags that have more than one value, create a new
+ # virtual series
+ #
+ subseriesCount = 0
+ # List of loadables that look like subseries that contain the full series except a single frame
+ probableLocalizerFreeLoadables = []
+ for tag in subseriesTags:
+ if len(subseriesValues[tag]) > 1:
+ subseriesCount += 1
+ for valueIndex, value in enumerate(subseriesValues[tag]):
+ # default loadable includes all files for series
+ loadable = DICOMLoadable()
+ loadable.files = subseriesFiles[tag, value]
+ # value can be a long string (and it will be used for generating node name)
+ # therefore use just an index instead
+ if tag in subseriesTagsToEnumerateValues:
+ loadable.name = seriesName + " - %s %d" % (tag, valueIndex + 1)
+ else:
+ loadable.name = seriesName + f" - {tag} {value}"
+ loadable.name = self.cleanNodeName(loadable.name)
+ loadable.tooltip = "%d files, grouped by %s = %s. First file: %s. %s = %s" % (len(loadable.files), tag, value, loadable.files[0], tag, value)
+ loadable.selected = False
+ loadables.append(loadable)
+ if len(subseriesValues[tag]) == 2:
+ otherValue = subseriesValues[tag][1 - valueIndex]
+ if len(subseriesFiles[tag, value]) > 1 and len(subseriesFiles[tag, otherValue]) == 1:
+ # this looks like a subseries without a localizer image
+ probableLocalizerFreeLoadables.append(loadable)
+
+ # remove any files from loadables that don't have pixel data (no point sending them to ITK for reading)
+ # also remove DICOM SEG, since it is not handled by ITK readers
+ newLoadables = []
+ for loadable in loadables:
+ newFiles = []
+ excludedLoadable = False
+ for file in loadable.files:
+ if slicer.dicomDatabase.fileValueExists(file, self.tags['pixelData']):
+ newFiles.append(file)
+ if slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.66.4':
+ excludedLoadable = True
+ if 'DICOMSegmentationPlugin' not in slicer.modules.dicomPlugins:
+ logging.warning('Please install Quantitative Reporting extension to enable loading of DICOM Segmentation objects')
+ elif slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID']) == '1.2.840.10008.5.1.4.1.1.481.3':
+ excludedLoadable = True
+ if 'DicomRtImportExportPlugin' not in slicer.modules.dicomPlugins:
+ logging.warning('Please install SlicerRT extension to enable loading of DICOM RT Structure Set objects')
+ if len(newFiles) > 0 and not excludedLoadable:
+ loadable.files = newFiles
+ loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(newFiles[0], self.tags['photometricInterpretation']))
+ newLoadables.append(loadable)
+ elif excludedLoadable:
+ continue
+ else:
+ # here all files in have no pixel data, so they might be
+ # secondary capture images which will read, so let's pass
+ # them through with a warning and low confidence
+ loadable.warning += "There is no pixel data attribute for the DICOM objects, but they might be readable as secondary capture images. "
+ loadable.confidence = 0.2
+ loadable.grayscale = ('MONOCHROME' in slicer.dicomDatabase.fileValue(loadable.files[0], self.tags['photometricInterpretation']))
+ newLoadables.append(loadable)
+ loadables = newLoadables
+
+ #
+ # now for each series and subseries, sort the images
+ # by position and check for consistency
+ # then adjust confidence values based on warnings
+ #
+ for loadable in loadables:
+ loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon)
+
+ loadablesBetterThanAllFiles = []
+ if allFilesLoadable.warning != "":
+ for probableLocalizerFreeLoadable in probableLocalizerFreeLoadables:
+ if probableLocalizerFreeLoadable.warning == "":
+ # localizer-free loadables are better then all files, if they don't have warning
+ loadablesBetterThanAllFiles.append(probableLocalizerFreeLoadable)
+ if not loadablesBetterThanAllFiles and subseriesCount == 1:
+ # there was a sorting warning and
+ # only one kind of subseries, so it's probably correct
+ # to have lower confidence in the default all-files version.
+ for loadable in loadables:
+ if loadable != allFilesLoadable and loadable.warning == "":
+ loadablesBetterThanAllFiles.append(loadable)
+
+ # if there are loadables that are clearly better then all files, then use those (otherwise use all files loadable)
+ preferredLoadables = loadablesBetterThanAllFiles if loadablesBetterThanAllFiles else [allFilesLoadable]
+ # reduce confidence and deselect all non-preferred loadables
+ for loadable in loadables:
+ if loadable in preferredLoadables:
+ loadable.selected = True
+ else:
+ loadable.selected = False
+ if loadable.confidence > .45:
+ loadable.confidence = .45
+
+ return loadables
+
+ def seriesSorter(self, x, y):
+ """ returns -1, 0, 1 for sorting of strings like: "400: series description"
+ Works for DICOMLoadable or other objects with name attribute
+ """
+ if not (hasattr(x, 'name') and hasattr(y, 'name')):
+ return 0
+ xName = x.name
+ yName = y.name
+ try:
+ xNumber = int(xName[:xName.index(':')])
+ yNumber = int(yName[:yName.index(':')])
+ except ValueError:
+ return 0
+ cmp = xNumber - yNumber
+ return cmp
#
- # now for each series and subseries, sort the images
- # by position and check for consistency
- # then adjust confidence values based on warnings
+ # different ways to load a set of dicom files:
+ # - Logic: relies on the same loading mechanism used
+ # by the File->Add Data dialog in the Slicer GUI.
+ # This uses vtkITK under the hood with GDCM as
+ # the default loader.
+ # - DCMTK: explicitly uses the DCMTKImageIO
+ # - GDCM: explicitly uses the GDCMImageIO
#
- for loadable in loadables:
- loadable.files, distances, loadable.warning = DICOMUtils.getSortedImageFiles(loadable.files, self.epsilon)
-
- loadablesBetterThanAllFiles = []
- if allFilesLoadable.warning != "":
- for probableLocalizerFreeLoadable in probableLocalizerFreeLoadables:
- if probableLocalizerFreeLoadable.warning == "":
- # localizer-free loadables are better then all files, if they don't have warning
- loadablesBetterThanAllFiles.append(probableLocalizerFreeLoadable)
- if not loadablesBetterThanAllFiles and subseriesCount == 1:
- # there was a sorting warning and
- # only one kind of subseries, so it's probably correct
- # to have lower confidence in the default all-files version.
- for loadable in loadables:
- if loadable != allFilesLoadable and loadable.warning == "":
- loadablesBetterThanAllFiles.append(loadable)
-
- # if there are loadables that are clearly better then all files, then use those (otherwise use all files loadable)
- preferredLoadables = loadablesBetterThanAllFiles if loadablesBetterThanAllFiles else [allFilesLoadable]
- # reduce confidence and deselect all non-preferred loadables
- for loadable in loadables:
- if loadable in preferredLoadables:
- loadable.selected = True
- else:
- loadable.selected = False
- if loadable.confidence > .45:
- loadable.confidence = .45
-
- return loadables
-
- def seriesSorter(self, x, y):
- """ returns -1, 0, 1 for sorting of strings like: "400: series description"
- Works for DICOMLoadable or other objects with name attribute
- """
- if not (hasattr(x, 'name') and hasattr(y, 'name')):
- return 0
- xName = x.name
- yName = y.name
- try:
- xNumber = int(xName[:xName.index(':')])
- yNumber = int(yName[:yName.index(':')])
- except ValueError:
- return 0
- cmp = xNumber - yNumber
- return cmp
-
- #
- # different ways to load a set of dicom files:
- # - Logic: relies on the same loading mechanism used
- # by the File->Add Data dialog in the Slicer GUI.
- # This uses vtkITK under the hood with GDCM as
- # the default loader.
- # - DCMTK: explicitly uses the DCMTKImageIO
- # - GDCM: explicitly uses the GDCMImageIO
- #
-
- def loadFilesWithArchetype(self, files, name):
- """Load files in the traditional Slicer manner
- using the volume logic helper class
- and the vtkITK archetype helper code
- """
- fileList = vtk.vtkStringArray()
- for f in files:
- fileList.InsertNextValue(f)
- volumesLogic = slicer.modules.volumes.logic()
- return(volumesLogic.AddArchetypeScalarVolume(files[0], name, 0, fileList))
-
- def loadFilesWithSeriesReader(self, imageIOName, files, name, grayscale=True):
- """ Explicitly use the named imageIO to perform the loading
- """
- if grayscale:
- reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
- else:
- reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
- reader.SetArchetype(files[0])
- for f in files:
- reader.AddFileName(f)
- reader.SetSingleFile(0)
- reader.SetOutputScalarTypeToNative()
- reader.SetDesiredCoordinateOrientationToNative()
- reader.SetUseNativeOriginOn()
- if imageIOName == "GDCM":
- reader.SetDICOMImageIOApproachToGDCM()
- elif imageIOName == "DCMTK":
- reader.SetDICOMImageIOApproachToDCMTK()
- else:
- raise Exception("Invalid imageIOName of %s" % imageIOName)
- logging.info("Loading with imageIOName: %s" % imageIOName)
- reader.Update()
-
- slicer.modules.reader = reader
- if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
- errorStrings = (imageIOName, vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()))
- logging.error("Could not read scalar volume using %s approach. Error is: %s" % errorStrings)
- return
-
- imageChangeInformation = vtk.vtkImageChangeInformation()
- imageChangeInformation.SetInputConnection(reader.GetOutputPort())
- imageChangeInformation.SetOutputSpacing(1, 1, 1)
- imageChangeInformation.SetOutputOrigin(0, 0, 0)
- imageChangeInformation.Update()
-
- name = slicer.mrmlScene.GenerateUniqueName(name)
- if grayscale:
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", name)
- else:
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", name)
- volumeNode.SetAndObserveImageData(imageChangeInformation.GetOutputDataObject(0))
- slicer.vtkMRMLVolumeArchetypeStorageNode.SetMetaDataDictionaryFromReader(volumeNode, reader)
- volumeNode.SetRASToIJKMatrix(reader.GetRasToIjkMatrix())
- volumeNode.CreateDefaultDisplayNodes()
-
- slicer.modules.DICOMInstance.reader = reader
- slicer.modules.DICOMInstance.imageChangeInformation = imageChangeInformation
-
- return(volumeNode)
-
- def setVolumeNodeProperties(self, volumeNode, loadable):
- """After the scalar volume has been loaded, populate the node
- attributes and display node with values extracted from the dicom instances
- """
- if volumeNode:
- #
- # create subject hierarchy items for the loaded series
- #
- self.addSeriesInSubjectHierarchy(loadable, volumeNode)
-
- #
- # add list of DICOM instance UIDs to the volume node
- # corresponding to the loaded files
- #
- instanceUIDs = ""
- for file in loadable.files:
- uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID'])
- if uid == "":
- uid = "Unknown"
- instanceUIDs += uid + " "
- instanceUIDs = instanceUIDs[:-1] # strip last space
- volumeNode.SetAttribute("DICOM.instanceUIDs", instanceUIDs)
-
- # Choose a file in the middle of the series as representative frame,
- # because that is more likely to contain the object of interest than the first or last frame.
- # This is important for example for getting a relevant window/center value for the series.
- file = loadable.files[int(len(loadable.files) / 2)]
-
- #
- # automatically select the volume to display
- #
- appLogic = slicer.app.applicationLogic()
- selNode = appLogic.GetSelectionNode()
- selNode.SetActiveVolumeID(volumeNode.GetID())
- appLogic.PropagateVolumeSelection()
-
- #
- # apply window/level from DICOM if available (the first pair that is found)
- # Note: There can be multiple presets (multiplicity 1-n) in the standard [1]. We have
- # a way to put these into the display node [2], so they can be selected in the Volumes
- # module.
- # [1] https://medical.nema.org/medical/dicom/current/output/html/part06.html
- # [2] https://github.com/Slicer/Slicer/blob/3bfa2fc2b310d41c09b7a9e8f8f6c4f43d3bd1e2/Libs/MRML/Core/vtkMRMLScalarVolumeDisplayNode.h#L172
- #
- try:
- windowCenter = float(slicer.dicomDatabase.fileValue(file, self.tags['windowCenter']))
- windowWidth = float(slicer.dicomDatabase.fileValue(file, self.tags['windowWidth']))
- displayNode = volumeNode.GetDisplayNode()
- if displayNode:
- logging.info('Window/level found in DICOM tags (center=' + str(windowCenter) + ', width=' + str(windowWidth) + ') has been applied to volume ' + volumeNode.GetName())
- displayNode.AddWindowLevelPreset(windowWidth, windowCenter)
- displayNode.SetWindowLevelFromPreset(0)
+ def loadFilesWithArchetype(self, files, name):
+ """Load files in the traditional Slicer manner
+ using the volume logic helper class
+ and the vtkITK archetype helper code
+ """
+ fileList = vtk.vtkStringArray()
+ for f in files:
+ fileList.InsertNextValue(f)
+ volumesLogic = slicer.modules.volumes.logic()
+ return(volumesLogic.AddArchetypeScalarVolume(files[0], name, 0, fileList))
+
+ def loadFilesWithSeriesReader(self, imageIOName, files, name, grayscale=True):
+ """ Explicitly use the named imageIO to perform the loading
+ """
+
+ if grayscale:
+ reader = vtkITK.vtkITKArchetypeImageSeriesScalarReader()
else:
- logging.info('No display node: cannot use window/level found in DICOM tags')
- except ValueError:
- pass # DICOM tags cannot be parsed to floating point numbers
-
- sopClassUID = slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID'])
-
- # initialize color lookup table
- modality = self.mapSOPClassUIDToModality(sopClassUID)
- if modality == "PT":
- displayNode = volumeNode.GetDisplayNode()
- if displayNode:
- displayNode.SetAndObserveColorNodeID(slicer.modules.colors.logic().GetPETColorNodeID(slicer.vtkMRMLPETProceduralColorNode.PETheat))
-
- # initialize quantity and units codes
- (quantity, units) = self.mapSOPClassUIDToDICOMQuantityAndUnits(sopClassUID)
- if quantity is not None:
- volumeNode.SetVoxelValueQuantity(quantity)
- if units is not None:
- volumeNode.SetVoxelValueUnits(units)
-
- def loadWithMultipleLoaders(self, loadable):
- """Load using multiple paths (for testing)
- """
- volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name + "-archetype")
- self.setVolumeNodeProperties(volumeNode, loadable)
- volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name + "-gdcm", loadable.grayscale)
- self.setVolumeNodeProperties(volumeNode, loadable)
- volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name + "-dcmtk", loadable.grayscale)
- self.setVolumeNodeProperties(volumeNode, loadable)
-
- return volumeNode
-
- def load(self, loadable, readerApproach=None):
- """Load the select as a scalar volume using desired approach
- """
- # first, determine which reader approach the user prefers
- if not readerApproach:
- readerIndex = slicer.util.settingsValue('DICOM/ScalarVolume/ReaderApproach', 0, converter=int)
- readerApproach = DICOMScalarVolumePluginClass.readerApproaches()[readerIndex]
- # second, try to load with the selected approach
- if readerApproach == "Archetype":
- volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name)
- elif readerApproach == "GDCM with DCMTK fallback":
- volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name, loadable.grayscale)
- if not volumeNode:
- volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name, loadable.grayscale)
- else:
- volumeNode = self.loadFilesWithSeriesReader(readerApproach, loadable.files, loadable.name, loadable.grayscale)
- # third, transfer data from the dicom instances into the appropriate Slicer data containers
- self.setVolumeNodeProperties(volumeNode, loadable)
-
- # examine the loaded volume and if needed create a new transform
- # that makes the loaded volume match the DICOM coordinates of
- # the individual frames. Save the class instance so external
- # code such as the DICOMReaders test can introspect to validate.
-
- if volumeNode:
- self.acquisitionModeling = self.AcquisitionModeling()
- self.acquisitionModeling.createAcquisitionTransform(volumeNode,
- addAcquisitionTransformIfNeeded=self.acquisitionGeometryRegularizationEnabled())
-
- return volumeNode
-
- def examineForExport(self, subjectHierarchyItemID):
- """Return a list of DICOMExportable instances that describe the
- available techniques that this plugin offers to convert MRML
- data into DICOM data
- """
- # cannot export if there is no data node or the data node is not a volume
- shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- dataNode = shn.GetItemDataNode(subjectHierarchyItemID)
- if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'):
- return []
-
- # Define basic properties of the exportable
- exportable = slicer.qSlicerDICOMExportable()
- exportable.name = self.loadType
- exportable.tooltip = "Creates a series of DICOM files from scalar volumes"
- exportable.subjectHierarchyItemID = subjectHierarchyItemID
- exportable.pluginClass = self.__module__
- exportable.confidence = 0.5 # There could be more specialized volume types
-
- # Define required tags and default values
- exportable.setTag('SeriesDescription', 'No series description')
- exportable.setTag('Modality', 'CT')
- exportable.setTag('Manufacturer', 'Unknown manufacturer')
- exportable.setTag('Model', 'Unknown model')
- exportable.setTag('StudyDate', '')
- exportable.setTag('StudyTime', '')
- exportable.setTag('StudyInstanceUID', '')
- exportable.setTag('SeriesDate', '')
- exportable.setTag('SeriesTime', '')
- exportable.setTag('ContentDate', '')
- exportable.setTag('ContentTime', '')
- exportable.setTag('SeriesNumber', '1')
- exportable.setTag('SeriesInstanceUID', '')
- exportable.setTag('FrameOfReferenceUID', '')
-
- return [exportable]
-
- def export(self, exportables):
- for exportable in exportables:
- # Get volume node to export
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- if shNode is None:
- error = "Invalid subject hierarchy"
- logging.error(error)
- return error
- volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID)
- if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'):
- error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported"
- logging.error(error)
- return error
-
- # Get output directory and create a subdirectory. This is necessary
- # to avoid overwriting the files in case of multiple exportables, as
- # naming of the DICOM files is static
- directoryName = 'ScalarVolume_' + str(exportable.subjectHierarchyItemID)
- directoryDir = qt.QDir(exportable.directory)
- directoryDir.mkpath(directoryName)
- directoryDir.cd(directoryName)
- directory = directoryDir.absolutePath()
- logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory)
-
- # Get study and patient items
- studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID)
- if not studyItemID:
- error = "Unable to get study for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
- patientItemID = shNode.GetItemParent(studyItemID)
- if not patientItemID:
- error = "Unable to get patient for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
-
- # Assemble tags dictionary for volume export
- tags = {}
- tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
- tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
- tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
- tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
- tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
- tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName())
- tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
- tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
- tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
- tags['Modality'] = exportable.tag('Modality')
- tags['Manufacturer'] = exportable.tag('Manufacturer')
- tags['Model'] = exportable.tag('Model')
- tags['Series Description'] = exportable.tag('SeriesDescription')
- tags['Series Number'] = exportable.tag('SeriesNumber')
- tags['Series Date'] = exportable.tag('SeriesDate')
- tags['Series Time'] = exportable.tag('SeriesTime')
- tags['Content Date'] = exportable.tag('ContentDate')
- tags['Content Time'] = exportable.tag('ContentTime')
-
- tags['Study Instance UID'] = exportable.tag('StudyInstanceUID')
- tags['Series Instance UID'] = exportable.tag('SeriesInstanceUID')
- tags['Frame of Reference UID'] = exportable.tag('FrameOfReferenceUID')
-
- # Generate any missing but required UIDs
- if not tags['Study Instance UID']:
- import pydicom as dicom
- tags['Study Instance UID'] = dicom.uid.generate_uid()
- if not tags['Series Instance UID']:
- import pydicom as dicom
- tags['Series Instance UID'] = dicom.uid.generate_uid()
- if not tags['Frame of Reference UID']:
- import pydicom as dicom
- tags['Frame of Reference UID'] = dicom.uid.generate_uid()
-
- # Use the default Study ID if none is specified
- if not tags['Study ID']:
- tags['Study ID'] = self.defaultStudyID
-
- # Validate tags
- if tags['Modality'] == "":
- error = "Empty modality for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
-
- seriesInstanceUID = tags['Series Instance UID']
- if seriesInstanceUID:
- # Make sure we don't use a series instance UID that already exists (it would mix in more slices into an existing series,
- # which is very unlikely that users would want).
- db = slicer.dicomDatabase
- studyInstanceUID = db.studyForSeries(seriesInstanceUID)
- if studyInstanceUID:
- # This seriesInstanceUID is already found in the database
- if len(seriesInstanceUID) > 25:
- seriesInstanceUID = seriesInstanceUID[:20] + "..."
- error = f"A series already exists in the database by SeriesInstanceUID {seriesInstanceUID}."
- logging.error(error)
- return error
-
- # TODO: more tag checks
-
- # Perform export
- exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory)
- if not exporter.export():
- return "Creating DICOM files from scalar volume failed. See the application log for details."
-
- # Success
- return ""
-
- class AcquisitionModeling:
- """Code for representing and analyzing acquisition properties in slicer
- This is an internal class of the DICOMScalarVolumePluginClass so that
- it can be used here and from within the DICOMReaders test.
-
- TODO: This code work on legacy single frame DICOM images that have position and orientation
- flags in each instance (not on multiframe with per-frame positions).
- """
-
- def __init__(self, cornerEpsilon=1e-3, zeroEpsilon=1e-6):
- """cornerEpsilon sets the threshold for the amount of difference between the
- vtkITK generated volume geometry vs the DICOM geometry. Any spatial dimension with
- a difference larger than cornerEpsilon will trigger the addition of a grid transform.
- Any difference less than zeroEpsilon is assumed to be numerical error.
- """
- self.cornerEpsilon = cornerEpsilon
- self.zeroEpsilon = zeroEpsilon
-
- def gridTransformFromCorners(self, volumeNode, sourceCorners, targetCorners):
- """Create a grid transform that maps between the current and the desired corners.
- """
- # sanity check
- columns, rows, slices = volumeNode.GetImageData().GetDimensions()
- cornerShape = (slices, 2, 2, 3)
- if not (sourceCorners.shape == cornerShape and targetCorners.shape == cornerShape):
- raise Exception("Corner shapes do not match volume dimensions %s, %s, %s" %
- (sourceCorners.shape, targetCorners.shape, cornerShape))
-
- # create the grid transform node
- gridTransform = slicer.vtkMRMLGridTransformNode()
- gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform'))
- slicer.mrmlScene.AddNode(gridTransform)
-
- # place grid transform in the same subject hierarchy folder as the volume node
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode))
- shNode.SetItemParent(shNode.GetItemByDataNode(gridTransform), volumeParentItemId)
-
- # create a grid transform with one vector at the corner of each slice
- # the transform is in the same space and orientation as the volume node
- gridImage = vtk.vtkImageData()
- gridImage.SetOrigin(*volumeNode.GetOrigin())
- gridImage.SetDimensions(2, 2, slices)
- sourceSpacing = volumeNode.GetSpacing()
- gridImage.SetSpacing(sourceSpacing[0] * columns, sourceSpacing[1] * rows, sourceSpacing[2])
- gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3)
- transform = slicer.vtkOrientedGridTransform()
- directionMatrix = vtk.vtkMatrix4x4()
- volumeNode.GetIJKToRASDirectionMatrix(directionMatrix)
- transform.SetGridDirectionMatrix(directionMatrix)
- transform.SetDisplacementGridData(gridImage)
- gridTransform.SetAndObserveTransformToParent(transform)
- volumeNode.SetAndObserveTransformNodeID(gridTransform.GetID())
-
- # populate the grid so that each corner of each slice
- # is mapped from the source corner to the target corner
- displacements = slicer.util.arrayFromGridTransform(gridTransform)
- for sliceIndex in range(slices):
- for row in range(2):
- for column in range(2):
- displacements[sliceIndex][row][column] = targetCorners[sliceIndex][row][column] - sourceCorners[sliceIndex][row][column]
-
- def sliceCornersFromDICOM(self, volumeNode):
- """Calculate the RAS position of each of the four corners of each
- slice of a volume node based on the dicom headers
-
- Note: PixelSpacing is row spacing followed by column spacing [1] (i.e. vertical then horizontal)
- while ImageOrientationPatient is row cosines then column cosines [2] (i.e. horizontal then vertical).
- [1] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_10.7.1.1
- [2] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.2
- """
- spacingTag = "0028,0030"
- positionTag = "0020,0032"
- orientationTag = "0020,0037"
-
- columns, rows, slices = volumeNode.GetImageData().GetDimensions()
- corners = numpy.zeros(shape=[slices, 2, 2, 3])
- instanceUIDsAttribute = volumeNode.GetAttribute('DICOM.instanceUIDs')
- uids = instanceUIDsAttribute.split() if instanceUIDsAttribute else []
- if len(uids) != slices:
- # There is no uid for each slice, so most likely all frames are in a single file
- # or maybe there is a problem with the sequence
- logging.warning("Cannot get DICOM slice positions for volume " + volumeNode.GetName())
- return None
- for sliceIndex in range(slices):
- uid = uids[sliceIndex]
- # get slice geometry from instance
- positionString = slicer.dicomDatabase.instanceValue(uid, positionTag)
- orientationString = slicer.dicomDatabase.instanceValue(uid, orientationTag)
- spacingString = slicer.dicomDatabase.instanceValue(uid, spacingTag)
- if positionString == "" or orientationString == "" or spacingString == "":
- logging.warning('No geometry information available for DICOM data, skipping corner calculations')
- return None
-
- position = numpy.array(list(map(float, positionString.split('\\'))))
- orientation = list(map(float, orientationString.split('\\')))
- rowOrientation = numpy.array(orientation[:3])
- columnOrientation = numpy.array(orientation[3:])
- spacing = numpy.array(list(map(float, spacingString.split('\\'))))
- # map from LPS to RAS
- lpsToRAS = numpy.array([-1, -1, 1])
- position *= lpsToRAS
- rowOrientation *= lpsToRAS
- columnOrientation *= lpsToRAS
- rowVector = columns * spacing[1] * rowOrientation # dicom PixelSpacing is between rows first, then columns
- columnVector = rows * spacing[0] * columnOrientation
- # apply the transform to the four corners
- for column in range(2):
- for row in range(2):
- corners[sliceIndex][row][column] = position
- corners[sliceIndex][row][column] += column * rowVector
- corners[sliceIndex][row][column] += row * columnVector
- return corners
-
- def sliceCornersFromIJKToRAS(self, volumeNode):
- """Calculate the RAS position of each of the four corners of each
- slice of a volume node based on the ijkToRAS matrix of the volume node
- """
- ijkToRAS = vtk.vtkMatrix4x4()
- volumeNode.GetIJKToRASMatrix(ijkToRAS)
- columns, rows, slices = volumeNode.GetImageData().GetDimensions()
- corners = numpy.zeros(shape=[slices, 2, 2, 3])
- for sliceIndex in range(slices):
- for column in range(2):
- for row in range(2):
- corners[sliceIndex][row][column] = numpy.array(ijkToRAS.MultiplyPoint([column * columns, row * rows, sliceIndex, 1])[:3])
- return corners
-
- def cornersToWorld(self, volumeNode, corners):
- """Map corners through the volumeNodes transform to world
- This can be used to confirm that an acquisition transform has correctly
- mapped the slice corners to match the dicom acquisition.
- """
- columns, rows, slices = volumeNode.GetImageData().GetDimensions()
- worldCorners = numpy.zeros(shape=[slices, 2, 2, 3])
- for slice in range(slices):
- for row in range(2):
- for column in range(2):
- volumeNode.TransformPointToWorld(corners[slice, row, column], worldCorners[slice, row, column])
- return worldCorners
-
- def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded=True):
- """Creates the actual transform if needed.
- Slice corners are cached for inpection by tests
- """
- self.originalCorners = self.sliceCornersFromIJKToRAS(volumeNode)
- self.targetCorners = self.sliceCornersFromDICOM(volumeNode)
- if self.originalCorners is None or self.targetCorners is None:
- # can't create transform without corner information
- return
- maxError = (abs(self.originalCorners - self.targetCorners)).max()
-
- if maxError > self.cornerEpsilon:
- warningText = f"Irregular volume geometry detected (maximum error of {maxError:g} mm is above tolerance threshold of {self.cornerEpsilon:g} mm)."
- if addAcquisitionTransformIfNeeded:
- logging.warning(warningText + " Adding acquisition transform to regularize geometry.")
- self.gridTransformFromCorners(volumeNode, self.originalCorners, self.targetCorners)
- self.fixedCorners = self.cornersToWorld(volumeNode, self.originalCorners)
- if not numpy.allclose(self.fixedCorners, self.targetCorners):
- raise Exception("Acquisition transform didn't fix slice corners!")
+ reader = vtkITK.vtkITKArchetypeImageSeriesVectorReaderFile()
+ reader.SetArchetype(files[0])
+ for f in files:
+ reader.AddFileName(f)
+ reader.SetSingleFile(0)
+ reader.SetOutputScalarTypeToNative()
+ reader.SetDesiredCoordinateOrientationToNative()
+ reader.SetUseNativeOriginOn()
+ if imageIOName == "GDCM":
+ reader.SetDICOMImageIOApproachToGDCM()
+ elif imageIOName == "DCMTK":
+ reader.SetDICOMImageIOApproachToDCMTK()
+ else:
+ raise Exception("Invalid imageIOName of %s" % imageIOName)
+ logging.info("Loading with imageIOName: %s" % imageIOName)
+ reader.Update()
+
+ slicer.modules.reader = reader
+ if reader.GetErrorCode() != vtk.vtkErrorCode.NoError:
+ errorStrings = (imageIOName, vtk.vtkErrorCode.GetStringFromErrorCode(reader.GetErrorCode()))
+ logging.error("Could not read scalar volume using %s approach. Error is: %s" % errorStrings)
+ return
+
+ imageChangeInformation = vtk.vtkImageChangeInformation()
+ imageChangeInformation.SetInputConnection(reader.GetOutputPort())
+ imageChangeInformation.SetOutputSpacing(1, 1, 1)
+ imageChangeInformation.SetOutputOrigin(0, 0, 0)
+ imageChangeInformation.Update()
+
+ name = slicer.mrmlScene.GenerateUniqueName(name)
+ if grayscale:
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", name)
else:
- logging.warning(warningText + " Regularization transform is not added, as the option is disabled.")
- elif maxError > 0 and maxError > self.zeroEpsilon:
- logging.debug("Irregular volume geometry detected, but maximum error is within tolerance" +
- f" (maximum error of {maxError:g} mm, tolerance threshold is {self.cornerEpsilon:g} mm).")
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode", name)
+ volumeNode.SetAndObserveImageData(imageChangeInformation.GetOutputDataObject(0))
+ slicer.vtkMRMLVolumeArchetypeStorageNode.SetMetaDataDictionaryFromReader(volumeNode, reader)
+ volumeNode.SetRASToIJKMatrix(reader.GetRasToIjkMatrix())
+ volumeNode.CreateDefaultDisplayNodes()
+
+ slicer.modules.DICOMInstance.reader = reader
+ slicer.modules.DICOMInstance.imageChangeInformation = imageChangeInformation
+
+ return(volumeNode)
+
+ def setVolumeNodeProperties(self, volumeNode, loadable):
+ """After the scalar volume has been loaded, populate the node
+ attributes and display node with values extracted from the dicom instances
+ """
+ if volumeNode:
+ #
+ # create subject hierarchy items for the loaded series
+ #
+ self.addSeriesInSubjectHierarchy(loadable, volumeNode)
+
+ #
+ # add list of DICOM instance UIDs to the volume node
+ # corresponding to the loaded files
+ #
+ instanceUIDs = ""
+ for file in loadable.files:
+ uid = slicer.dicomDatabase.fileValue(file, self.tags['instanceUID'])
+ if uid == "":
+ uid = "Unknown"
+ instanceUIDs += uid + " "
+ instanceUIDs = instanceUIDs[:-1] # strip last space
+ volumeNode.SetAttribute("DICOM.instanceUIDs", instanceUIDs)
+
+ # Choose a file in the middle of the series as representative frame,
+ # because that is more likely to contain the object of interest than the first or last frame.
+ # This is important for example for getting a relevant window/center value for the series.
+ file = loadable.files[int(len(loadable.files) / 2)]
+
+ #
+ # automatically select the volume to display
+ #
+ appLogic = slicer.app.applicationLogic()
+ selNode = appLogic.GetSelectionNode()
+ selNode.SetActiveVolumeID(volumeNode.GetID())
+ appLogic.PropagateVolumeSelection()
+
+ #
+ # apply window/level from DICOM if available (the first pair that is found)
+ # Note: There can be multiple presets (multiplicity 1-n) in the standard [1]. We have
+ # a way to put these into the display node [2], so they can be selected in the Volumes
+ # module.
+ # [1] https://medical.nema.org/medical/dicom/current/output/html/part06.html
+ # [2] https://github.com/Slicer/Slicer/blob/3bfa2fc2b310d41c09b7a9e8f8f6c4f43d3bd1e2/Libs/MRML/Core/vtkMRMLScalarVolumeDisplayNode.h#L172
+ #
+ try:
+ windowCenter = float(slicer.dicomDatabase.fileValue(file, self.tags['windowCenter']))
+ windowWidth = float(slicer.dicomDatabase.fileValue(file, self.tags['windowWidth']))
+ displayNode = volumeNode.GetDisplayNode()
+ if displayNode:
+ logging.info('Window/level found in DICOM tags (center=' + str(windowCenter) + ', width=' + str(windowWidth) + ') has been applied to volume ' + volumeNode.GetName())
+ displayNode.AddWindowLevelPreset(windowWidth, windowCenter)
+ displayNode.SetWindowLevelFromPreset(0)
+ else:
+ logging.info('No display node: cannot use window/level found in DICOM tags')
+ except ValueError:
+ pass # DICOM tags cannot be parsed to floating point numbers
+
+ sopClassUID = slicer.dicomDatabase.fileValue(file, self.tags['sopClassUID'])
+
+ # initialize color lookup table
+ modality = self.mapSOPClassUIDToModality(sopClassUID)
+ if modality == "PT":
+ displayNode = volumeNode.GetDisplayNode()
+ if displayNode:
+ displayNode.SetAndObserveColorNodeID(slicer.modules.colors.logic().GetPETColorNodeID(slicer.vtkMRMLPETProceduralColorNode.PETheat))
+
+ # initialize quantity and units codes
+ (quantity, units) = self.mapSOPClassUIDToDICOMQuantityAndUnits(sopClassUID)
+ if quantity is not None:
+ volumeNode.SetVoxelValueQuantity(quantity)
+ if units is not None:
+ volumeNode.SetVoxelValueUnits(units)
+
+ def loadWithMultipleLoaders(self, loadable):
+ """Load using multiple paths (for testing)
+ """
+ volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name + "-archetype")
+ self.setVolumeNodeProperties(volumeNode, loadable)
+ volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name + "-gdcm", loadable.grayscale)
+ self.setVolumeNodeProperties(volumeNode, loadable)
+ volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name + "-dcmtk", loadable.grayscale)
+ self.setVolumeNodeProperties(volumeNode, loadable)
+
+ return volumeNode
+
+ def load(self, loadable, readerApproach=None):
+ """Load the select as a scalar volume using desired approach
+ """
+ # first, determine which reader approach the user prefers
+ if not readerApproach:
+ readerIndex = slicer.util.settingsValue('DICOM/ScalarVolume/ReaderApproach', 0, converter=int)
+ readerApproach = DICOMScalarVolumePluginClass.readerApproaches()[readerIndex]
+ # second, try to load with the selected approach
+ if readerApproach == "Archetype":
+ volumeNode = self.loadFilesWithArchetype(loadable.files, loadable.name)
+ elif readerApproach == "GDCM with DCMTK fallback":
+ volumeNode = self.loadFilesWithSeriesReader("GDCM", loadable.files, loadable.name, loadable.grayscale)
+ if not volumeNode:
+ volumeNode = self.loadFilesWithSeriesReader("DCMTK", loadable.files, loadable.name, loadable.grayscale)
+ else:
+ volumeNode = self.loadFilesWithSeriesReader(readerApproach, loadable.files, loadable.name, loadable.grayscale)
+ # third, transfer data from the dicom instances into the appropriate Slicer data containers
+ self.setVolumeNodeProperties(volumeNode, loadable)
+
+ # examine the loaded volume and if needed create a new transform
+ # that makes the loaded volume match the DICOM coordinates of
+ # the individual frames. Save the class instance so external
+ # code such as the DICOMReaders test can introspect to validate.
+
+ if volumeNode:
+ self.acquisitionModeling = self.AcquisitionModeling()
+ self.acquisitionModeling.createAcquisitionTransform(volumeNode,
+ addAcquisitionTransformIfNeeded=self.acquisitionGeometryRegularizationEnabled())
+
+ return volumeNode
+
+ def examineForExport(self, subjectHierarchyItemID):
+ """Return a list of DICOMExportable instances that describe the
+ available techniques that this plugin offers to convert MRML
+ data into DICOM data
+ """
+ # cannot export if there is no data node or the data node is not a volume
+ shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ dataNode = shn.GetItemDataNode(subjectHierarchyItemID)
+ if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'):
+ return []
+
+ # Define basic properties of the exportable
+ exportable = slicer.qSlicerDICOMExportable()
+ exportable.name = self.loadType
+ exportable.tooltip = "Creates a series of DICOM files from scalar volumes"
+ exportable.subjectHierarchyItemID = subjectHierarchyItemID
+ exportable.pluginClass = self.__module__
+ exportable.confidence = 0.5 # There could be more specialized volume types
+
+ # Define required tags and default values
+ exportable.setTag('SeriesDescription', 'No series description')
+ exportable.setTag('Modality', 'CT')
+ exportable.setTag('Manufacturer', 'Unknown manufacturer')
+ exportable.setTag('Model', 'Unknown model')
+ exportable.setTag('StudyDate', '')
+ exportable.setTag('StudyTime', '')
+ exportable.setTag('StudyInstanceUID', '')
+ exportable.setTag('SeriesDate', '')
+ exportable.setTag('SeriesTime', '')
+ exportable.setTag('ContentDate', '')
+ exportable.setTag('ContentTime', '')
+ exportable.setTag('SeriesNumber', '1')
+ exportable.setTag('SeriesInstanceUID', '')
+ exportable.setTag('FrameOfReferenceUID', '')
+
+ return [exportable]
+
+ def export(self, exportables):
+ for exportable in exportables:
+ # Get volume node to export
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ if shNode is None:
+ error = "Invalid subject hierarchy"
+ logging.error(error)
+ return error
+ volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID)
+ if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'):
+ error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported"
+ logging.error(error)
+ return error
+
+ # Get output directory and create a subdirectory. This is necessary
+ # to avoid overwriting the files in case of multiple exportables, as
+ # naming of the DICOM files is static
+ directoryName = 'ScalarVolume_' + str(exportable.subjectHierarchyItemID)
+ directoryDir = qt.QDir(exportable.directory)
+ directoryDir.mkpath(directoryName)
+ directoryDir.cd(directoryName)
+ directory = directoryDir.absolutePath()
+ logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory)
+
+ # Get study and patient items
+ studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID)
+ if not studyItemID:
+ error = "Unable to get study for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+ patientItemID = shNode.GetItemParent(studyItemID)
+ if not patientItemID:
+ error = "Unable to get patient for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+
+ # Assemble tags dictionary for volume export
+ tags = {}
+ tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
+ tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
+ tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
+ tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
+ tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
+ tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName())
+ tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
+ tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
+ tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
+ tags['Modality'] = exportable.tag('Modality')
+ tags['Manufacturer'] = exportable.tag('Manufacturer')
+ tags['Model'] = exportable.tag('Model')
+ tags['Series Description'] = exportable.tag('SeriesDescription')
+ tags['Series Number'] = exportable.tag('SeriesNumber')
+ tags['Series Date'] = exportable.tag('SeriesDate')
+ tags['Series Time'] = exportable.tag('SeriesTime')
+ tags['Content Date'] = exportable.tag('ContentDate')
+ tags['Content Time'] = exportable.tag('ContentTime')
+
+ tags['Study Instance UID'] = exportable.tag('StudyInstanceUID')
+ tags['Series Instance UID'] = exportable.tag('SeriesInstanceUID')
+ tags['Frame of Reference UID'] = exportable.tag('FrameOfReferenceUID')
+
+ # Generate any missing but required UIDs
+ if not tags['Study Instance UID']:
+ import pydicom as dicom
+ tags['Study Instance UID'] = dicom.uid.generate_uid()
+ if not tags['Series Instance UID']:
+ import pydicom as dicom
+ tags['Series Instance UID'] = dicom.uid.generate_uid()
+ if not tags['Frame of Reference UID']:
+ import pydicom as dicom
+ tags['Frame of Reference UID'] = dicom.uid.generate_uid()
+
+ # Use the default Study ID if none is specified
+ if not tags['Study ID']:
+ tags['Study ID'] = self.defaultStudyID
+
+ # Validate tags
+ if tags['Modality'] == "":
+ error = "Empty modality for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+
+ seriesInstanceUID = tags['Series Instance UID']
+ if seriesInstanceUID:
+ # Make sure we don't use a series instance UID that already exists (it would mix in more slices into an existing series,
+ # which is very unlikely that users would want).
+ db = slicer.dicomDatabase
+ studyInstanceUID = db.studyForSeries(seriesInstanceUID)
+ if studyInstanceUID:
+ # This seriesInstanceUID is already found in the database
+ if len(seriesInstanceUID) > 25:
+ seriesInstanceUID = seriesInstanceUID[:20] + "..."
+ error = f"A series already exists in the database by SeriesInstanceUID {seriesInstanceUID}."
+ logging.error(error)
+ return error
+
+ # TODO: more tag checks
+
+ # Perform export
+ exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory)
+ if not exporter.export():
+ return "Creating DICOM files from scalar volume failed. See the application log for details."
+
+ # Success
+ return ""
+
+ class AcquisitionModeling:
+ """Code for representing and analyzing acquisition properties in slicer
+ This is an internal class of the DICOMScalarVolumePluginClass so that
+ it can be used here and from within the DICOMReaders test.
+
+ TODO: This code work on legacy single frame DICOM images that have position and orientation
+ flags in each instance (not on multiframe with per-frame positions).
+ """
+
+ def __init__(self, cornerEpsilon=1e-3, zeroEpsilon=1e-6):
+ """cornerEpsilon sets the threshold for the amount of difference between the
+ vtkITK generated volume geometry vs the DICOM geometry. Any spatial dimension with
+ a difference larger than cornerEpsilon will trigger the addition of a grid transform.
+ Any difference less than zeroEpsilon is assumed to be numerical error.
+ """
+ self.cornerEpsilon = cornerEpsilon
+ self.zeroEpsilon = zeroEpsilon
+
+ def gridTransformFromCorners(self, volumeNode, sourceCorners, targetCorners):
+ """Create a grid transform that maps between the current and the desired corners.
+ """
+ # sanity check
+ columns, rows, slices = volumeNode.GetImageData().GetDimensions()
+ cornerShape = (slices, 2, 2, 3)
+ if not (sourceCorners.shape == cornerShape and targetCorners.shape == cornerShape):
+ raise Exception("Corner shapes do not match volume dimensions %s, %s, %s" %
+ (sourceCorners.shape, targetCorners.shape, cornerShape))
+
+ # create the grid transform node
+ gridTransform = slicer.vtkMRMLGridTransformNode()
+ gridTransform.SetName(slicer.mrmlScene.GenerateUniqueName(volumeNode.GetName() + ' acquisition transform'))
+ slicer.mrmlScene.AddNode(gridTransform)
+
+ # place grid transform in the same subject hierarchy folder as the volume node
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ volumeParentItemId = shNode.GetItemParent(shNode.GetItemByDataNode(volumeNode))
+ shNode.SetItemParent(shNode.GetItemByDataNode(gridTransform), volumeParentItemId)
+
+ # create a grid transform with one vector at the corner of each slice
+ # the transform is in the same space and orientation as the volume node
+ gridImage = vtk.vtkImageData()
+ gridImage.SetOrigin(*volumeNode.GetOrigin())
+ gridImage.SetDimensions(2, 2, slices)
+ sourceSpacing = volumeNode.GetSpacing()
+ gridImage.SetSpacing(sourceSpacing[0] * columns, sourceSpacing[1] * rows, sourceSpacing[2])
+ gridImage.AllocateScalars(vtk.VTK_DOUBLE, 3)
+ transform = slicer.vtkOrientedGridTransform()
+ directionMatrix = vtk.vtkMatrix4x4()
+ volumeNode.GetIJKToRASDirectionMatrix(directionMatrix)
+ transform.SetGridDirectionMatrix(directionMatrix)
+ transform.SetDisplacementGridData(gridImage)
+ gridTransform.SetAndObserveTransformToParent(transform)
+ volumeNode.SetAndObserveTransformNodeID(gridTransform.GetID())
+
+ # populate the grid so that each corner of each slice
+ # is mapped from the source corner to the target corner
+ displacements = slicer.util.arrayFromGridTransform(gridTransform)
+ for sliceIndex in range(slices):
+ for row in range(2):
+ for column in range(2):
+ displacements[sliceIndex][row][column] = targetCorners[sliceIndex][row][column] - sourceCorners[sliceIndex][row][column]
+
+ def sliceCornersFromDICOM(self, volumeNode):
+ """Calculate the RAS position of each of the four corners of each
+ slice of a volume node based on the dicom headers
+
+ Note: PixelSpacing is row spacing followed by column spacing [1] (i.e. vertical then horizontal)
+ while ImageOrientationPatient is row cosines then column cosines [2] (i.e. horizontal then vertical).
+ [1] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_10.7.1.1
+ [2] https://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.2
+ """
+ spacingTag = "0028,0030"
+ positionTag = "0020,0032"
+ orientationTag = "0020,0037"
+
+ columns, rows, slices = volumeNode.GetImageData().GetDimensions()
+ corners = numpy.zeros(shape=[slices, 2, 2, 3])
+ instanceUIDsAttribute = volumeNode.GetAttribute('DICOM.instanceUIDs')
+ uids = instanceUIDsAttribute.split() if instanceUIDsAttribute else []
+ if len(uids) != slices:
+ # There is no uid for each slice, so most likely all frames are in a single file
+ # or maybe there is a problem with the sequence
+ logging.warning("Cannot get DICOM slice positions for volume " + volumeNode.GetName())
+ return None
+ for sliceIndex in range(slices):
+ uid = uids[sliceIndex]
+ # get slice geometry from instance
+ positionString = slicer.dicomDatabase.instanceValue(uid, positionTag)
+ orientationString = slicer.dicomDatabase.instanceValue(uid, orientationTag)
+ spacingString = slicer.dicomDatabase.instanceValue(uid, spacingTag)
+ if positionString == "" or orientationString == "" or spacingString == "":
+ logging.warning('No geometry information available for DICOM data, skipping corner calculations')
+ return None
+
+ position = numpy.array(list(map(float, positionString.split('\\'))))
+ orientation = list(map(float, orientationString.split('\\')))
+ rowOrientation = numpy.array(orientation[:3])
+ columnOrientation = numpy.array(orientation[3:])
+ spacing = numpy.array(list(map(float, spacingString.split('\\'))))
+ # map from LPS to RAS
+ lpsToRAS = numpy.array([-1, -1, 1])
+ position *= lpsToRAS
+ rowOrientation *= lpsToRAS
+ columnOrientation *= lpsToRAS
+ rowVector = columns * spacing[1] * rowOrientation # dicom PixelSpacing is between rows first, then columns
+ columnVector = rows * spacing[0] * columnOrientation
+ # apply the transform to the four corners
+ for column in range(2):
+ for row in range(2):
+ corners[sliceIndex][row][column] = position
+ corners[sliceIndex][row][column] += column * rowVector
+ corners[sliceIndex][row][column] += row * columnVector
+ return corners
+
+ def sliceCornersFromIJKToRAS(self, volumeNode):
+ """Calculate the RAS position of each of the four corners of each
+ slice of a volume node based on the ijkToRAS matrix of the volume node
+ """
+ ijkToRAS = vtk.vtkMatrix4x4()
+ volumeNode.GetIJKToRASMatrix(ijkToRAS)
+ columns, rows, slices = volumeNode.GetImageData().GetDimensions()
+ corners = numpy.zeros(shape=[slices, 2, 2, 3])
+ for sliceIndex in range(slices):
+ for column in range(2):
+ for row in range(2):
+ corners[sliceIndex][row][column] = numpy.array(ijkToRAS.MultiplyPoint([column * columns, row * rows, sliceIndex, 1])[:3])
+ return corners
+
+ def cornersToWorld(self, volumeNode, corners):
+ """Map corners through the volumeNodes transform to world
+ This can be used to confirm that an acquisition transform has correctly
+ mapped the slice corners to match the dicom acquisition.
+ """
+ columns, rows, slices = volumeNode.GetImageData().GetDimensions()
+ worldCorners = numpy.zeros(shape=[slices, 2, 2, 3])
+ for slice in range(slices):
+ for row in range(2):
+ for column in range(2):
+ volumeNode.TransformPointToWorld(corners[slice, row, column], worldCorners[slice, row, column])
+ return worldCorners
+
+ def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded=True):
+ """Creates the actual transform if needed.
+ Slice corners are cached for inpection by tests
+ """
+ self.originalCorners = self.sliceCornersFromIJKToRAS(volumeNode)
+ self.targetCorners = self.sliceCornersFromDICOM(volumeNode)
+ if self.originalCorners is None or self.targetCorners is None:
+ # can't create transform without corner information
+ return
+ maxError = (abs(self.originalCorners - self.targetCorners)).max()
+
+ if maxError > self.cornerEpsilon:
+ warningText = f"Irregular volume geometry detected (maximum error of {maxError:g} mm is above tolerance threshold of {self.cornerEpsilon:g} mm)."
+ if addAcquisitionTransformIfNeeded:
+ logging.warning(warningText + " Adding acquisition transform to regularize geometry.")
+ self.gridTransformFromCorners(volumeNode, self.originalCorners, self.targetCorners)
+ self.fixedCorners = self.cornersToWorld(volumeNode, self.originalCorners)
+ if not numpy.allclose(self.fixedCorners, self.targetCorners):
+ raise Exception("Acquisition transform didn't fix slice corners!")
+ else:
+ logging.warning(warningText + " Regularization transform is not added, as the option is disabled.")
+ elif maxError > 0 and maxError > self.zeroEpsilon:
+ logging.debug("Irregular volume geometry detected, but maximum error is within tolerance" +
+ f" (maximum error of {maxError:g} mm, tolerance threshold is {self.cornerEpsilon:g} mm).")
#
@@ -849,34 +849,34 @@ def createAcquisitionTransform(self, volumeNode, addAcquisitionTransformIfNeeded
#
class DICOMScalarVolumePlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM Scalar Volume Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Queen's)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM Scalar Volume Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Queen's)"]
+ parent.helpText = """
Plugin to the DICOM Module to parse and load scalar volumes
from DICOM files.
No module interface here, only in the DICOM module
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This DICOM Plugin was developed by
Steve Pieper, Isomics, Inc.
and was partially funded by NIH grant 3P41RR013218.
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
-
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMScalarVolumePlugin'] = DICOMScalarVolumePluginClass
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
+
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMScalarVolumePlugin'] = DICOMScalarVolumePluginClass
diff --git a/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py
index c5769825dc3..eab648dc128 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMSlicerDataBundlePlugin.py
@@ -16,217 +16,217 @@
#
class DICOMSlicerDataBundlePluginClass(DICOMPlugin):
- """ DICOM import/export plugin for Slicer Scene Bundle
- (MRML scene file embedded in private tag of a DICOM file)
- """
-
- def __init__(self):
- super().__init__()
- self.loadType = "Slicer Data Bundle"
- self.tags['seriesDescription'] = "0008,103e"
- self.tags['candygram'] = "cadb,0010"
- self.tags['zipSize'] = "cadb,1008"
- self.tags['zipData'] = "cadb,1010"
-
- def examineForImport(self, fileLists):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- fileLists parameter.
- """
- loadables = []
- for files in fileLists:
- cachedLoadables = self.getCachedLoadables(files)
- if cachedLoadables:
- loadables += cachedLoadables
- else:
- loadablesForFiles = self.examineFiles(files)
- loadables += loadablesForFiles
- self.cacheLoadables(files, loadablesForFiles)
- return loadables
-
- def examineFiles(self, files):
- """ Returns a list of DICOMLoadable instances
- corresponding to ways of interpreting the
- files parameter.
- Look for the special private creator tags that indicate
- a slicer data bundle
- Note that each data bundle is in a unique series, so
- if 'files' is a list of more than one element, then this
- is not a data bundle.
- """
-
- loadables = []
- if len(files) == 1:
- f = files[0]
- # get the series description to use as base for volume name
- name = slicer.dicomDatabase.fileValue(f, self.tags['seriesDescription'])
- if name == "":
- name = "Unknown"
- candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram'])
-
- if candygramValue:
- # default loadable includes all files for series
- loadable = DICOMLoadable()
- loadable.files = [f]
- loadable.name = name + ' - as Slicer Scene'
- loadable.selected = True
- loadable.tooltip = 'Contains a Slicer scene'
- loadable.confidence = 0.9
- loadables.append(loadable)
- return loadables
-
- def load(self, loadable):
- """Load the selection as a data bundle
- by extracting the embedded zip file and passing it to the application logic
- """
-
- f = loadable.files[0]
-
- try:
- # TODO: this method should work, but not correctly encoded in real tag
- zipSizeString = slicer.dicomDatabase.fileValue(f, self.tags['zipSize'])
- zipSize = int(zipSizeString)
- # instead use this hack where the number is in the creator string
- candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram'])
- zipSize = int(candygramValue.split(' ')[2])
- except ValueError:
- logging.error("Could not get zipSize for %s" % f)
- return False
-
- logging.info('importing file: %s' % f)
- logging.info('size: %d' % zipSize)
-
- # require that the databundle be the last element of the file
- # so we can seek from the end by the size of the zip data
- sceneDir = tempfile.mkdtemp('', 'sceneImport', slicer.app.temporaryPath)
- fp = open(f, 'rb')
-
- # The previous code only works for files with odd number of bits.
- if zipSize % 2 == 0:
- fp.seek(-1 * (zipSize), os.SEEK_END)
- else:
- fp.seek(-1 * (1 + zipSize), os.SEEK_END)
- zipData = fp.read(zipSize)
- fp.close()
-
- # save to a temp zip file
- zipPath = os.path.join(sceneDir, 'scene.zip')
- fp = open(zipPath, 'wb')
- fp.write(zipData)
- fp.close()
-
- logging.info('saved zip file to: %s' % zipPath)
-
- nodesBeforeLoading = slicer.util.getNodes()
-
- # let the scene unpack it and load it
- appLogic = slicer.app.applicationLogic()
- sceneFile = appLogic.OpenSlicerDataBundle(zipPath, sceneDir)
- logging.info("loaded %s" % sceneFile)
-
- # Create subject hierarchy items for the loaded series.
- # In order for the series information are saved in the scene (and subject hierarchy
- # creation does not fail), a "main" data node needs to be selected: the first volume,
- # model, or markups node is used as series node.
- # TODO: Maybe all the nodes containing data could be added under the study, but
- # the DICOM plugins don't support it yet.
- dataNode = None
- nodesAfterLoading = slicer.util.getNodes()
- loadedNodes = [node for node in list(nodesAfterLoading.values()) if
- node not in list(nodesBeforeLoading.values())]
- for node in loadedNodes:
- if node.IsA('vtkMRMLScalarVolumeNode'):
- dataNode = node
- if dataNode is None:
- for node in loadedNodes:
- if node.IsA('vtkMRMLModelNode') and node.GetName() not in ['Red Volume Slice', 'Yellow Volume Slice',
- 'Green Volume Slice']:
- dataNode = node
- break
- if dataNode is None:
- for node in loadedNodes:
- if node.IsA('vtkMRMLMarkupsNode'):
- dataNode = node
- break
- if dataNode is not None:
- self.addSeriesInSubjectHierarchy(loadable, dataNode)
- else:
- logging.warning('Failed to find suitable series node in loaded scene')
-
- return sceneFile != ""
-
- def examineForExport(self, subjectHierarchyItemID):
- """Return a list of DICOMExportable instances that describe the
- available techniques that this plugin offers to convert MRML
- data into DICOM data
+ """ DICOM import/export plugin for Slicer Scene Bundle
+ (MRML scene file embedded in private tag of a DICOM file)
"""
- # Define basic properties of the exportable
- exportable = slicer.qSlicerDICOMExportable()
- exportable.name = "Slicer data bundle"
- exportable.tooltip = "Creates a series that embeds the entire Slicer scene in a private DICOM tag"
- exportable.subjectHierarchyItemID = subjectHierarchyItemID
- exportable.pluginClass = self.__module__
- exportable.confidence = 0.1 # There could be more specialized volume types
-
- # Do not define tags (exportable.setTag) because they would overwrite values in the reference series
-
- return [exportable]
-
- def export(self, exportables):
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- if shNode is None:
- error = "Invalid subject hierarchy"
- logging.error(error)
- return error
- dicomFiles = []
- for exportable in exportables:
- # Find reference series (series that will be modified into a scene data bundle)
- # Get DICOM UID - can be study instance UID or series instance UID
- dicomUid = shNode.GetItemUID(exportable.subjectHierarchyItemID,
- slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName())
- if not dicomUid:
- continue
- # Get series instance UID
- if shNode.GetItemLevel(exportable.subjectHierarchyItemID) == slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy():
- # Study is selected
- seriesInstanceUids = slicer.dicomDatabase.seriesForStudy(dicomUid)
- seriesInstanceUid = seriesInstanceUids[0] if seriesInstanceUids else None
- else:
- # Series is selected
- seriesInstanceUid = dicomUid
- # Get first file of the series
- dicomFiles = slicer.dicomDatabase.filesForSeries(seriesInstanceUid, 1)
- if not dicomFiles:
- continue
- break
- if not dicomFiles:
- error = "Slicer data bundle export failed. No file is found for any of the selected items."
- logging.error(error)
- return error
-
- # Assemble tags dictionary for volume export
- tags = {}
- tags['PatientName'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
- tags['PatientID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
- tags['PatientBirthDate'] = exportable.tag(
- slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
- tags['PatientSex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
- tags['PatientComments'] = exportable.tag(
- slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
-
- tags['StudyDate'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
- tags['StudyTime'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
- tags['StudyDescription'] = exportable.tag(
- slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
-
- # Perform export
- exporter = DICOMExportScene(dicomFiles[0], exportable.directory)
- exporter.optionalTags = tags
- exporter.export()
-
- # Success
- return ""
+ def __init__(self):
+ super().__init__()
+ self.loadType = "Slicer Data Bundle"
+ self.tags['seriesDescription'] = "0008,103e"
+ self.tags['candygram'] = "cadb,0010"
+ self.tags['zipSize'] = "cadb,1008"
+ self.tags['zipData'] = "cadb,1010"
+
+ def examineForImport(self, fileLists):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ fileLists parameter.
+ """
+ loadables = []
+ for files in fileLists:
+ cachedLoadables = self.getCachedLoadables(files)
+ if cachedLoadables:
+ loadables += cachedLoadables
+ else:
+ loadablesForFiles = self.examineFiles(files)
+ loadables += loadablesForFiles
+ self.cacheLoadables(files, loadablesForFiles)
+ return loadables
+
+ def examineFiles(self, files):
+ """ Returns a list of DICOMLoadable instances
+ corresponding to ways of interpreting the
+ files parameter.
+ Look for the special private creator tags that indicate
+ a slicer data bundle
+ Note that each data bundle is in a unique series, so
+ if 'files' is a list of more than one element, then this
+ is not a data bundle.
+ """
+
+ loadables = []
+ if len(files) == 1:
+ f = files[0]
+ # get the series description to use as base for volume name
+ name = slicer.dicomDatabase.fileValue(f, self.tags['seriesDescription'])
+ if name == "":
+ name = "Unknown"
+ candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram'])
+
+ if candygramValue:
+ # default loadable includes all files for series
+ loadable = DICOMLoadable()
+ loadable.files = [f]
+ loadable.name = name + ' - as Slicer Scene'
+ loadable.selected = True
+ loadable.tooltip = 'Contains a Slicer scene'
+ loadable.confidence = 0.9
+ loadables.append(loadable)
+ return loadables
+
+ def load(self, loadable):
+ """Load the selection as a data bundle
+ by extracting the embedded zip file and passing it to the application logic
+ """
+
+ f = loadable.files[0]
+
+ try:
+ # TODO: this method should work, but not correctly encoded in real tag
+ zipSizeString = slicer.dicomDatabase.fileValue(f, self.tags['zipSize'])
+ zipSize = int(zipSizeString)
+ # instead use this hack where the number is in the creator string
+ candygramValue = slicer.dicomDatabase.fileValue(f, self.tags['candygram'])
+ zipSize = int(candygramValue.split(' ')[2])
+ except ValueError:
+ logging.error("Could not get zipSize for %s" % f)
+ return False
+
+ logging.info('importing file: %s' % f)
+ logging.info('size: %d' % zipSize)
+
+ # require that the databundle be the last element of the file
+ # so we can seek from the end by the size of the zip data
+ sceneDir = tempfile.mkdtemp('', 'sceneImport', slicer.app.temporaryPath)
+ fp = open(f, 'rb')
+
+ # The previous code only works for files with odd number of bits.
+ if zipSize % 2 == 0:
+ fp.seek(-1 * (zipSize), os.SEEK_END)
+ else:
+ fp.seek(-1 * (1 + zipSize), os.SEEK_END)
+ zipData = fp.read(zipSize)
+ fp.close()
+
+ # save to a temp zip file
+ zipPath = os.path.join(sceneDir, 'scene.zip')
+ fp = open(zipPath, 'wb')
+ fp.write(zipData)
+ fp.close()
+
+ logging.info('saved zip file to: %s' % zipPath)
+
+ nodesBeforeLoading = slicer.util.getNodes()
+
+ # let the scene unpack it and load it
+ appLogic = slicer.app.applicationLogic()
+ sceneFile = appLogic.OpenSlicerDataBundle(zipPath, sceneDir)
+ logging.info("loaded %s" % sceneFile)
+
+ # Create subject hierarchy items for the loaded series.
+ # In order for the series information are saved in the scene (and subject hierarchy
+ # creation does not fail), a "main" data node needs to be selected: the first volume,
+ # model, or markups node is used as series node.
+ # TODO: Maybe all the nodes containing data could be added under the study, but
+ # the DICOM plugins don't support it yet.
+ dataNode = None
+ nodesAfterLoading = slicer.util.getNodes()
+ loadedNodes = [node for node in list(nodesAfterLoading.values()) if
+ node not in list(nodesBeforeLoading.values())]
+ for node in loadedNodes:
+ if node.IsA('vtkMRMLScalarVolumeNode'):
+ dataNode = node
+ if dataNode is None:
+ for node in loadedNodes:
+ if node.IsA('vtkMRMLModelNode') and node.GetName() not in ['Red Volume Slice', 'Yellow Volume Slice',
+ 'Green Volume Slice']:
+ dataNode = node
+ break
+ if dataNode is None:
+ for node in loadedNodes:
+ if node.IsA('vtkMRMLMarkupsNode'):
+ dataNode = node
+ break
+ if dataNode is not None:
+ self.addSeriesInSubjectHierarchy(loadable, dataNode)
+ else:
+ logging.warning('Failed to find suitable series node in loaded scene')
+
+ return sceneFile != ""
+
+ def examineForExport(self, subjectHierarchyItemID):
+ """Return a list of DICOMExportable instances that describe the
+ available techniques that this plugin offers to convert MRML
+ data into DICOM data
+ """
+
+ # Define basic properties of the exportable
+ exportable = slicer.qSlicerDICOMExportable()
+ exportable.name = "Slicer data bundle"
+ exportable.tooltip = "Creates a series that embeds the entire Slicer scene in a private DICOM tag"
+ exportable.subjectHierarchyItemID = subjectHierarchyItemID
+ exportable.pluginClass = self.__module__
+ exportable.confidence = 0.1 # There could be more specialized volume types
+
+ # Do not define tags (exportable.setTag) because they would overwrite values in the reference series
+
+ return [exportable]
+
+ def export(self, exportables):
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ if shNode is None:
+ error = "Invalid subject hierarchy"
+ logging.error(error)
+ return error
+ dicomFiles = []
+ for exportable in exportables:
+ # Find reference series (series that will be modified into a scene data bundle)
+ # Get DICOM UID - can be study instance UID or series instance UID
+ dicomUid = shNode.GetItemUID(exportable.subjectHierarchyItemID,
+ slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMUIDName())
+ if not dicomUid:
+ continue
+ # Get series instance UID
+ if shNode.GetItemLevel(exportable.subjectHierarchyItemID) == slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMLevelStudy():
+ # Study is selected
+ seriesInstanceUids = slicer.dicomDatabase.seriesForStudy(dicomUid)
+ seriesInstanceUid = seriesInstanceUids[0] if seriesInstanceUids else None
+ else:
+ # Series is selected
+ seriesInstanceUid = dicomUid
+ # Get first file of the series
+ dicomFiles = slicer.dicomDatabase.filesForSeries(seriesInstanceUid, 1)
+ if not dicomFiles:
+ continue
+ break
+ if not dicomFiles:
+ error = "Slicer data bundle export failed. No file is found for any of the selected items."
+ logging.error(error)
+ return error
+
+ # Assemble tags dictionary for volume export
+ tags = {}
+ tags['PatientName'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
+ tags['PatientID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
+ tags['PatientBirthDate'] = exportable.tag(
+ slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
+ tags['PatientSex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
+ tags['PatientComments'] = exportable.tag(
+ slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
+
+ tags['StudyDate'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
+ tags['StudyTime'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
+ tags['StudyDescription'] = exportable.tag(
+ slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
+
+ # Perform export
+ exporter = DICOMExportScene(dicomFiles[0], exportable.directory)
+ exporter.optionalTags = tags
+ exporter.export()
+
+ # Success
+ return ""
#
@@ -234,37 +234,37 @@ def export(self, exportables):
#
class DICOMSlicerDataBundlePlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM Diffusion Volume Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Pixel Medical, Inc.)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM Diffusion Volume Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Steve Pieper (Isomics Inc.), Csaba Pinter (Pixel Medical, Inc.)"]
+ parent.helpText = """
Plugin to the DICOM Module to parse and load diffusion volumes
from DICOM files.
No module interface here, only in the DICOM module
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This DICOM Plugin was developed by
Steve Pieper, Isomics, Inc.
and was partially funded by NIH grant 3P41RR013218.
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMSlicerDataBundlePlugin'] = DICOMSlicerDataBundlePluginClass
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMSlicerDataBundlePlugin'] = DICOMSlicerDataBundlePluginClass
#
@@ -272,15 +272,15 @@ def __init__(self, parent):
#
class DICOMSlicerDataBundleWidget:
- def __init__(self, parent=None):
- self.parent = parent
+ def __init__(self, parent=None):
+ self.parent = parent
- def setup(self):
- # don't display anything for this widget - it will be hidden anyway
- pass
+ def setup(self):
+ # don't display anything for this widget - it will be hidden anyway
+ pass
- def enter(self):
- pass
+ def enter(self):
+ pass
- def exit(self):
- pass
+ def exit(self):
+ pass
diff --git a/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py b/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py
index 3348a8aaeb3..0181e4006fb 100644
--- a/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py
+++ b/Modules/Scripted/DICOMPlugins/DICOMVolumeSequencePlugin.py
@@ -15,249 +15,249 @@
#
class DICOMVolumeSequencePluginClass(DICOMPlugin):
- """ Volume sequence export plugin
- """
-
- def __init__(self):
- super().__init__()
- self.loadType = "Volume Sequence"
-
- self.tags['studyID'] = '0020,0010'
- self.tags['seriesDescription'] = "0008,103e"
- self.tags['seriesUID'] = "0020,000E"
- self.tags['seriesNumber'] = "0020,0011"
- self.tags['seriesDate'] = "0008,0021"
- self.tags['seriesTime'] = "0020,0031"
- self.tags['position'] = "0020,0032"
- self.tags['orientation'] = "0020,0037"
- self.tags['pixelData'] = "7fe0,0010"
- self.tags['seriesInstanceUID'] = "0020,000E"
- self.tags['contentTime'] = "0008,0033"
- self.tags['triggerTime'] = "0018,1060"
- self.tags['diffusionGradientOrientation'] = "0018,9089"
- self.tags['imageOrientationPatient'] = "0020,0037"
- self.tags['numberOfFrames'] = "0028,0008"
- self.tags['instanceUID'] = "0008,0018"
- self.tags['windowCenter'] = "0028,1050"
- self.tags['windowWidth'] = "0028,1051"
- self.tags['classUID'] = "0008,0016"
-
- def getSequenceBrowserNodeForMasterOutputNode(self, masterOutputNode):
- browserNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSequenceBrowserNode')
- browserNodes.UnRegister(None)
- for itemIndex in range(browserNodes.GetNumberOfItems()):
- sequenceBrowserNode = browserNodes.GetItemAsObject(itemIndex)
- if sequenceBrowserNode.GetProxyNode(sequenceBrowserNode.GetMasterSequenceNode()) == masterOutputNode:
- return sequenceBrowserNode
- return None
-
- def examineForExport(self, subjectHierarchyItemID):
- """Return a list of DICOMExportable instances that describe the
- available techniques that this plugin offers to convert MRML
- data into DICOM data
+ """ Volume sequence export plugin
"""
- # Check if setting of DICOM UIDs is supported (if not, then we cannot export to sequence)
- dicomUIDSettingSupported = False
- createDicomSeriesParameterNode = slicer.modules.createdicomseries.cliModuleLogic().CreateNode()
- # CreateNode() factory method incremented the reference count, we decrement now prevent memory leaks
- # (a reference is still kept by createDicomSeriesParameterNode Python variable).
- createDicomSeriesParameterNode.UnRegister(None)
- for groupIndex in range(createDicomSeriesParameterNode.GetNumberOfParameterGroups()):
- if createDicomSeriesParameterNode.GetParameterGroupLabel(groupIndex) == "Unique Identifiers (UIDs)":
- dicomUIDSettingSupported = True
- if not dicomUIDSettingSupported:
- # This version of Slicer does not allow setting DICOM UIDs for export
- return []
-
- # cannot export if there is no data node or the data node is not a volume
- shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- dataNode = shn.GetItemDataNode(subjectHierarchyItemID)
- if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'):
- # not a volume node
- return []
-
- sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(dataNode)
- if not sequenceBrowserNode:
- # this seems to be a simple volume node (not a proxy node of a volume
- # sequence). This plugin only deals with volume sequences.
- return []
-
- sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
- if sequenceItemCount <= 1:
- # this plugin is only relevant if there are multiple items in the sequence
- return []
-
- # Define basic properties of the exportable
- exportable = slicer.qSlicerDICOMExportable()
- exportable.name = self.loadType
- exportable.tooltip = "Creates a series of DICOM files from volume sequences"
- exportable.subjectHierarchyItemID = subjectHierarchyItemID
- exportable.pluginClass = self.__module__
- exportable.confidence = 0.6 # Simple volume has confidence of 0.5, use a slightly higher value here
-
- # Define required tags and default values
- exportable.setTag('SeriesDescription', f'Volume sequence of {sequenceItemCount} frames')
- exportable.setTag('Modality', 'CT')
- exportable.setTag('Manufacturer', 'Unknown manufacturer')
- exportable.setTag('Model', 'Unknown model')
- exportable.setTag('StudyID', '1')
- exportable.setTag('SeriesNumber', '1')
- exportable.setTag('SeriesDate', '')
- exportable.setTag('SeriesTime', '')
-
- return [exportable]
-
- def datetimeFromDicom(self, dt, tm):
- year = 0
- month = 0
- day = 0
- if len(dt) == 8: # YYYYMMDD
- year = int(dt[0:4])
- month = int(dt[4:6])
- day = int(dt[6:8])
- else:
- raise OSError("Invalid DICOM date string: " + tm + " (failed to parse YYYYMMDD)")
-
- hour = 0
- minute = 0
- second = 0
- microsecond = 0
- if len(tm) >= 6:
- try:
- hhmmss = str.split(tm, '.')[0]
- except:
- hhmmss = tm
- try:
- microsecond = int(float('0.' + str.split(tm, '.')[1]) * 1e6)
- except:
+ def __init__(self):
+ super().__init__()
+ self.loadType = "Volume Sequence"
+
+ self.tags['studyID'] = '0020,0010'
+ self.tags['seriesDescription'] = "0008,103e"
+ self.tags['seriesUID'] = "0020,000E"
+ self.tags['seriesNumber'] = "0020,0011"
+ self.tags['seriesDate'] = "0008,0021"
+ self.tags['seriesTime'] = "0020,0031"
+ self.tags['position'] = "0020,0032"
+ self.tags['orientation'] = "0020,0037"
+ self.tags['pixelData'] = "7fe0,0010"
+ self.tags['seriesInstanceUID'] = "0020,000E"
+ self.tags['contentTime'] = "0008,0033"
+ self.tags['triggerTime'] = "0018,1060"
+ self.tags['diffusionGradientOrientation'] = "0018,9089"
+ self.tags['imageOrientationPatient'] = "0020,0037"
+ self.tags['numberOfFrames'] = "0028,0008"
+ self.tags['instanceUID'] = "0008,0018"
+ self.tags['windowCenter'] = "0028,1050"
+ self.tags['windowWidth'] = "0028,1051"
+ self.tags['classUID'] = "0008,0016"
+
+ def getSequenceBrowserNodeForMasterOutputNode(self, masterOutputNode):
+ browserNodes = slicer.mrmlScene.GetNodesByClass('vtkMRMLSequenceBrowserNode')
+ browserNodes.UnRegister(None)
+ for itemIndex in range(browserNodes.GetNumberOfItems()):
+ sequenceBrowserNode = browserNodes.GetItemAsObject(itemIndex)
+ if sequenceBrowserNode.GetProxyNode(sequenceBrowserNode.GetMasterSequenceNode()) == masterOutputNode:
+ return sequenceBrowserNode
+ return None
+
+ def examineForExport(self, subjectHierarchyItemID):
+ """Return a list of DICOMExportable instances that describe the
+ available techniques that this plugin offers to convert MRML
+ data into DICOM data
+ """
+
+ # Check if setting of DICOM UIDs is supported (if not, then we cannot export to sequence)
+ dicomUIDSettingSupported = False
+ createDicomSeriesParameterNode = slicer.modules.createdicomseries.cliModuleLogic().CreateNode()
+ # CreateNode() factory method incremented the reference count, we decrement now prevent memory leaks
+ # (a reference is still kept by createDicomSeriesParameterNode Python variable).
+ createDicomSeriesParameterNode.UnRegister(None)
+ for groupIndex in range(createDicomSeriesParameterNode.GetNumberOfParameterGroups()):
+ if createDicomSeriesParameterNode.GetParameterGroupLabel(groupIndex) == "Unique Identifiers (UIDs)":
+ dicomUIDSettingSupported = True
+ if not dicomUIDSettingSupported:
+ # This version of Slicer does not allow setting DICOM UIDs for export
+ return []
+
+ # cannot export if there is no data node or the data node is not a volume
+ shn = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ dataNode = shn.GetItemDataNode(subjectHierarchyItemID)
+ if dataNode is None or not dataNode.IsA('vtkMRMLScalarVolumeNode'):
+ # not a volume node
+ return []
+
+ sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(dataNode)
+ if not sequenceBrowserNode:
+ # this seems to be a simple volume node (not a proxy node of a volume
+ # sequence). This plugin only deals with volume sequences.
+ return []
+
+ sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
+ if sequenceItemCount <= 1:
+ # this plugin is only relevant if there are multiple items in the sequence
+ return []
+
+ # Define basic properties of the exportable
+ exportable = slicer.qSlicerDICOMExportable()
+ exportable.name = self.loadType
+ exportable.tooltip = "Creates a series of DICOM files from volume sequences"
+ exportable.subjectHierarchyItemID = subjectHierarchyItemID
+ exportable.pluginClass = self.__module__
+ exportable.confidence = 0.6 # Simple volume has confidence of 0.5, use a slightly higher value here
+
+ # Define required tags and default values
+ exportable.setTag('SeriesDescription', f'Volume sequence of {sequenceItemCount} frames')
+ exportable.setTag('Modality', 'CT')
+ exportable.setTag('Manufacturer', 'Unknown manufacturer')
+ exportable.setTag('Model', 'Unknown model')
+ exportable.setTag('StudyID', '1')
+ exportable.setTag('SeriesNumber', '1')
+ exportable.setTag('SeriesDate', '')
+ exportable.setTag('SeriesTime', '')
+
+ return [exportable]
+
+ def datetimeFromDicom(self, dt, tm):
+ year = 0
+ month = 0
+ day = 0
+ if len(dt) == 8: # YYYYMMDD
+ year = int(dt[0:4])
+ month = int(dt[4:6])
+ day = int(dt[6:8])
+ else:
+ raise OSError("Invalid DICOM date string: " + tm + " (failed to parse YYYYMMDD)")
+
+ hour = 0
+ minute = 0
+ second = 0
microsecond = 0
- if len(hhmmss) == 6: # HHMMSS
- hour = int(hhmmss[0:2])
- minute = int(hhmmss[2:4])
- second = int(hhmmss[4:6])
- elif len(hhmmss) == 4: # HHMM
- hour = int(hhmmss[0:2])
- minute = int(hhmmss[2:4])
- elif len(hhmmss) == 2: # HH
- hour = int(hhmmss[0:2])
- else:
- raise OSError("Invalid DICOM time string: " + tm + " (failed to parse HHMMSS)")
-
- import datetime
- return datetime.datetime(year, month, day, hour, minute, second, microsecond)
-
- def export(self, exportables):
- for exportable in exportables:
- # Get volume node to export
- shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
- if shNode is None:
- error = "Invalid subject hierarchy"
- logging.error(error)
- return error
- volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID)
- if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'):
- error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
- logging.error(error)
- return error
-
- sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(volumeNode)
- if not sequenceBrowserNode:
- error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
- logging.error(error)
- return error
-
- volumeSequenceNode = sequenceBrowserNode.GetSequenceNode(volumeNode)
- if not volumeSequenceNode:
- error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
- logging.error(error)
- return error
-
- # Get study and patient items
- studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID)
- if not studyItemID:
- error = "Unable to get study for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
- patientItemID = shNode.GetItemParent(studyItemID)
- if not patientItemID:
- error = "Unable to get patient for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
-
- # Assemble tags dictionary for volume export
-
- tags = {}
- tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
- tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
- tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
- tags['Study Instance UID'] = pydicom.uid.generate_uid()
- tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
- tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
- tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName())
- tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
- tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
- tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
- tags['Modality'] = exportable.tag('Modality')
- tags['Manufacturer'] = exportable.tag('Manufacturer')
- tags['Model'] = exportable.tag('Model')
- tags['Series Description'] = exportable.tag('SeriesDescription')
- tags['Series Number'] = exportable.tag('SeriesNumber')
- tags['Series Date'] = exportable.tag("SeriesDate")
- tags['Series Time'] = exportable.tag("SeriesTime")
- tags['Series Instance UID'] = pydicom.uid.generate_uid()
- tags['Frame of Reference UID'] = pydicom.uid.generate_uid()
-
- # Validate tags
- if tags['Modality'] == "":
- error = "Empty modality for series '" + volumeNode.GetName() + "'"
- logging.error(error)
- return error
- # TODO: more tag checks
-
- sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
- originalSelectedSequenceItemNumber = sequenceBrowserNode.GetSelectedItemNumber()
- masterVolumeNode = sequenceBrowserNode.GetMasterSequenceNode()
-
- # initialize content datetime from series datetime
- contentStartDate = exportable.tag("SeriesDate")
- contentStartTime = exportable.tag("SeriesTime")
- import datetime
- datetimeNow = datetime.datetime.now()
- if not contentStartDate:
- contentStartDate = datetimeNow.strftime("%Y%m%d")
- if not contentStartTime:
- contentStartTime = datetimeNow.strftime("%H%M%S.%f")
- contentStartDatetime = self.datetimeFromDicom(contentStartDate, contentStartTime)
-
- # Get output directory and create a subdirectory. This is necessary
- # to avoid overwriting the files in case of multiple exportables, as
- # naming of the DICOM files is static
- directoryName = 'VolumeSequence_' + str(exportable.subjectHierarchyItemID)
- directoryDir = qt.QDir(exportable.directory)
- directoryDir.mkdir(directoryName)
- directoryDir.cd(directoryName)
- directory = directoryDir.absolutePath()
- logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory)
-
- for sequenceItemIndex in range(sequenceItemCount):
-
- # Switch to next item in the series
- sequenceBrowserNode.SetSelectedItemNumber(sequenceItemIndex)
- slicer.app.processEvents()
- # Compute content date&time
- # TODO: verify that unit in sequence node is "second" (and convert to seconds if not)
- timeOffsetSec = float(masterVolumeNode.GetNthIndexValue(sequenceItemIndex)) - float(masterVolumeNode.GetNthIndexValue(0))
- contentDatetime = contentStartDatetime + datetime.timedelta(seconds=timeOffsetSec)
- tags['Content Date'] = contentDatetime.strftime("%Y%m%d")
- tags['Content Time'] = contentDatetime.strftime("%H%M%S.%f")
- # Perform export
- filenamePrefix = f"IMG_{sequenceItemIndex:04d}_"
- exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory, filenamePrefix)
- exporter.export()
-
- # Success
- return ""
+ if len(tm) >= 6:
+ try:
+ hhmmss = str.split(tm, '.')[0]
+ except:
+ hhmmss = tm
+ try:
+ microsecond = int(float('0.' + str.split(tm, '.')[1]) * 1e6)
+ except:
+ microsecond = 0
+ if len(hhmmss) == 6: # HHMMSS
+ hour = int(hhmmss[0:2])
+ minute = int(hhmmss[2:4])
+ second = int(hhmmss[4:6])
+ elif len(hhmmss) == 4: # HHMM
+ hour = int(hhmmss[0:2])
+ minute = int(hhmmss[2:4])
+ elif len(hhmmss) == 2: # HH
+ hour = int(hhmmss[0:2])
+ else:
+ raise OSError("Invalid DICOM time string: " + tm + " (failed to parse HHMMSS)")
+
+ import datetime
+ return datetime.datetime(year, month, day, hour, minute, second, microsecond)
+
+ def export(self, exportables):
+ for exportable in exportables:
+ # Get volume node to export
+ shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+ if shNode is None:
+ error = "Invalid subject hierarchy"
+ logging.error(error)
+ return error
+ volumeNode = shNode.GetItemDataNode(exportable.subjectHierarchyItemID)
+ if volumeNode is None or not volumeNode.IsA('vtkMRMLScalarVolumeNode'):
+ error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
+ logging.error(error)
+ return error
+
+ sequenceBrowserNode = self.getSequenceBrowserNodeForMasterOutputNode(volumeNode)
+ if not sequenceBrowserNode:
+ error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
+ logging.error(error)
+ return error
+
+ volumeSequenceNode = sequenceBrowserNode.GetSequenceNode(volumeNode)
+ if not volumeSequenceNode:
+ error = "Series '" + shNode.GetItemName(exportable.subjectHierarchyItemID) + "' cannot be exported as volume sequence"
+ logging.error(error)
+ return error
+
+ # Get study and patient items
+ studyItemID = shNode.GetItemParent(exportable.subjectHierarchyItemID)
+ if not studyItemID:
+ error = "Unable to get study for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+ patientItemID = shNode.GetItemParent(studyItemID)
+ if not patientItemID:
+ error = "Unable to get patient for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+
+ # Assemble tags dictionary for volume export
+
+ tags = {}
+ tags['Patient Name'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientNameTagName())
+ tags['Patient ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientIDTagName())
+ tags['Patient Comments'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientCommentsTagName())
+ tags['Study Instance UID'] = pydicom.uid.generate_uid()
+ tags['Patient Birth Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientBirthDateTagName())
+ tags['Patient Sex'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMPatientSexTagName())
+ tags['Study ID'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyIDTagName())
+ tags['Study Date'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDateTagName())
+ tags['Study Time'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyTimeTagName())
+ tags['Study Description'] = exportable.tag(slicer.vtkMRMLSubjectHierarchyConstants.GetDICOMStudyDescriptionTagName())
+ tags['Modality'] = exportable.tag('Modality')
+ tags['Manufacturer'] = exportable.tag('Manufacturer')
+ tags['Model'] = exportable.tag('Model')
+ tags['Series Description'] = exportable.tag('SeriesDescription')
+ tags['Series Number'] = exportable.tag('SeriesNumber')
+ tags['Series Date'] = exportable.tag("SeriesDate")
+ tags['Series Time'] = exportable.tag("SeriesTime")
+ tags['Series Instance UID'] = pydicom.uid.generate_uid()
+ tags['Frame of Reference UID'] = pydicom.uid.generate_uid()
+
+ # Validate tags
+ if tags['Modality'] == "":
+ error = "Empty modality for series '" + volumeNode.GetName() + "'"
+ logging.error(error)
+ return error
+ # TODO: more tag checks
+
+ sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
+ originalSelectedSequenceItemNumber = sequenceBrowserNode.GetSelectedItemNumber()
+ masterVolumeNode = sequenceBrowserNode.GetMasterSequenceNode()
+
+ # initialize content datetime from series datetime
+ contentStartDate = exportable.tag("SeriesDate")
+ contentStartTime = exportable.tag("SeriesTime")
+ import datetime
+ datetimeNow = datetime.datetime.now()
+ if not contentStartDate:
+ contentStartDate = datetimeNow.strftime("%Y%m%d")
+ if not contentStartTime:
+ contentStartTime = datetimeNow.strftime("%H%M%S.%f")
+ contentStartDatetime = self.datetimeFromDicom(contentStartDate, contentStartTime)
+
+ # Get output directory and create a subdirectory. This is necessary
+ # to avoid overwriting the files in case of multiple exportables, as
+ # naming of the DICOM files is static
+ directoryName = 'VolumeSequence_' + str(exportable.subjectHierarchyItemID)
+ directoryDir = qt.QDir(exportable.directory)
+ directoryDir.mkdir(directoryName)
+ directoryDir.cd(directoryName)
+ directory = directoryDir.absolutePath()
+ logging.info("Export scalar volume '" + volumeNode.GetName() + "' to directory " + directory)
+
+ for sequenceItemIndex in range(sequenceItemCount):
+
+ # Switch to next item in the series
+ sequenceBrowserNode.SetSelectedItemNumber(sequenceItemIndex)
+ slicer.app.processEvents()
+ # Compute content date&time
+ # TODO: verify that unit in sequence node is "second" (and convert to seconds if not)
+ timeOffsetSec = float(masterVolumeNode.GetNthIndexValue(sequenceItemIndex)) - float(masterVolumeNode.GetNthIndexValue(0))
+ contentDatetime = contentStartDatetime + datetime.timedelta(seconds=timeOffsetSec)
+ tags['Content Date'] = contentDatetime.strftime("%Y%m%d")
+ tags['Content Time'] = contentDatetime.strftime("%H%M%S.%f")
+ # Perform export
+ filenamePrefix = f"IMG_{sequenceItemIndex:04d}_"
+ exporter = DICOMExportScalarVolume(tags['Study ID'], volumeNode, tags, directory, filenamePrefix)
+ exporter.export()
+
+ # Success
+ return ""
#
@@ -265,31 +265,31 @@ def export(self, exportables):
#
class DICOMVolumeSequencePlugin:
- """
- This class is the 'hook' for slicer to detect and recognize the plugin
- as a loadable scripted module
- """
-
- def __init__(self, parent):
- parent.title = "DICOM Volume Sequence Export Plugin"
- parent.categories = ["Developer Tools.DICOM Plugins"]
- parent.contributors = ["Andras Lasso (PerkLab)"]
- parent.helpText = """
+ """
+ This class is the 'hook' for slicer to detect and recognize the plugin
+ as a loadable scripted module
+ """
+
+ def __init__(self, parent):
+ parent.title = "DICOM Volume Sequence Export Plugin"
+ parent.categories = ["Developer Tools.DICOM Plugins"]
+ parent.contributors = ["Andras Lasso (PerkLab)"]
+ parent.helpText = """
Plugin to the DICOM Module to export volume sequence to DICOM file.
No module interface here, only in the DICOM module.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
Originally developed by Andras Lasso (PekLab).
"""
- # don't show this module - it only appears in the DICOM module
- parent.hidden = True
-
- # Add this extension to the DICOM module's list for discovery when the module
- # is created. Since this module may be discovered before DICOM itself,
- # create the list if it doesn't already exist.
- try:
- slicer.modules.dicomPlugins
- except AttributeError:
- slicer.modules.dicomPlugins = {}
- slicer.modules.dicomPlugins['DICOMVolumeSequencePlugin'] = DICOMVolumeSequencePluginClass
+ # don't show this module - it only appears in the DICOM module
+ parent.hidden = True
+
+ # Add this extension to the DICOM module's list for discovery when the module
+ # is created. Since this module may be discovered before DICOM itself,
+ # create the list if it doesn't already exist.
+ try:
+ slicer.modules.dicomPlugins
+ except AttributeError:
+ slicer.modules.dicomPlugins = {}
+ slicer.modules.dicomPlugins['DICOMVolumeSequencePlugin'] = DICOMVolumeSequencePluginClass
diff --git a/Modules/Scripted/DMRIInstall/DMRIInstall.py b/Modules/Scripted/DMRIInstall/DMRIInstall.py
index adc885ee92c..985f249488d 100644
--- a/Modules/Scripted/DMRIInstall/DMRIInstall.py
+++ b/Modules/Scripted/DMRIInstall/DMRIInstall.py
@@ -12,11 +12,11 @@
#
class DMRIInstall(ScriptedLoadableModule):
- """
- """
+ """
+ """
- helpText = textwrap.dedent(
- """
+ helpText = textwrap.dedent(
+ """
The SlicerDMRI extension provides diffusion-related tools including:
@@ -39,8 +39,8 @@ class DMRIInstall(ScriptedLoadableModule):
https://discourse.slicer.org
""")
- errorText = textwrap.dedent(
- """
+ errorText = textwrap.dedent(
+ """
The SlicerDMRI extension is currently unavailable.
Please try a manual installation via the Extensions Manager,
and contact the Slicer forum at:
@@ -55,23 +55,23 @@ class DMRIInstall(ScriptedLoadableModule):
revision=slicer.app.repositoryRevision,
platform=slicer.app.platform)
- def __init__(self, parent):
+ def __init__(self, parent):
- # Hide this module if SlicerDMRI is already installed
- model = slicer.app.extensionsManagerModel()
- if model.isExtensionInstalled("SlicerDMRI"):
- parent.hidden = True
+ # Hide this module if SlicerDMRI is already installed
+ model = slicer.app.extensionsManagerModel()
+ if model.isExtensionInstalled("SlicerDMRI"):
+ parent.hidden = True
- ScriptedLoadableModule.__init__(self, parent)
+ ScriptedLoadableModule.__init__(self, parent)
- self.parent.categories = ["Diffusion"]
- self.parent.title = "Install Slicer Diffusion Tools (SlicerDMRI)"
- self.parent.dependencies = []
- self.parent.contributors = ["Isaiah Norton (BWH), Lauren O'Donnell (BWH)"]
- self.parent.helpText = DMRIInstall.helpText
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = textwrap.dedent(
- """
+ self.parent.categories = ["Diffusion"]
+ self.parent.title = "Install Slicer Diffusion Tools (SlicerDMRI)"
+ self.parent.dependencies = []
+ self.parent.contributors = ["Isaiah Norton (BWH), Lauren O'Donnell (BWH)"]
+ self.parent.helpText = DMRIInstall.helpText
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = textwrap.dedent(
+ """
SlicerDMRI supported by NIH NCI ITCR U01CA199459 (Open Source Diffusion MRI
Technology For Brain Cancer Research), and made possible by NA-MIC, NAC,
BIRN, NCIGT, and the Slicer Community.
@@ -79,49 +79,49 @@ def __init__(self, parent):
class DMRIInstallWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- self.textBox = ctk.ctkFittedTextBrowser()
- self.textBox.setOpenExternalLinks(True) # Open links in default browser
- self.textBox.setHtml(DMRIInstall.helpText)
- self.parent.layout().addWidget(self.textBox)
-
- #
- # Apply Button
- #
- self.applyButton = qt.QPushButton("Install SlicerDMRI")
- self.applyButton.toolTip = 'Installs the "SlicerDMRI" extension from the Diffusion category.'
- self.applyButton.icon = qt.QIcon(":/Icons/ExtensionDefaultIcon.png")
- self.applyButton.enabled = True
- self.applyButton.connect('clicked()', self.onApply)
- self.parent.layout().addWidget(self.applyButton)
-
- self.parent.layout().addStretch(1)
-
- def onError(self):
- self.applyButton.enabled = False
- self.textBox.setHtml(DMRIInstall.errorText)
- return
-
- def onApply(self):
- emm = slicer.app.extensionsManagerModel()
-
- if emm.isExtensionInstalled("SlicerDMRI"):
- self.textBox.setHtml("SlicerDMRI is already installed.")
- self.applyButton.enabled = False
- return
-
- md = emm.retrieveExtensionMetadataByName("SlicerDMRI")
-
- if not md or 'extension_id' not in md:
- return self.onError()
-
- if emm.downloadAndInstallExtension(md['extension_id']):
- slicer.app.confirmRestart("Restart to complete SlicerDMRI installation?")
- else:
- self.onError()
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ self.textBox = ctk.ctkFittedTextBrowser()
+ self.textBox.setOpenExternalLinks(True) # Open links in default browser
+ self.textBox.setHtml(DMRIInstall.helpText)
+ self.parent.layout().addWidget(self.textBox)
+
+ #
+ # Apply Button
+ #
+ self.applyButton = qt.QPushButton("Install SlicerDMRI")
+ self.applyButton.toolTip = 'Installs the "SlicerDMRI" extension from the Diffusion category.'
+ self.applyButton.icon = qt.QIcon(":/Icons/ExtensionDefaultIcon.png")
+ self.applyButton.enabled = True
+ self.applyButton.connect('clicked()', self.onApply)
+ self.parent.layout().addWidget(self.applyButton)
+
+ self.parent.layout().addStretch(1)
+
+ def onError(self):
+ self.applyButton.enabled = False
+ self.textBox.setHtml(DMRIInstall.errorText)
+ return
+
+ def onApply(self):
+ emm = slicer.app.extensionsManagerModel()
+
+ if emm.isExtensionInstalled("SlicerDMRI"):
+ self.textBox.setHtml("SlicerDMRI is already installed.")
+ self.applyButton.enabled = False
+ return
+
+ md = emm.retrieveExtensionMetadataByName("SlicerDMRI")
+
+ if not md or 'extension_id' not in md:
+ return self.onError()
+
+ if emm.downloadAndInstallExtension(md['extension_id']):
+ slicer.app.confirmRestart("Restart to complete SlicerDMRI installation?")
+ else:
+ self.onError()
diff --git a/Modules/Scripted/DataProbe/DataProbe.py b/Modules/Scripted/DataProbe/DataProbe.py
index 514a81ff17f..178a32b1986 100644
--- a/Modules/Scripted/DataProbe/DataProbe.py
+++ b/Modules/Scripted/DataProbe/DataProbe.py
@@ -15,503 +15,503 @@
#
class DataProbe(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
- parent.title = "DataProbe"
- parent.categories = ["Quantification"]
- parent.contributors = ["Steve Pieper (Isomics)"]
- parent.helpText = """
+ parent.title = "DataProbe"
+ parent.categories = ["Quantification"]
+ parent.contributors = ["Steve Pieper (Isomics)"]
+ parent.helpText = """
The DataProbe module is used to get information about the current RAS position being
indicated by the mouse position.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- parent.acknowledgementText = """This work is supported by NA-MIC, NAC, NCIGT, NIH U24 CA180918 (PIs Kikinis and Fedorov) and the Slicer Community."""
- # TODO: need a DataProbe icon
- # parent.icon = qt.QIcon(':Icons/XLarge/SlicerDownloadMRHead.png')
- self.infoWidget = None
-
- if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
- slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
-
- # Trigger the menu to be added when application has started up
- if not slicer.app.commandOptions().noMainWindow:
- slicer.app.connect("startupCompleted()", self.addView)
-
- def __del__(self):
- if self.infoWidget:
- self.infoWidget.removeObservers()
-
- def addView(self):
- """
- Create the persistent widget shown in the bottom left of the user interface
- Do this in a startupCompleted callback so the rest of the interface is already
- built.
- """
- try:
- mw = slicer.util.mainWindow()
- parent = slicer.util.findChild(mw, "DataProbeCollapsibleWidget")
- except IndexError:
- print("No Data Probe frame - cannot create DataProbe")
- return
- self.infoWidget = DataProbeInfoWidget(parent)
- parent.layout().insertWidget(0, self.infoWidget.frame)
-
- def showZoomedSlice(self, value=False):
- self.showZoomedSlice = value
- if self.infoWidget:
- self.infoWidget.onShowImage(value)
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ parent.acknowledgementText = """This work is supported by NA-MIC, NAC, NCIGT, NIH U24 CA180918 (PIs Kikinis and Fedorov) and the Slicer Community."""
+ # TODO: need a DataProbe icon
+ # parent.icon = qt.QIcon(':Icons/XLarge/SlicerDownloadMRHead.png')
+ self.infoWidget = None
+
+ if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
+ slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
+
+ # Trigger the menu to be added when application has started up
+ if not slicer.app.commandOptions().noMainWindow:
+ slicer.app.connect("startupCompleted()", self.addView)
+
+ def __del__(self):
+ if self.infoWidget:
+ self.infoWidget.removeObservers()
+
+ def addView(self):
+ """
+ Create the persistent widget shown in the bottom left of the user interface
+ Do this in a startupCompleted callback so the rest of the interface is already
+ built.
+ """
+ try:
+ mw = slicer.util.mainWindow()
+ parent = slicer.util.findChild(mw, "DataProbeCollapsibleWidget")
+ except IndexError:
+ print("No Data Probe frame - cannot create DataProbe")
+ return
+ self.infoWidget = DataProbeInfoWidget(parent)
+ parent.layout().insertWidget(0, self.infoWidget.frame)
+
+ def showZoomedSlice(self, value=False):
+ self.showZoomedSlice = value
+ if self.infoWidget:
+ self.infoWidget.onShowImage(value)
class DataProbeInfoWidget:
- def __init__(self, parent=None):
- self.nameSize = 24
-
- self.CrosshairNode = None
- self.CrosshairNodeObserverTag = None
-
- self.frame = qt.QFrame(parent)
- self.frame.setLayout(qt.QVBoxLayout())
- # Set horizontal policy to Ignored to prevent a long segment or volume name making the widget wider.
- # If the module panel made larger then the image viewers would move and the mouse pointer position
- # would change in the image, potentially pointing outside the node with the long name, resulting in the
- # module panel collapsing to the original size, causing an infinite oscillation.
- qSize = qt.QSizePolicy()
- qSize.setHorizontalPolicy(qt.QSizePolicy.Ignored)
- qSize.setVerticalPolicy(qt.QSizePolicy.Preferred)
- self.frame.setSizePolicy(qSize)
-
- modulePath = slicer.modules.dataprobe.path.replace("DataProbe.py", "")
- self.iconsDIR = modulePath + '/Resources/Icons'
-
- self.showImage = False
-
- # Used in _createMagnifiedPixmap()
- self.imageCrop = vtk.vtkExtractVOI()
- self.canvas = vtk.vtkImageCanvasSource2D()
- self.painter = qt.QPainter()
- self.pen = qt.QPen()
-
- self._createSmall()
-
- # Helper class to calculate and display tensor scalars
- self.calculateTensorScalars = CalculateTensorScalars()
-
- # Observe the crosshair node to get the current cursor position
- self.CrosshairNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLCrosshairNode')
- if self.CrosshairNode:
- self.CrosshairNodeObserverTag = self.CrosshairNode.AddObserver(slicer.vtkMRMLCrosshairNode.CursorPositionModifiedEvent, self.processEvent)
-
- def __del__(self):
- self.removeObservers()
-
- def fitName(self, name, nameSize=None):
- if not nameSize:
- nameSize = self.nameSize
- if len(name) > nameSize:
- preSize = int(nameSize / 2)
- postSize = preSize - 3
- name = name[:preSize] + "..." + name[-postSize:]
- return name
-
- def removeObservers(self):
- # remove observers and reset
- if self.CrosshairNode and self.CrosshairNodeObserverTag:
- self.CrosshairNode.RemoveObserver(self.CrosshairNodeObserverTag)
- self.CrosshairNodeObserverTag = None
-
- def getPixelString(self, volumeNode, ijk):
- """Given a volume node, create a human readable
- string describing the contents"""
- # TODO: the volume nodes should have a way to generate
- # these strings in a generic way
- if not volumeNode:
- return "No volume"
- imageData = volumeNode.GetImageData()
- if not imageData:
- return "No Image"
- dims = imageData.GetDimensions()
- for ele in range(3):
- if ijk[ele] < 0 or ijk[ele] >= dims[ele]:
- return "Out of Frame"
- pixel = ""
- if volumeNode.IsA("vtkMRMLLabelMapVolumeNode"):
- labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0))
- labelValue = "Unknown"
- displayNode = volumeNode.GetDisplayNode()
- if displayNode:
- colorNode = displayNode.GetColorNode()
- if colorNode:
- labelValue = colorNode.GetColorName(labelIndex)
- return "%s (%d)" % (labelValue, labelIndex)
-
- if volumeNode.IsA("vtkMRMLDiffusionTensorVolumeNode"):
- point_idx = imageData.FindPoint(ijk[0], ijk[1], ijk[2])
- if point_idx == -1:
- return "Out of bounds"
-
- if not imageData.GetPointData():
- return "No Point Data"
-
- tensors = imageData.GetPointData().GetTensors()
- if not tensors:
- return "No Tensor Data"
-
- tensor = imageData.GetPointData().GetTensors().GetTuple9(point_idx)
- scalarVolumeDisplayNode = volumeNode.GetScalarVolumeDisplayNode()
-
- if scalarVolumeDisplayNode:
- operation = scalarVolumeDisplayNode.GetScalarInvariant()
+ def __init__(self, parent=None):
+ self.nameSize = 24
+
+ self.CrosshairNode = None
+ self.CrosshairNodeObserverTag = None
+
+ self.frame = qt.QFrame(parent)
+ self.frame.setLayout(qt.QVBoxLayout())
+ # Set horizontal policy to Ignored to prevent a long segment or volume name making the widget wider.
+ # If the module panel made larger then the image viewers would move and the mouse pointer position
+ # would change in the image, potentially pointing outside the node with the long name, resulting in the
+ # module panel collapsing to the original size, causing an infinite oscillation.
+ qSize = qt.QSizePolicy()
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Ignored)
+ qSize.setVerticalPolicy(qt.QSizePolicy.Preferred)
+ self.frame.setSizePolicy(qSize)
+
+ modulePath = slicer.modules.dataprobe.path.replace("DataProbe.py", "")
+ self.iconsDIR = modulePath + '/Resources/Icons'
+
+ self.showImage = False
+
+ # Used in _createMagnifiedPixmap()
+ self.imageCrop = vtk.vtkExtractVOI()
+ self.canvas = vtk.vtkImageCanvasSource2D()
+ self.painter = qt.QPainter()
+ self.pen = qt.QPen()
+
+ self._createSmall()
+
+ # Helper class to calculate and display tensor scalars
+ self.calculateTensorScalars = CalculateTensorScalars()
+
+ # Observe the crosshair node to get the current cursor position
+ self.CrosshairNode = slicer.mrmlScene.GetFirstNodeByClass('vtkMRMLCrosshairNode')
+ if self.CrosshairNode:
+ self.CrosshairNodeObserverTag = self.CrosshairNode.AddObserver(slicer.vtkMRMLCrosshairNode.CursorPositionModifiedEvent, self.processEvent)
+
+ def __del__(self):
+ self.removeObservers()
+
+ def fitName(self, name, nameSize=None):
+ if not nameSize:
+ nameSize = self.nameSize
+ if len(name) > nameSize:
+ preSize = int(nameSize / 2)
+ postSize = preSize - 3
+ name = name[:preSize] + "..." + name[-postSize:]
+ return name
+
+ def removeObservers(self):
+ # remove observers and reset
+ if self.CrosshairNode and self.CrosshairNodeObserverTag:
+ self.CrosshairNode.RemoveObserver(self.CrosshairNodeObserverTag)
+ self.CrosshairNodeObserverTag = None
+
+ def getPixelString(self, volumeNode, ijk):
+ """Given a volume node, create a human readable
+ string describing the contents"""
+ # TODO: the volume nodes should have a way to generate
+ # these strings in a generic way
+ if not volumeNode:
+ return "No volume"
+ imageData = volumeNode.GetImageData()
+ if not imageData:
+ return "No Image"
+ dims = imageData.GetDimensions()
+ for ele in range(3):
+ if ijk[ele] < 0 or ijk[ele] >= dims[ele]:
+ return "Out of Frame"
+ pixel = ""
+ if volumeNode.IsA("vtkMRMLLabelMapVolumeNode"):
+ labelIndex = int(imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], 0))
+ labelValue = "Unknown"
+ displayNode = volumeNode.GetDisplayNode()
+ if displayNode:
+ colorNode = displayNode.GetColorNode()
+ if colorNode:
+ labelValue = colorNode.GetColorName(labelIndex)
+ return "%s (%d)" % (labelValue, labelIndex)
+
+ if volumeNode.IsA("vtkMRMLDiffusionTensorVolumeNode"):
+ point_idx = imageData.FindPoint(ijk[0], ijk[1], ijk[2])
+ if point_idx == -1:
+ return "Out of bounds"
+
+ if not imageData.GetPointData():
+ return "No Point Data"
+
+ tensors = imageData.GetPointData().GetTensors()
+ if not tensors:
+ return "No Tensor Data"
+
+ tensor = imageData.GetPointData().GetTensors().GetTuple9(point_idx)
+ scalarVolumeDisplayNode = volumeNode.GetScalarVolumeDisplayNode()
+
+ if scalarVolumeDisplayNode:
+ operation = scalarVolumeDisplayNode.GetScalarInvariant()
+ else:
+ operation = None
+
+ value = self.calculateTensorScalars(tensor, operation=operation)
+ if value is not None:
+ valueString = ("%f" % value).rstrip('0').rstrip('.')
+ return "%s %s" % (scalarVolumeDisplayNode.GetScalarInvariantAsString(), valueString)
+ else:
+ return scalarVolumeDisplayNode.GetScalarInvariantAsString()
+
+ # default - non label scalar volume
+ numberOfComponents = imageData.GetNumberOfScalarComponents()
+ if numberOfComponents > 3:
+ return "%d components" % numberOfComponents
+ for c in range(numberOfComponents):
+ component = imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], c)
+ if component.is_integer():
+ component = int(component)
+ # format string according to suggestion here:
+ # https://stackoverflow.com/questions/2440692/formatting-floats-in-python-without-superfluous-zeros
+ # also set the default field width for each coordinate
+ componentString = ("%4f" % component).rstrip('0').rstrip('.')
+ pixel += ("%s, " % componentString)
+ return pixel[:-2]
+
+ def processEvent(self, observee, event):
+ # TODO: use a timer to delay calculation and compress events
+ insideView = False
+ ras = [0.0, 0.0, 0.0]
+ xyz = [0.0, 0.0, 0.0]
+ sliceNode = None
+ if self.CrosshairNode:
+ insideView = self.CrosshairNode.GetCursorPositionRAS(ras)
+ sliceNode = self.CrosshairNode.GetCursorPositionXYZ(xyz)
+
+ sliceLogic = None
+ if sliceNode:
+ appLogic = slicer.app.applicationLogic()
+ if appLogic:
+ sliceLogic = appLogic.GetSliceLogic(sliceNode)
+
+ if not insideView or not sliceNode or not sliceLogic:
+ # reset all the readouts
+ self.viewerColor.text = ""
+ self.viewInfo.text = ""
+ layers = ('L', 'F', 'B')
+ for layer in layers:
+ self.layerNames[layer].setText("")
+ self.layerIJKs[layer].setText("")
+ self.layerValues[layer].setText("")
+ self.imageLabel.hide()
+ self.viewerColor.hide()
+ self.viewInfo.hide()
+ self.viewerFrame.hide()
+ self.showImageFrame.show()
+ return
+
+ self.viewerColor.show()
+ self.viewInfo.show()
+ self.viewerFrame.show()
+ self.showImageFrame.hide()
+
+ # populate the widgets
+ self.viewerColor.setText(" ")
+ rgbColor = sliceNode.GetLayoutColor()
+ color = qt.QColor.fromRgbF(rgbColor[0], rgbColor[1], rgbColor[2])
+ if hasattr(color, 'name'):
+ self.viewerColor.setStyleSheet('QLabel {background-color : %s}' % color.name())
+
+ self.viewInfo.text = self.generateViewDescription(xyz, ras, sliceNode, sliceLogic)
+
+ def _roundInt(value):
+ try:
+ return int(round(value))
+ except ValueError:
+ return 0
+
+ hasVolume = False
+ layerLogicCalls = (('L', sliceLogic.GetLabelLayer),
+ ('F', sliceLogic.GetForegroundLayer),
+ ('B', sliceLogic.GetBackgroundLayer))
+ for layer, logicCall in layerLogicCalls:
+ layerLogic = logicCall()
+ volumeNode = layerLogic.GetVolumeNode()
+ ijk = [0, 0, 0]
+ if volumeNode:
+ hasVolume = True
+ xyToIJK = layerLogic.GetXYToIJKTransform()
+ ijkFloat = xyToIJK.TransformDoublePoint(xyz)
+ ijk = [_roundInt(value) for value in ijkFloat]
+ self.layerNames[layer].setText(self.generateLayerName(layerLogic))
+ self.layerIJKs[layer].setText(self.generateIJKPixelDescription(ijk, layerLogic))
+ self.layerValues[layer].setText(self.generateIJKPixelValueDescription(ijk, layerLogic))
+
+ # collect information from displayable managers
+ displayableManagerCollection = vtk.vtkCollection()
+ if sliceNode:
+ sliceWidget = slicer.app.layoutManager().sliceWidget(sliceNode.GetName())
+ if sliceWidget:
+ # sliceWidget is owned by the layout manager
+ sliceView = sliceWidget.sliceView()
+ sliceView.getDisplayableManagers(displayableManagerCollection)
+ aggregatedDisplayableManagerInfo = ''
+ for index in range(displayableManagerCollection.GetNumberOfItems()):
+ displayableManager = displayableManagerCollection.GetItemAsObject(index)
+ infoString = displayableManager.GetDataProbeInfoStringForPosition(xyz)
+ if infoString != "":
+ aggregatedDisplayableManagerInfo += infoString + "
"
+ if aggregatedDisplayableManagerInfo != '':
+ self.displayableManagerInfo.text = '' + aggregatedDisplayableManagerInfo + ''
+ self.displayableManagerInfo.show()
else:
- operation = None
-
- value = self.calculateTensorScalars(tensor, operation=operation)
- if value is not None:
- valueString = ("%f" % value).rstrip('0').rstrip('.')
- return "%s %s" % (scalarVolumeDisplayNode.GetScalarInvariantAsString(), valueString)
+ self.displayableManagerInfo.hide()
+
+ # set image
+ if (not slicer.mrmlScene.IsBatchProcessing()) and sliceLogic and hasVolume and self.showImage:
+ pixmap = self._createMagnifiedPixmap(
+ xyz, sliceLogic.GetBlend().GetOutputPort(), self.imageLabel.size, color)
+ if pixmap:
+ self.imageLabel.setPixmap(pixmap)
+ self.onShowImage(self.showImage)
+
+ if hasattr(self.frame.parent(), 'text'):
+ sceneName = slicer.mrmlScene.GetURL()
+ if sceneName != "":
+ self.frame.parent().text = "Data Probe: %s" % self.fitName(sceneName, nameSize=2 * self.nameSize)
+ else:
+ self.frame.parent().text = "Data Probe"
+
+ def generateViewDescription(self, xyz, ras, sliceNode, sliceLogic):
+
+ # Note that 'xyz' is unused in the Slicer implementation but could
+ # be used when customizing the behavior of this function in extension.
+
+ # Described below are the details for the ras coordinate width set to 6:
+ # 1: sign
+ # 3: suggested number of digits before decimal point
+ # 1: decimal point:
+ # 1: number of digits after decimal point
+
+ spacing = "%.1f" % sliceLogic.GetLowestVolumeSliceSpacing()[2]
+ if sliceNode.GetSliceSpacingMode() == slicer.vtkMRMLSliceNode.PrescribedSliceSpacingMode:
+ spacing = "(%s)" % spacing
+
+ return \
+ " {layoutName: <8s} ({rLabel} {ras_x:3.1f}, {aLabel} {ras_y:3.1f}, {sLabel} {ras_z:3.1f}) {orient: >8s} Sp: {spacing:s}" \
+ .format(layoutName=sliceNode.GetLayoutName(),
+ rLabel=sliceNode.GetAxisLabel(1) if ras[0] >= 0 else sliceNode.GetAxisLabel(0),
+ aLabel=sliceNode.GetAxisLabel(3) if ras[1] >= 0 else sliceNode.GetAxisLabel(2),
+ sLabel=sliceNode.GetAxisLabel(5) if ras[2] >= 0 else sliceNode.GetAxisLabel(4),
+ ras_x=abs(ras[0]),
+ ras_y=abs(ras[1]),
+ ras_z=abs(ras[2]),
+ orient=sliceNode.GetOrientationString(),
+ spacing=spacing
+ )
+
+ def generateLayerName(self, slicerLayerLogic):
+ volumeNode = slicerLayerLogic.GetVolumeNode()
+ return "%s" % (self.fitName(volumeNode.GetName()) if volumeNode else "None")
+
+ def generateIJKPixelDescription(self, ijk, slicerLayerLogic):
+ volumeNode = slicerLayerLogic.GetVolumeNode()
+ return f"({ijk[0]:3d}, {ijk[1]:3d}, {ijk[2]:3d})" if volumeNode else ""
+
+ def generateIJKPixelValueDescription(self, ijk, slicerLayerLogic):
+ volumeNode = slicerLayerLogic.GetVolumeNode()
+ return "%s" % self.getPixelString(volumeNode, ijk) if volumeNode else ""
+
+ def _createMagnifiedPixmap(self, xyz, inputImageDataConnection, outputSize, crosshairColor, imageZoom=10):
+
+ # Use existing instance of objects to avoid instantiating one at each event.
+ imageCrop = self.imageCrop
+ painter = self.painter
+ pen = self.pen
+
+ def _roundInt(value):
+ try:
+ return int(round(value))
+ except ValueError:
+ return 0
+
+ imageCrop.SetInputConnection(inputImageDataConnection)
+ xyzInt = [0, 0, 0]
+ xyzInt = [_roundInt(value) for value in xyz]
+ producer = inputImageDataConnection.GetProducer()
+ dims = producer.GetOutput().GetDimensions()
+ minDim = min(dims[0], dims[1])
+ imageSize = _roundInt(minDim / imageZoom / 2.0)
+ imin = xyzInt[0] - imageSize
+ imax = xyzInt[0] + imageSize
+ jmin = xyzInt[1] - imageSize
+ jmax = xyzInt[1] + imageSize
+ imin_trunc = max(0, imin)
+ imax_trunc = min(dims[0] - 1, imax)
+ jmin_trunc = max(0, jmin)
+ jmax_trunc = min(dims[1] - 1, jmax)
+ # The extra complexity of the canvas is used here to maintain a fixed size
+ # output due to the imageCrop returning a smaller image if the limits are
+ # outside the input image bounds. Specially useful when zooming at the borders.
+ canvas = self.canvas
+ canvas.SetScalarType(producer.GetOutput().GetScalarType())
+ canvas.SetNumberOfScalarComponents(producer.GetOutput().GetNumberOfScalarComponents())
+ canvas.SetExtent(imin, imax, jmin, jmax, 0, 0)
+ canvas.FillBox(imin, imax, jmin, jmax)
+ canvas.Update()
+ if (imin_trunc <= imax_trunc) and (jmin_trunc <= jmax_trunc):
+ imageCrop.SetVOI(imin_trunc, imax_trunc, jmin_trunc, jmax_trunc, 0, 0)
+ imageCrop.Update()
+ vtkImageCropped = imageCrop.GetOutput()
+ xyzBounds = [0] * 6
+ vtkImageCropped.GetBounds(xyzBounds)
+ xyzBounds = [_roundInt(value) for value in xyzBounds]
+ canvas.DrawImage(xyzBounds[0], xyzBounds[2], vtkImageCropped)
+ canvas.Update()
+ vtkImageFromCanvas = canvas.GetOutput()
+ if vtkImageFromCanvas:
+ qImage = qt.QImage()
+ slicer.qMRMLUtils().vtkImageDataToQImage(vtkImageFromCanvas, qImage)
+ imagePixmap = qt.QPixmap.fromImage(qImage)
+ imagePixmap = imagePixmap.scaled(outputSize, qt.Qt.KeepAspectRatio, qt.Qt.FastTransformation)
+
+ # draw crosshair
+ painter.begin(imagePixmap)
+ pen = qt.QPen()
+ pen.setColor(crosshairColor)
+ painter.setPen(pen)
+ painter.drawLine(0, int(imagePixmap.height() / 2), imagePixmap.width(), int(imagePixmap.height() / 2))
+ painter.drawLine(int(imagePixmap.width() / 2), 0, int(imagePixmap.width() / 2), imagePixmap.height())
+ painter.end()
+ return imagePixmap
+ return None
+
+ def _createSmall(self):
+ """Make the internals of the widget to display in the
+ Data Probe frame (lower left of slicer main window by default)"""
+
+ # this method makes SliceView Annotation
+ self.sliceAnnotations = DataProbeLib.SliceAnnotations()
+
+ # goto module button
+ self.goToModule = qt.QPushButton('->', self.frame)
+ self.goToModule.setToolTip('Go to the DataProbe module for more information and options')
+ self.frame.layout().addWidget(self.goToModule)
+ self.goToModule.connect("clicked()", self.onGoToModule)
+ # hide this for now - there's not much to see in the module itself
+ self.goToModule.hide()
+
+ # image view: To ensure the height of the checkbox matches the height of the
+ # viewerFrame, it is added to a frame setting the layout and hard-coding the
+ # content margins.
+ # TODO: Revisit the approach and avoid hard-coding content margins
+ self.showImageFrame = qt.QFrame(self.frame)
+ self.frame.layout().addWidget(self.showImageFrame)
+ self.showImageFrame.setLayout(qt.QHBoxLayout())
+ self.showImageFrame.layout().setContentsMargins(0, 3, 0, 3)
+ self.showImageBox = qt.QCheckBox('Show Zoomed Slice', self.showImageFrame)
+ self.showImageFrame.layout().addWidget(self.showImageBox)
+ self.showImageBox.connect("toggled(bool)", self.onShowImage)
+ self.showImageBox.setChecked(False)
+
+ self.imageLabel = qt.QLabel()
+
+ # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ # fails on some systems, therefore set the policies using separate method calls
+ qSize = qt.QSizePolicy()
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
+ qSize.setVerticalPolicy(qt.QSizePolicy.Expanding)
+ self.imageLabel.setSizePolicy(qSize)
+ # self.imageLabel.setScaledContents(True)
+ self.frame.layout().addWidget(self.imageLabel)
+ self.onShowImage(False)
+
+ # top row - things about the viewer itself
+ self.viewerFrame = qt.QFrame(self.frame)
+ self.viewerFrame.setLayout(qt.QHBoxLayout())
+ self.frame.layout().addWidget(self.viewerFrame)
+ self.viewerColor = qt.QLabel(self.viewerFrame)
+ self.viewerColor.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Preferred)
+ self.viewerFrame.layout().addWidget(self.viewerColor)
+ self.viewInfo = qt.QLabel()
+ self.viewerFrame.layout().addWidget(self.viewInfo)
+
+ def _setFixedFontFamily(widget, family=None):
+ if family is None:
+ family = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont).family()
+ font = widget.font
+ font.setFamily(family)
+ widget.font = font
+ widget.wordWrap = True
+ _setFixedFontFamily(self.viewInfo)
+
+ # the grid - things about the layers
+ # this method makes labels
+ self.layerGrid = qt.QFrame(self.frame)
+ layout = qt.QGridLayout()
+ self.layerGrid.setLayout(layout)
+ self.frame.layout().addWidget(self.layerGrid)
+ layers = ('L', 'F', 'B')
+ self.layerNames = {}
+ self.layerIJKs = {}
+ self.layerValues = {}
+ for (row, layer) in enumerate(layers):
+ col = 0
+ layout.addWidget(qt.QLabel(layer), row, col)
+ col += 1
+ self.layerNames[layer] = qt.QLabel()
+ layout.addWidget(self.layerNames[layer], row, col)
+ col += 1
+ self.layerIJKs[layer] = qt.QLabel()
+ layout.addWidget(self.layerIJKs[layer], row, col)
+ col += 1
+ self.layerValues[layer] = qt.QLabel()
+ layout.addWidget(self.layerValues[layer], row, col)
+ layout.setColumnStretch(col, 100)
+
+ _setFixedFontFamily(self.layerNames[layer])
+ _setFixedFontFamily(self.layerIJKs[layer])
+ _setFixedFontFamily(self.layerValues[layer])
+
+ # information collected about the current crosshair position
+ # from displayable managers registered to the current view
+ self.displayableManagerInfo = qt.QLabel()
+ self.displayableManagerInfo.indent = 6
+ self.displayableManagerInfo.wordWrap = True
+ self.frame.layout().addWidget(self.displayableManagerInfo)
+ # only show if not empty
+ self.displayableManagerInfo.hide()
+
+ # goto module button
+ self.goToModule = qt.QPushButton('->', self.frame)
+ self.goToModule.setToolTip('Go to the DataProbe module for more information and options')
+ self.frame.layout().addWidget(self.goToModule)
+ self.goToModule.connect("clicked()", self.onGoToModule)
+ # hide this for now - there's not much to see in the module itself
+ self.goToModule.hide()
+
+ def onGoToModule(self):
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('DataProbe')
+
+ def onShowImage(self, value=False):
+ self.showImage = value
+ if value:
+ self.imageLabel.show()
else:
- return scalarVolumeDisplayNode.GetScalarInvariantAsString()
-
- # default - non label scalar volume
- numberOfComponents = imageData.GetNumberOfScalarComponents()
- if numberOfComponents > 3:
- return "%d components" % numberOfComponents
- for c in range(numberOfComponents):
- component = imageData.GetScalarComponentAsDouble(ijk[0], ijk[1], ijk[2], c)
- if component.is_integer():
- component = int(component)
- # format string according to suggestion here:
- # https://stackoverflow.com/questions/2440692/formatting-floats-in-python-without-superfluous-zeros
- # also set the default field width for each coordinate
- componentString = ("%4f" % component).rstrip('0').rstrip('.')
- pixel += ("%s, " % componentString)
- return pixel[:-2]
-
- def processEvent(self, observee, event):
- # TODO: use a timer to delay calculation and compress events
- insideView = False
- ras = [0.0, 0.0, 0.0]
- xyz = [0.0, 0.0, 0.0]
- sliceNode = None
- if self.CrosshairNode:
- insideView = self.CrosshairNode.GetCursorPositionRAS(ras)
- sliceNode = self.CrosshairNode.GetCursorPositionXYZ(xyz)
-
- sliceLogic = None
- if sliceNode:
- appLogic = slicer.app.applicationLogic()
- if appLogic:
- sliceLogic = appLogic.GetSliceLogic(sliceNode)
-
- if not insideView or not sliceNode or not sliceLogic:
- # reset all the readouts
- self.viewerColor.text = ""
- self.viewInfo.text = ""
- layers = ('L', 'F', 'B')
- for layer in layers:
- self.layerNames[layer].setText("")
- self.layerIJKs[layer].setText("")
- self.layerValues[layer].setText("")
- self.imageLabel.hide()
- self.viewerColor.hide()
- self.viewInfo.hide()
- self.viewerFrame.hide()
- self.showImageFrame.show()
- return
-
- self.viewerColor.show()
- self.viewInfo.show()
- self.viewerFrame.show()
- self.showImageFrame.hide()
-
- # populate the widgets
- self.viewerColor.setText(" ")
- rgbColor = sliceNode.GetLayoutColor()
- color = qt.QColor.fromRgbF(rgbColor[0], rgbColor[1], rgbColor[2])
- if hasattr(color, 'name'):
- self.viewerColor.setStyleSheet('QLabel {background-color : %s}' % color.name())
-
- self.viewInfo.text = self.generateViewDescription(xyz, ras, sliceNode, sliceLogic)
-
- def _roundInt(value):
- try:
- return int(round(value))
- except ValueError:
- return 0
-
- hasVolume = False
- layerLogicCalls = (('L', sliceLogic.GetLabelLayer),
- ('F', sliceLogic.GetForegroundLayer),
- ('B', sliceLogic.GetBackgroundLayer))
- for layer, logicCall in layerLogicCalls:
- layerLogic = logicCall()
- volumeNode = layerLogic.GetVolumeNode()
- ijk = [0, 0, 0]
- if volumeNode:
- hasVolume = True
- xyToIJK = layerLogic.GetXYToIJKTransform()
- ijkFloat = xyToIJK.TransformDoublePoint(xyz)
- ijk = [_roundInt(value) for value in ijkFloat]
- self.layerNames[layer].setText(self.generateLayerName(layerLogic))
- self.layerIJKs[layer].setText(self.generateIJKPixelDescription(ijk, layerLogic))
- self.layerValues[layer].setText(self.generateIJKPixelValueDescription(ijk, layerLogic))
-
- # collect information from displayable managers
- displayableManagerCollection = vtk.vtkCollection()
- if sliceNode:
- sliceWidget = slicer.app.layoutManager().sliceWidget(sliceNode.GetName())
- if sliceWidget:
- # sliceWidget is owned by the layout manager
- sliceView = sliceWidget.sliceView()
- sliceView.getDisplayableManagers(displayableManagerCollection)
- aggregatedDisplayableManagerInfo = ''
- for index in range(displayableManagerCollection.GetNumberOfItems()):
- displayableManager = displayableManagerCollection.GetItemAsObject(index)
- infoString = displayableManager.GetDataProbeInfoStringForPosition(xyz)
- if infoString != "":
- aggregatedDisplayableManagerInfo += infoString + "
"
- if aggregatedDisplayableManagerInfo != '':
- self.displayableManagerInfo.text = '' + aggregatedDisplayableManagerInfo + ''
- self.displayableManagerInfo.show()
- else:
- self.displayableManagerInfo.hide()
-
- # set image
- if (not slicer.mrmlScene.IsBatchProcessing()) and sliceLogic and hasVolume and self.showImage:
- pixmap = self._createMagnifiedPixmap(
- xyz, sliceLogic.GetBlend().GetOutputPort(), self.imageLabel.size, color)
- if pixmap:
- self.imageLabel.setPixmap(pixmap)
- self.onShowImage(self.showImage)
-
- if hasattr(self.frame.parent(), 'text'):
- sceneName = slicer.mrmlScene.GetURL()
- if sceneName != "":
- self.frame.parent().text = "Data Probe: %s" % self.fitName(sceneName, nameSize=2 * self.nameSize)
- else:
- self.frame.parent().text = "Data Probe"
-
- def generateViewDescription(self, xyz, ras, sliceNode, sliceLogic):
-
- # Note that 'xyz' is unused in the Slicer implementation but could
- # be used when customizing the behavior of this function in extension.
-
- # Described below are the details for the ras coordinate width set to 6:
- # 1: sign
- # 3: suggested number of digits before decimal point
- # 1: decimal point:
- # 1: number of digits after decimal point
-
- spacing = "%.1f" % sliceLogic.GetLowestVolumeSliceSpacing()[2]
- if sliceNode.GetSliceSpacingMode() == slicer.vtkMRMLSliceNode.PrescribedSliceSpacingMode:
- spacing = "(%s)" % spacing
-
- return \
- " {layoutName: <8s} ({rLabel} {ras_x:3.1f}, {aLabel} {ras_y:3.1f}, {sLabel} {ras_z:3.1f}) {orient: >8s} Sp: {spacing:s}" \
- .format(layoutName=sliceNode.GetLayoutName(),
- rLabel=sliceNode.GetAxisLabel(1) if ras[0] >= 0 else sliceNode.GetAxisLabel(0),
- aLabel=sliceNode.GetAxisLabel(3) if ras[1] >= 0 else sliceNode.GetAxisLabel(2),
- sLabel=sliceNode.GetAxisLabel(5) if ras[2] >= 0 else sliceNode.GetAxisLabel(4),
- ras_x=abs(ras[0]),
- ras_y=abs(ras[1]),
- ras_z=abs(ras[2]),
- orient=sliceNode.GetOrientationString(),
- spacing=spacing
- )
-
- def generateLayerName(self, slicerLayerLogic):
- volumeNode = slicerLayerLogic.GetVolumeNode()
- return "%s" % (self.fitName(volumeNode.GetName()) if volumeNode else "None")
-
- def generateIJKPixelDescription(self, ijk, slicerLayerLogic):
- volumeNode = slicerLayerLogic.GetVolumeNode()
- return f"({ijk[0]:3d}, {ijk[1]:3d}, {ijk[2]:3d})" if volumeNode else ""
-
- def generateIJKPixelValueDescription(self, ijk, slicerLayerLogic):
- volumeNode = slicerLayerLogic.GetVolumeNode()
- return "%s" % self.getPixelString(volumeNode, ijk) if volumeNode else ""
-
- def _createMagnifiedPixmap(self, xyz, inputImageDataConnection, outputSize, crosshairColor, imageZoom=10):
-
- # Use existing instance of objects to avoid instantiating one at each event.
- imageCrop = self.imageCrop
- painter = self.painter
- pen = self.pen
-
- def _roundInt(value):
- try:
- return int(round(value))
- except ValueError:
- return 0
-
- imageCrop.SetInputConnection(inputImageDataConnection)
- xyzInt = [0, 0, 0]
- xyzInt = [_roundInt(value) for value in xyz]
- producer = inputImageDataConnection.GetProducer()
- dims = producer.GetOutput().GetDimensions()
- minDim = min(dims[0], dims[1])
- imageSize = _roundInt(minDim / imageZoom / 2.0)
- imin = xyzInt[0] - imageSize
- imax = xyzInt[0] + imageSize
- jmin = xyzInt[1] - imageSize
- jmax = xyzInt[1] + imageSize
- imin_trunc = max(0, imin)
- imax_trunc = min(dims[0] - 1, imax)
- jmin_trunc = max(0, jmin)
- jmax_trunc = min(dims[1] - 1, jmax)
- # The extra complexity of the canvas is used here to maintain a fixed size
- # output due to the imageCrop returning a smaller image if the limits are
- # outside the input image bounds. Specially useful when zooming at the borders.
- canvas = self.canvas
- canvas.SetScalarType(producer.GetOutput().GetScalarType())
- canvas.SetNumberOfScalarComponents(producer.GetOutput().GetNumberOfScalarComponents())
- canvas.SetExtent(imin, imax, jmin, jmax, 0, 0)
- canvas.FillBox(imin, imax, jmin, jmax)
- canvas.Update()
- if (imin_trunc <= imax_trunc) and (jmin_trunc <= jmax_trunc):
- imageCrop.SetVOI(imin_trunc, imax_trunc, jmin_trunc, jmax_trunc, 0, 0)
- imageCrop.Update()
- vtkImageCropped = imageCrop.GetOutput()
- xyzBounds = [0] * 6
- vtkImageCropped.GetBounds(xyzBounds)
- xyzBounds = [_roundInt(value) for value in xyzBounds]
- canvas.DrawImage(xyzBounds[0], xyzBounds[2], vtkImageCropped)
- canvas.Update()
- vtkImageFromCanvas = canvas.GetOutput()
- if vtkImageFromCanvas:
- qImage = qt.QImage()
- slicer.qMRMLUtils().vtkImageDataToQImage(vtkImageFromCanvas, qImage)
- imagePixmap = qt.QPixmap.fromImage(qImage)
- imagePixmap = imagePixmap.scaled(outputSize, qt.Qt.KeepAspectRatio, qt.Qt.FastTransformation)
-
- # draw crosshair
- painter.begin(imagePixmap)
- pen = qt.QPen()
- pen.setColor(crosshairColor)
- painter.setPen(pen)
- painter.drawLine(0, int(imagePixmap.height() / 2), imagePixmap.width(), int(imagePixmap.height() / 2))
- painter.drawLine(int(imagePixmap.width() / 2), 0, int(imagePixmap.width() / 2), imagePixmap.height())
- painter.end()
- return imagePixmap
- return None
-
- def _createSmall(self):
- """Make the internals of the widget to display in the
- Data Probe frame (lower left of slicer main window by default)"""
-
- # this method makes SliceView Annotation
- self.sliceAnnotations = DataProbeLib.SliceAnnotations()
-
- # goto module button
- self.goToModule = qt.QPushButton('->', self.frame)
- self.goToModule.setToolTip('Go to the DataProbe module for more information and options')
- self.frame.layout().addWidget(self.goToModule)
- self.goToModule.connect("clicked()", self.onGoToModule)
- # hide this for now - there's not much to see in the module itself
- self.goToModule.hide()
-
- # image view: To ensure the height of the checkbox matches the height of the
- # viewerFrame, it is added to a frame setting the layout and hard-coding the
- # content margins.
- # TODO: Revisit the approach and avoid hard-coding content margins
- self.showImageFrame = qt.QFrame(self.frame)
- self.frame.layout().addWidget(self.showImageFrame)
- self.showImageFrame.setLayout(qt.QHBoxLayout())
- self.showImageFrame.layout().setContentsMargins(0, 3, 0, 3)
- self.showImageBox = qt.QCheckBox('Show Zoomed Slice', self.showImageFrame)
- self.showImageFrame.layout().addWidget(self.showImageBox)
- self.showImageBox.connect("toggled(bool)", self.onShowImage)
- self.showImageBox.setChecked(False)
-
- self.imageLabel = qt.QLabel()
-
- # qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
- # fails on some systems, therefore set the policies using separate method calls
- qSize = qt.QSizePolicy()
- qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
- qSize.setVerticalPolicy(qt.QSizePolicy.Expanding)
- self.imageLabel.setSizePolicy(qSize)
- # self.imageLabel.setScaledContents(True)
- self.frame.layout().addWidget(self.imageLabel)
- self.onShowImage(False)
-
- # top row - things about the viewer itself
- self.viewerFrame = qt.QFrame(self.frame)
- self.viewerFrame.setLayout(qt.QHBoxLayout())
- self.frame.layout().addWidget(self.viewerFrame)
- self.viewerColor = qt.QLabel(self.viewerFrame)
- self.viewerColor.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Preferred)
- self.viewerFrame.layout().addWidget(self.viewerColor)
- self.viewInfo = qt.QLabel()
- self.viewerFrame.layout().addWidget(self.viewInfo)
-
- def _setFixedFontFamily(widget, family=None):
- if family is None:
- family = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont).family()
- font = widget.font
- font.setFamily(family)
- widget.font = font
- widget.wordWrap = True
- _setFixedFontFamily(self.viewInfo)
-
- # the grid - things about the layers
- # this method makes labels
- self.layerGrid = qt.QFrame(self.frame)
- layout = qt.QGridLayout()
- self.layerGrid.setLayout(layout)
- self.frame.layout().addWidget(self.layerGrid)
- layers = ('L', 'F', 'B')
- self.layerNames = {}
- self.layerIJKs = {}
- self.layerValues = {}
- for (row, layer) in enumerate(layers):
- col = 0
- layout.addWidget(qt.QLabel(layer), row, col)
- col += 1
- self.layerNames[layer] = qt.QLabel()
- layout.addWidget(self.layerNames[layer], row, col)
- col += 1
- self.layerIJKs[layer] = qt.QLabel()
- layout.addWidget(self.layerIJKs[layer], row, col)
- col += 1
- self.layerValues[layer] = qt.QLabel()
- layout.addWidget(self.layerValues[layer], row, col)
- layout.setColumnStretch(col, 100)
-
- _setFixedFontFamily(self.layerNames[layer])
- _setFixedFontFamily(self.layerIJKs[layer])
- _setFixedFontFamily(self.layerValues[layer])
-
- # information collected about the current crosshair position
- # from displayable managers registered to the current view
- self.displayableManagerInfo = qt.QLabel()
- self.displayableManagerInfo.indent = 6
- self.displayableManagerInfo.wordWrap = True
- self.frame.layout().addWidget(self.displayableManagerInfo)
- # only show if not empty
- self.displayableManagerInfo.hide()
-
- # goto module button
- self.goToModule = qt.QPushButton('->', self.frame)
- self.goToModule.setToolTip('Go to the DataProbe module for more information and options')
- self.frame.layout().addWidget(self.goToModule)
- self.goToModule.connect("clicked()", self.onGoToModule)
- # hide this for now - there's not much to see in the module itself
- self.goToModule.hide()
-
- def onGoToModule(self):
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('DataProbe')
-
- def onShowImage(self, value=False):
- self.showImage = value
- if value:
- self.imageLabel.show()
- else:
- self.imageLabel.hide()
- pixmap = qt.QPixmap()
- self.imageLabel.setPixmap(pixmap)
+ self.imageLabel.hide()
+ pixmap = qt.QPixmap()
+ self.imageLabel.setPixmap(pixmap)
#
@@ -520,29 +520,29 @@ def onShowImage(self, value=False):
class DataProbeWidget(ScriptedLoadableModuleWidget):
- def enter(self):
- pass
+ def enter(self):
+ pass
- def exit(self):
- pass
+ def exit(self):
+ pass
- def updateGUIFromMRML(self, caller, event):
- pass
+ def updateGUIFromMRML(self, caller, event):
+ pass
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
- # Instantiate and connect widgets ...
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+ # Instantiate and connect widgets ...
- settingsCollapsibleButton = ctk.ctkCollapsibleButton()
- settingsCollapsibleButton.text = "Slice View Annotations Settings"
- self.layout.addWidget(settingsCollapsibleButton)
- settingsVBoxLayout = qt.QVBoxLayout(settingsCollapsibleButton)
- dataProbeInstance = slicer.modules.DataProbeInstance
- if dataProbeInstance.infoWidget:
- sliceAnnotationsFrame = dataProbeInstance.infoWidget.sliceAnnotations.window
- settingsVBoxLayout.addWidget(sliceAnnotationsFrame)
+ settingsCollapsibleButton = ctk.ctkCollapsibleButton()
+ settingsCollapsibleButton.text = "Slice View Annotations Settings"
+ self.layout.addWidget(settingsCollapsibleButton)
+ settingsVBoxLayout = qt.QVBoxLayout(settingsCollapsibleButton)
+ dataProbeInstance = slicer.modules.DataProbeInstance
+ if dataProbeInstance.infoWidget:
+ sliceAnnotationsFrame = dataProbeInstance.infoWidget.sliceAnnotations.window
+ settingsVBoxLayout.addWidget(sliceAnnotationsFrame)
- self.parent.layout().addStretch(1)
+ self.parent.layout().addStretch(1)
class CalculateTensorScalars:
@@ -583,49 +583,49 @@ def __call__(self, tensor, operation=None):
class DataProbeTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- pass
-
- def runTest(self):
- """Run as few or as many tests as needed here.
"""
- self.setUp()
- self.test_DataProbe1()
-
- def test_DataProbe1(self):
- """ Ideally you should have several levels of tests. At the lowest level
- tests should exercise the functionality of the logic with different inputs
- (both valid and invalid). At higher levels your tests should emulate the
- way the user would interact with your code and confirm that it still works
- the way you intended.
- One of the most important features of the tests is that it should alert other
- developers when their changes will have an impact on the behavior of your
- module. For example, if a developer removes a feature that you depend on,
- your test should break so they know that the feature is needed.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting the test")
-
- #
- # first, get some data
- #
- import SampleData
- SampleData.downloadFromURL(
- nodeNames='FA',
- fileNames='FA.nrrd',
- uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560',
- checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560')
- self.delayDisplay('Finished with download and loading')
-
- self.widget = DataProbeInfoWidget()
- self.widget.frame.show()
-
- self.delayDisplay('Test passed!')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ pass
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_DataProbe1()
+
+ def test_DataProbe1(self):
+ """ Ideally you should have several levels of tests. At the lowest level
+ tests should exercise the functionality of the logic with different inputs
+ (both valid and invalid). At higher levels your tests should emulate the
+ way the user would interact with your code and confirm that it still works
+ the way you intended.
+ One of the most important features of the tests is that it should alert other
+ developers when their changes will have an impact on the behavior of your
+ module. For example, if a developer removes a feature that you depend on,
+ your test should break so they know that the feature is needed.
+ """
+
+ self.delayDisplay("Starting the test")
+
+ #
+ # first, get some data
+ #
+ import SampleData
+ SampleData.downloadFromURL(
+ nodeNames='FA',
+ fileNames='FA.nrrd',
+ uris=TESTING_DATA_URL + 'SHA256/12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560',
+ checksums='SHA256:12d17fba4f2e1f1a843f0757366f28c3f3e1a8bb38836f0de2a32bb1cd476560')
+ self.delayDisplay('Finished with download and loading')
+
+ self.widget = DataProbeInfoWidget()
+ self.widget.frame.show()
+
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py b/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py
index 07353c6093e..5608bae1feb 100644
--- a/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py
+++ b/Modules/Scripted/DataProbe/DataProbeLib/DataProbeUtil.py
@@ -18,33 +18,33 @@
class DataProbeUtil:
- def getParameterNode(self):
- """Get the DataProbe parameter node - a singleton in the scene"""
- node = self._findParameterNodeInScene()
- if not node:
- node = self._createParameterNode()
- return node
-
- def _findParameterNodeInScene(self):
- node = None
- size = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode")
- for i in range(size):
- n = slicer.mrmlScene.GetNthNodeByClass(i, "vtkMRMLScriptedModuleNode")
- if n.GetModuleName() == "DataProbe":
- node = n
- return node
-
- def _createParameterNode(self):
- """create the DataProbe parameter node - a singleton in the scene
- This is used internally by getParameterNode - shouldn't really
- be called for any other reason.
- """
- node = slicer.vtkMRMLScriptedModuleNode()
- node.SetSingletonTag("DataProbe")
- node.SetModuleName("DataProbe")
- # node.SetParameter( "label", "1" )
- slicer.mrmlScene.AddNode(node)
- # Since we are a singleton, the scene won't add our node into the scene,
- # but will instead insert a copy, so we find that and return it
- node = self._findParameterNodeInScene()
- return node
+ def getParameterNode(self):
+ """Get the DataProbe parameter node - a singleton in the scene"""
+ node = self._findParameterNodeInScene()
+ if not node:
+ node = self._createParameterNode()
+ return node
+
+ def _findParameterNodeInScene(self):
+ node = None
+ size = slicer.mrmlScene.GetNumberOfNodesByClass("vtkMRMLScriptedModuleNode")
+ for i in range(size):
+ n = slicer.mrmlScene.GetNthNodeByClass(i, "vtkMRMLScriptedModuleNode")
+ if n.GetModuleName() == "DataProbe":
+ node = n
+ return node
+
+ def _createParameterNode(self):
+ """create the DataProbe parameter node - a singleton in the scene
+ This is used internally by getParameterNode - shouldn't really
+ be called for any other reason.
+ """
+ node = slicer.vtkMRMLScriptedModuleNode()
+ node.SetSingletonTag("DataProbe")
+ node.SetModuleName("DataProbe")
+ # node.SetParameter( "label", "1" )
+ slicer.mrmlScene.AddNode(node)
+ # Since we are a singleton, the scene won't add our node into the scene,
+ # but will instead insert a copy, so we find that and return it
+ node = self._findParameterNodeInScene()
+ return node
diff --git a/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py b/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py
index 185808f27c3..5774a7cb5c6 100644
--- a/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py
+++ b/Modules/Scripted/DataProbe/DataProbeLib/SliceViewAnnotations.py
@@ -11,647 +11,647 @@
class SliceAnnotations(VTKObservationMixin):
- """Implement the Qt window showing settings for Slice View Annotations
- """
-
- def __init__(self, layoutManager=None):
- VTKObservationMixin.__init__(self)
-
- self.layoutManager = layoutManager
- if self.layoutManager is None:
- self.layoutManager = slicer.app.layoutManager()
- self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed)
-
- self.dataProbeUtil = DataProbeUtil.DataProbeUtil()
-
- self.dicomVolumeNode = 0
-
- # Cache recently used extracted DICOM values.
- # Getting all necessary DICOM values from the database (tag cache)
- # would slow down slice browsing significantly.
- # We may have several different volumes shown in different slice views,
- # so we keep in the cache a number of items, not just 2.
- self.extractedDICOMValuesCacheSize = 12
- import collections
- self.extractedDICOMValuesCache = collections.OrderedDict()
-
- self.sliceViewNames = []
- self.popupGeometry = qt.QRect()
- self.cornerTexts = []
- # Bottom Left Corner Text
- self.cornerTexts.append({
- '1-Label': {'text': '', 'category': 'A'},
- '2-Foreground': {'text': '', 'category': 'A'},
- '3-Background': {'text': '', 'category': 'A'}
- })
- # Bottom Right Corner Text
- # Not used - orientation figure may be drawn there
- self.cornerTexts.append({
- '1-TR': {'text': '', 'category': 'A'},
- '2-TE': {'text': '', 'category': 'A'}
- })
- # Top Left Corner Text
- self.cornerTexts.append({
- '1-PatientName': {'text': '', 'category': 'B'},
- '2-PatientID': {'text': '', 'category': 'A'},
- '3-PatientInfo': {'text': '', 'category': 'B'},
- '4-Bg-SeriesDate': {'text': '', 'category': 'B'},
- '5-Fg-SeriesDate': {'text': '', 'category': 'B'},
- '6-Bg-SeriesTime': {'text': '', 'category': 'C'},
- '7-Bg-SeriesTime': {'text': '', 'category': 'C'},
- '8-Bg-SeriesDescription': {'text': '', 'category': 'C'},
- '9-Fg-SeriesDescription': {'text': '', 'category': 'C'}
- })
- # Top Right Corner Text
- self.cornerTexts.append({
- '1-Institution-Name': {'text': '', 'category': 'B'},
- '2-Referring-Phisycian': {'text': '', 'category': 'B'},
- '3-Manufacturer': {'text': '', 'category': 'C'},
- '4-Model': {'text': '', 'category': 'C'},
- '5-Patient-Position': {'text': '', 'category': 'A'},
- '6-TR': {'text': '', 'category': 'A'},
- '7-TE': {'text': '', 'category': 'A'}
- })
-
- self.annotationsDisplayAmount = 0
-
- #
- self.scene = slicer.mrmlScene
- self.sliceViews = {}
-
- # If there are no user settings load defaults
- self.sliceViewAnnotationsEnabled = settingsValue('DataProbe/sliceViewAnnotations.enabled', 1, converter=int)
-
- self.bottomLeft = settingsValue('DataProbe/sliceViewAnnotations.bottomLeft', 1, converter=int)
- self.topLeft = settingsValue('DataProbe/sliceViewAnnotations.topLeft', 0, converter=int)
- self.topRight = settingsValue('DataProbe/sliceViewAnnotations.topRight', 0, converter=int)
- self.fontFamily = settingsValue('DataProbe/sliceViewAnnotations.fontFamily', 'Times')
- self.fontSize = settingsValue('DataProbe/sliceViewAnnotations.fontSize', 14, converter=int)
- self.backgroundDICOMAnnotationsPersistence = settingsValue(
- 'DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', 0, converter=int)
- self.sliceViewAnnotationsEnabledparameter = 'sliceViewAnnotationsEnabled'
- self.parameterNode = self.dataProbeUtil.getParameterNode()
- self.addObserver(self.parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromMRML)
-
- self.maximumTextLength = 35
-
- self.create()
-
- if self.sliceViewAnnotationsEnabled:
- self.updateSliceViewFromGUI()
-
- def create(self):
- # Instantiate and connect widgets ...
- loader = qt.QUiLoader()
- path = os.path.join(os.path.dirname(__file__), 'Resources', 'UI', 'settings.ui')
- qfile = qt.QFile(path)
- qfile.open(qt.QFile.ReadOnly)
- self.window = loader.load(qfile)
- window = self.window
-
- find = slicer.util.findChildren
- self.cornerTextParametersCollapsibleButton = find(window, 'cornerTextParametersCollapsibleButton')[0]
- self.sliceViewAnnotationsCheckBox = find(window, 'sliceViewAnnotationsCheckBox')[0]
- self.sliceViewAnnotationsCheckBox.checked = self.sliceViewAnnotationsEnabled
-
- self.activateCornersGroupBox = find(window, 'activateCornersGroupBox')[0]
- self.topLeftCheckBox = find(window, 'topLeftCheckBox')[0]
- self.topLeftCheckBox.checked = self.topLeft
- self.topRightCheckBox = find(window, 'topRightCheckBox')[0]
- self.topRightCheckBox.checked = self.topRight
-
- self.bottomLeftCheckBox = find(window, 'bottomLeftCheckBox')[0]
- self.bottomLeftCheckBox.checked = self.bottomLeft
-
- self.level1RadioButton = find(window, 'level1RadioButton')[0]
- self.level2RadioButton = find(window, 'level2RadioButton')[0]
- self.level3RadioButton = find(window, 'level3RadioButton')[0]
-
- self.fontPropertiesGroupBox = find(window, 'fontPropertiesGroupBox')[0]
- self.timesFontRadioButton = find(window, 'timesFontRadioButton')[0]
- self.arialFontRadioButton = find(window, 'arialFontRadioButton')[0]
- if self.fontFamily == 'Times':
- self.timesFontRadioButton.checked = True
- else:
- self.arialFontRadioButton.checked = True
-
- self.fontSizeSpinBox = find(window, 'fontSizeSpinBox')[0]
- self.fontSizeSpinBox.value = self.fontSize
-
- self.backgroundPersistenceCheckBox = find(window, 'backgroundPersistenceCheckBox')[0]
- self.backgroundPersistenceCheckBox.checked = self.backgroundDICOMAnnotationsPersistence
-
- self.annotationsAmountGroupBox = find(window, 'annotationsAmountGroupBox')[0]
-
- self.restoreDefaultsButton = find(window, 'restoreDefaultsButton')[0]
-
- self.updateEnabledButtons()
-
- # connections
- self.sliceViewAnnotationsCheckBox.connect('clicked()', self.onSliceViewAnnotationsCheckBox)
-
- self.topLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
- self.topRightCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
- self.bottomLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
- self.timesFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton)
- self.arialFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton)
- self.fontSizeSpinBox.connect('valueChanged(int)', self.onFontSizeSpinBox)
-
- self.level1RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
- self.level2RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
- self.level3RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
-
- self.backgroundPersistenceCheckBox.connect('clicked()', self.onBackgroundLayerPersistenceCheckBox)
-
- self.restoreDefaultsButton.connect('clicked()', self.restoreDefaultValues)
-
- def onLayoutManagerDestroyed(self):
- self.layoutManager = slicer.app.layoutManager()
- if self.layoutManager:
- self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed)
-
- def onSliceViewAnnotationsCheckBox(self):
- if self.sliceViewAnnotationsCheckBox.checked:
- self.sliceViewAnnotationsEnabled = 1
- else:
- self.sliceViewAnnotationsEnabled = 0
-
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled)
-
- self.updateEnabledButtons()
- self.updateSliceViewFromGUI()
-
- def onBackgroundLayerPersistenceCheckBox(self):
- if self.backgroundPersistenceCheckBox.checked:
- self.backgroundDICOMAnnotationsPersistence = 1
- else:
- self.backgroundDICOMAnnotationsPersistence = 0
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence',
- self.backgroundDICOMAnnotationsPersistence)
- self.updateSliceViewFromGUI()
-
- def onCornerTextsActivationCheckBox(self):
- self.topLeft = int(self.topLeftCheckBox.checked)
- self.topRight = int(self.topRightCheckBox.checked)
- self.bottomLeft = int(self.bottomLeftCheckBox.checked)
-
- self.updateSliceViewFromGUI()
-
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.topLeft',
- self.topLeft)
- settings.setValue('DataProbe/sliceViewAnnotations.topRight',
- self.topRight)
- settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft',
- self.bottomLeft)
-
- def onFontFamilyRadioButton(self):
- # Updating font size and family
- if self.timesFontRadioButton.checked:
- self.fontFamily = 'Times'
- else:
- self.fontFamily = 'Arial'
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.fontFamily',
- self.fontFamily)
- self.updateSliceViewFromGUI()
-
- def onFontSizeSpinBox(self):
- self.fontSize = self.fontSizeSpinBox.value
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.fontSize',
- self.fontSize)
- self.updateSliceViewFromGUI()
-
- def restoreDefaultValues(self):
- self.topLeftCheckBox.checked = True
- self.topLeft = 1
- self.topRightCheckBox.checked = True
- self.topRight = 1
- self.bottomLeftCheckBox.checked = True
- self.bottomLeft = 1
- self.fontSizeSpinBox.value = 14
- self.timesFontRadioButton.checked = True
- self.fontFamily = 'Times'
- self.backgroundDICOMAnnotationsPersistence = 0
- self.backgroundPersistenceCheckBox.checked = False
-
- settings = qt.QSettings()
- settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled)
- settings.setValue('DataProbe/sliceViewAnnotations.topLeft', self.topLeft)
- settings.setValue('DataProbe/sliceViewAnnotations.topRight', self.topRight)
- settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', self.bottomLeft)
- settings.setValue('DataProbe/sliceViewAnnotations.fontFamily', self.fontFamily)
- settings.setValue('DataProbe/sliceViewAnnotations.fontSize', self.fontSize)
- settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence',
- self.backgroundDICOMAnnotationsPersistence)
-
- self.updateSliceViewFromGUI()
-
- def updateGUIFromMRML(self, caller, event):
- if self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter) == '':
- # parameter does not exist - probably initializing
- return
- self.sliceViewAnnotationsEnabled = int(self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter))
- self.updateSliceViewFromGUI()
-
- def updateEnabledButtons(self):
- enabled = self.sliceViewAnnotationsEnabled
-
- self.cornerTextParametersCollapsibleButton.enabled = enabled
- self.activateCornersGroupBox.enabled = enabled
- self.fontPropertiesGroupBox.enabled = enabled
- self.annotationsAmountGroupBox.enabled = enabled
- self.restoreDefaultsButton.enabled = enabled
-
- def updateSliceViewFromGUI(self):
- if not self.sliceViewAnnotationsEnabled:
- self.removeObservers(method=self.updateViewAnnotations)
- self.removeObservers(method=self.updateGUIFromMRML)
- return
-
- # Create corner annotations if have not created already
- if len(self.sliceViewNames) == 0:
- self.createCornerAnnotations()
-
- # Updating Annotations Amount
- if self.level1RadioButton.checked:
- self.annotationsDisplayAmount = 0
- elif self.level2RadioButton.checked:
- self.annotationsDisplayAmount = 1
- elif self.level3RadioButton.checked:
- self.annotationsDisplayAmount = 2
-
- for sliceViewName in self.sliceViewNames:
- sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
- if sliceWidget:
- sl = sliceWidget.sliceLogic()
- self.updateCornerAnnotation(sl)
-
- def createGlobalVariables(self):
- self.sliceViewNames = []
- self.sliceWidgets = {}
- self.sliceViews = {}
- self.renderers = {}
-
- def createCornerAnnotations(self):
- self.createGlobalVariables()
- self.sliceViewNames = list(self.layoutManager.sliceViewNames())
- for sliceViewName in self.sliceViewNames:
- self.addSliceViewObserver(sliceViewName)
- self.createActors(sliceViewName)
-
- def addSliceViewObserver(self, sliceViewName):
- sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
- self.sliceWidgets[sliceViewName] = sliceWidget
- sliceView = sliceWidget.sliceView()
-
- renderWindow = sliceView.renderWindow()
- renderer = renderWindow.GetRenderers().GetItemAsObject(0)
- self.renderers[sliceViewName] = renderer
-
- self.sliceViews[sliceViewName] = sliceView
- sliceLogic = sliceWidget.sliceLogic()
- self.addObserver(sliceLogic, vtk.vtkCommand.ModifiedEvent, self.updateViewAnnotations)
-
- def createActors(self, sliceViewName):
- sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
- self.sliceWidgets[sliceViewName] = sliceWidget
-
- def updateViewAnnotations(self, caller, event):
- if not self.sliceViewAnnotationsEnabled:
- # when self.sliceViewAnnotationsEnabled is set to false
- # then annotation and scalar bar gets hidden, therefore
- # we have nothing to do here
- return
-
- layoutManager = self.layoutManager
- if layoutManager is None:
- return
- sliceViewNames = layoutManager.sliceViewNames()
- for sliceViewName in sliceViewNames:
- if sliceViewName not in self.sliceViewNames:
- self.sliceViewNames.append(sliceViewName)
- self.addSliceViewObserver(sliceViewName)
- self.createActors(sliceViewName)
+ """Implement the Qt window showing settings for Slice View Annotations
+ """
+
+ def __init__(self, layoutManager=None):
+ VTKObservationMixin.__init__(self)
+
+ self.layoutManager = layoutManager
+ if self.layoutManager is None:
+ self.layoutManager = slicer.app.layoutManager()
+ self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed)
+
+ self.dataProbeUtil = DataProbeUtil.DataProbeUtil()
+
+ self.dicomVolumeNode = 0
+
+ # Cache recently used extracted DICOM values.
+ # Getting all necessary DICOM values from the database (tag cache)
+ # would slow down slice browsing significantly.
+ # We may have several different volumes shown in different slice views,
+ # so we keep in the cache a number of items, not just 2.
+ self.extractedDICOMValuesCacheSize = 12
+ import collections
+ self.extractedDICOMValuesCache = collections.OrderedDict()
+
+ self.sliceViewNames = []
+ self.popupGeometry = qt.QRect()
+ self.cornerTexts = []
+ # Bottom Left Corner Text
+ self.cornerTexts.append({
+ '1-Label': {'text': '', 'category': 'A'},
+ '2-Foreground': {'text': '', 'category': 'A'},
+ '3-Background': {'text': '', 'category': 'A'}
+ })
+ # Bottom Right Corner Text
+ # Not used - orientation figure may be drawn there
+ self.cornerTexts.append({
+ '1-TR': {'text': '', 'category': 'A'},
+ '2-TE': {'text': '', 'category': 'A'}
+ })
+ # Top Left Corner Text
+ self.cornerTexts.append({
+ '1-PatientName': {'text': '', 'category': 'B'},
+ '2-PatientID': {'text': '', 'category': 'A'},
+ '3-PatientInfo': {'text': '', 'category': 'B'},
+ '4-Bg-SeriesDate': {'text': '', 'category': 'B'},
+ '5-Fg-SeriesDate': {'text': '', 'category': 'B'},
+ '6-Bg-SeriesTime': {'text': '', 'category': 'C'},
+ '7-Bg-SeriesTime': {'text': '', 'category': 'C'},
+ '8-Bg-SeriesDescription': {'text': '', 'category': 'C'},
+ '9-Fg-SeriesDescription': {'text': '', 'category': 'C'}
+ })
+ # Top Right Corner Text
+ self.cornerTexts.append({
+ '1-Institution-Name': {'text': '', 'category': 'B'},
+ '2-Referring-Phisycian': {'text': '', 'category': 'B'},
+ '3-Manufacturer': {'text': '', 'category': 'C'},
+ '4-Model': {'text': '', 'category': 'C'},
+ '5-Patient-Position': {'text': '', 'category': 'A'},
+ '6-TR': {'text': '', 'category': 'A'},
+ '7-TE': {'text': '', 'category': 'A'}
+ })
+
+ self.annotationsDisplayAmount = 0
+
+ #
+ self.scene = slicer.mrmlScene
+ self.sliceViews = {}
+
+ # If there are no user settings load defaults
+ self.sliceViewAnnotationsEnabled = settingsValue('DataProbe/sliceViewAnnotations.enabled', 1, converter=int)
+
+ self.bottomLeft = settingsValue('DataProbe/sliceViewAnnotations.bottomLeft', 1, converter=int)
+ self.topLeft = settingsValue('DataProbe/sliceViewAnnotations.topLeft', 0, converter=int)
+ self.topRight = settingsValue('DataProbe/sliceViewAnnotations.topRight', 0, converter=int)
+ self.fontFamily = settingsValue('DataProbe/sliceViewAnnotations.fontFamily', 'Times')
+ self.fontSize = settingsValue('DataProbe/sliceViewAnnotations.fontSize', 14, converter=int)
+ self.backgroundDICOMAnnotationsPersistence = settingsValue(
+ 'DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence', 0, converter=int)
+ self.sliceViewAnnotationsEnabledparameter = 'sliceViewAnnotationsEnabled'
+ self.parameterNode = self.dataProbeUtil.getParameterNode()
+ self.addObserver(self.parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromMRML)
+
+ self.maximumTextLength = 35
+
+ self.create()
+
+ if self.sliceViewAnnotationsEnabled:
+ self.updateSliceViewFromGUI()
+
+ def create(self):
+ # Instantiate and connect widgets ...
+ loader = qt.QUiLoader()
+ path = os.path.join(os.path.dirname(__file__), 'Resources', 'UI', 'settings.ui')
+ qfile = qt.QFile(path)
+ qfile.open(qt.QFile.ReadOnly)
+ self.window = loader.load(qfile)
+ window = self.window
+
+ find = slicer.util.findChildren
+ self.cornerTextParametersCollapsibleButton = find(window, 'cornerTextParametersCollapsibleButton')[0]
+ self.sliceViewAnnotationsCheckBox = find(window, 'sliceViewAnnotationsCheckBox')[0]
+ self.sliceViewAnnotationsCheckBox.checked = self.sliceViewAnnotationsEnabled
+
+ self.activateCornersGroupBox = find(window, 'activateCornersGroupBox')[0]
+ self.topLeftCheckBox = find(window, 'topLeftCheckBox')[0]
+ self.topLeftCheckBox.checked = self.topLeft
+ self.topRightCheckBox = find(window, 'topRightCheckBox')[0]
+ self.topRightCheckBox.checked = self.topRight
+
+ self.bottomLeftCheckBox = find(window, 'bottomLeftCheckBox')[0]
+ self.bottomLeftCheckBox.checked = self.bottomLeft
+
+ self.level1RadioButton = find(window, 'level1RadioButton')[0]
+ self.level2RadioButton = find(window, 'level2RadioButton')[0]
+ self.level3RadioButton = find(window, 'level3RadioButton')[0]
+
+ self.fontPropertiesGroupBox = find(window, 'fontPropertiesGroupBox')[0]
+ self.timesFontRadioButton = find(window, 'timesFontRadioButton')[0]
+ self.arialFontRadioButton = find(window, 'arialFontRadioButton')[0]
+ if self.fontFamily == 'Times':
+ self.timesFontRadioButton.checked = True
+ else:
+ self.arialFontRadioButton.checked = True
+
+ self.fontSizeSpinBox = find(window, 'fontSizeSpinBox')[0]
+ self.fontSizeSpinBox.value = self.fontSize
+
+ self.backgroundPersistenceCheckBox = find(window, 'backgroundPersistenceCheckBox')[0]
+ self.backgroundPersistenceCheckBox.checked = self.backgroundDICOMAnnotationsPersistence
+
+ self.annotationsAmountGroupBox = find(window, 'annotationsAmountGroupBox')[0]
+
+ self.restoreDefaultsButton = find(window, 'restoreDefaultsButton')[0]
+
+ self.updateEnabledButtons()
+
+ # connections
+ self.sliceViewAnnotationsCheckBox.connect('clicked()', self.onSliceViewAnnotationsCheckBox)
+
+ self.topLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
+ self.topRightCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
+ self.bottomLeftCheckBox.connect('clicked()', self.onCornerTextsActivationCheckBox)
+ self.timesFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton)
+ self.arialFontRadioButton.connect('clicked()', self.onFontFamilyRadioButton)
+ self.fontSizeSpinBox.connect('valueChanged(int)', self.onFontSizeSpinBox)
+
+ self.level1RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
+ self.level2RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
+ self.level3RadioButton.connect('clicked()', self.updateSliceViewFromGUI)
+
+ self.backgroundPersistenceCheckBox.connect('clicked()', self.onBackgroundLayerPersistenceCheckBox)
+
+ self.restoreDefaultsButton.connect('clicked()', self.restoreDefaultValues)
+
+ def onLayoutManagerDestroyed(self):
+ self.layoutManager = slicer.app.layoutManager()
+ if self.layoutManager:
+ self.layoutManager.connect("destroyed()", self.onLayoutManagerDestroyed)
+
+ def onSliceViewAnnotationsCheckBox(self):
+ if self.sliceViewAnnotationsCheckBox.checked:
+ self.sliceViewAnnotationsEnabled = 1
+ else:
+ self.sliceViewAnnotationsEnabled = 0
+
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled)
+
+ self.updateEnabledButtons()
self.updateSliceViewFromGUI()
- self.makeAnnotationText(caller)
-
- def updateCornerAnnotation(self, sliceLogic):
-
- sliceNode = sliceLogic.GetBackgroundLayer().GetSliceNode()
- sliceViewName = sliceNode.GetLayoutName()
-
- enabled = self.sliceViewAnnotationsEnabled
-
- cornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation()
-
- if enabled:
- # Font
- cornerAnnotation.SetMaximumFontSize(self.fontSize)
- cornerAnnotation.SetMinimumFontSize(self.fontSize)
- cornerAnnotation.SetNonlinearFontScaleFactor(1)
- textProperty = cornerAnnotation.GetTextProperty()
- if self.fontFamily == 'Times':
- textProperty.SetFontFamilyToTimes()
- else:
- textProperty.SetFontFamilyToArial()
- # Text
- self.makeAnnotationText(sliceLogic)
- else:
- # Clear Annotations
- for position in range(4):
- cornerAnnotation.SetText(position, "")
-
- self.sliceViews[sliceViewName].scheduleRender()
-
- def makeAnnotationText(self, sliceLogic):
- self.resetTexts()
- sliceCompositeNode = sliceLogic.GetSliceCompositeNode()
- if not sliceCompositeNode:
- return
-
- # Get the layers
- backgroundLayer = sliceLogic.GetBackgroundLayer()
- foregroundLayer = sliceLogic.GetForegroundLayer()
- labelLayer = sliceLogic.GetLabelLayer()
-
- # Get the volumes
- backgroundVolume = backgroundLayer.GetVolumeNode()
- foregroundVolume = foregroundLayer.GetVolumeNode()
- labelVolume = labelLayer.GetVolumeNode()
-
- # Get slice view name
- sliceNode = backgroundLayer.GetSliceNode()
- if not sliceNode:
- return
- sliceViewName = sliceNode.GetLayoutName()
-
- if self.sliceViews[sliceViewName]:
- #
- # Update slice corner annotations
- #
- # Case I: Both background and foregraound
- if (backgroundVolume is not None and foregroundVolume is not None):
- if self.bottomLeft:
- foregroundOpacity = sliceCompositeNode.GetForegroundOpacity()
- backgroundVolumeName = backgroundVolume.GetName()
- foregroundVolumeName = foregroundVolume.GetName()
- self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName
- self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName + ' (' + str(
- "%d" % (foregroundOpacity * 100)) + '%)'
-
- bgUids = backgroundVolume.GetAttribute('DICOM.instanceUIDs')
- fgUids = foregroundVolume.GetAttribute('DICOM.instanceUIDs')
- if (bgUids and fgUids):
- bgUid = bgUids.partition(' ')[0]
- fgUid = fgUids.partition(' ')[0]
- self.dicomVolumeNode = 1
- self.makeDicomAnnotation(bgUid, fgUid, sliceViewName)
- elif (bgUids and self.backgroundDICOMAnnotationsPersistence):
- uid = bgUids.partition(' ')[0]
- self.dicomVolumeNode = 1
- self.makeDicomAnnotation(uid, None, sliceViewName)
+
+ def onBackgroundLayerPersistenceCheckBox(self):
+ if self.backgroundPersistenceCheckBox.checked:
+ self.backgroundDICOMAnnotationsPersistence = 1
+ else:
+ self.backgroundDICOMAnnotationsPersistence = 0
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence',
+ self.backgroundDICOMAnnotationsPersistence)
+ self.updateSliceViewFromGUI()
+
+ def onCornerTextsActivationCheckBox(self):
+ self.topLeft = int(self.topLeftCheckBox.checked)
+ self.topRight = int(self.topRightCheckBox.checked)
+ self.bottomLeft = int(self.bottomLeftCheckBox.checked)
+
+ self.updateSliceViewFromGUI()
+
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.topLeft',
+ self.topLeft)
+ settings.setValue('DataProbe/sliceViewAnnotations.topRight',
+ self.topRight)
+ settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft',
+ self.bottomLeft)
+
+ def onFontFamilyRadioButton(self):
+ # Updating font size and family
+ if self.timesFontRadioButton.checked:
+ self.fontFamily = 'Times'
else:
- for key in self.cornerTexts[2]:
- self.cornerTexts[2][key]['text'] = ''
- self.dicomVolumeNode = 0
-
- # Case II: Only background
- elif (backgroundVolume is not None):
- backgroundVolumeName = backgroundVolume.GetName()
- if self.bottomLeft:
- self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName
-
- uids = backgroundVolume.GetAttribute('DICOM.instanceUIDs')
- if uids:
- uid = uids.partition(' ')[0]
- self.makeDicomAnnotation(uid, None, sliceViewName)
- self.dicomVolumeNode = 1
+ self.fontFamily = 'Arial'
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.fontFamily',
+ self.fontFamily)
+ self.updateSliceViewFromGUI()
+
+ def onFontSizeSpinBox(self):
+ self.fontSize = self.fontSizeSpinBox.value
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.fontSize',
+ self.fontSize)
+ self.updateSliceViewFromGUI()
+
+ def restoreDefaultValues(self):
+ self.topLeftCheckBox.checked = True
+ self.topLeft = 1
+ self.topRightCheckBox.checked = True
+ self.topRight = 1
+ self.bottomLeftCheckBox.checked = True
+ self.bottomLeft = 1
+ self.fontSizeSpinBox.value = 14
+ self.timesFontRadioButton.checked = True
+ self.fontFamily = 'Times'
+ self.backgroundDICOMAnnotationsPersistence = 0
+ self.backgroundPersistenceCheckBox.checked = False
+
+ settings = qt.QSettings()
+ settings.setValue('DataProbe/sliceViewAnnotations.enabled', self.sliceViewAnnotationsEnabled)
+ settings.setValue('DataProbe/sliceViewAnnotations.topLeft', self.topLeft)
+ settings.setValue('DataProbe/sliceViewAnnotations.topRight', self.topRight)
+ settings.setValue('DataProbe/sliceViewAnnotations.bottomLeft', self.bottomLeft)
+ settings.setValue('DataProbe/sliceViewAnnotations.fontFamily', self.fontFamily)
+ settings.setValue('DataProbe/sliceViewAnnotations.fontSize', self.fontSize)
+ settings.setValue('DataProbe/sliceViewAnnotations.bgDICOMAnnotationsPersistence',
+ self.backgroundDICOMAnnotationsPersistence)
+
+ self.updateSliceViewFromGUI()
+
+ def updateGUIFromMRML(self, caller, event):
+ if self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter) == '':
+ # parameter does not exist - probably initializing
+ return
+ self.sliceViewAnnotationsEnabled = int(self.parameterNode.GetParameter(self.sliceViewAnnotationsEnabledparameter))
+ self.updateSliceViewFromGUI()
+
+ def updateEnabledButtons(self):
+ enabled = self.sliceViewAnnotationsEnabled
+
+ self.cornerTextParametersCollapsibleButton.enabled = enabled
+ self.activateCornersGroupBox.enabled = enabled
+ self.fontPropertiesGroupBox.enabled = enabled
+ self.annotationsAmountGroupBox.enabled = enabled
+ self.restoreDefaultsButton.enabled = enabled
+
+ def updateSliceViewFromGUI(self):
+ if not self.sliceViewAnnotationsEnabled:
+ self.removeObservers(method=self.updateViewAnnotations)
+ self.removeObservers(method=self.updateGUIFromMRML)
+ return
+
+ # Create corner annotations if have not created already
+ if len(self.sliceViewNames) == 0:
+ self.createCornerAnnotations()
+
+ # Updating Annotations Amount
+ if self.level1RadioButton.checked:
+ self.annotationsDisplayAmount = 0
+ elif self.level2RadioButton.checked:
+ self.annotationsDisplayAmount = 1
+ elif self.level3RadioButton.checked:
+ self.annotationsDisplayAmount = 2
+
+ for sliceViewName in self.sliceViewNames:
+ sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
+ if sliceWidget:
+ sl = sliceWidget.sliceLogic()
+ self.updateCornerAnnotation(sl)
+
+ def createGlobalVariables(self):
+ self.sliceViewNames = []
+ self.sliceWidgets = {}
+ self.sliceViews = {}
+ self.renderers = {}
+
+ def createCornerAnnotations(self):
+ self.createGlobalVariables()
+ self.sliceViewNames = list(self.layoutManager.sliceViewNames())
+ for sliceViewName in self.sliceViewNames:
+ self.addSliceViewObserver(sliceViewName)
+ self.createActors(sliceViewName)
+
+ def addSliceViewObserver(self, sliceViewName):
+ sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
+ self.sliceWidgets[sliceViewName] = sliceWidget
+ sliceView = sliceWidget.sliceView()
+
+ renderWindow = sliceView.renderWindow()
+ renderer = renderWindow.GetRenderers().GetItemAsObject(0)
+ self.renderers[sliceViewName] = renderer
+
+ self.sliceViews[sliceViewName] = sliceView
+ sliceLogic = sliceWidget.sliceLogic()
+ self.addObserver(sliceLogic, vtk.vtkCommand.ModifiedEvent, self.updateViewAnnotations)
+
+ def createActors(self, sliceViewName):
+ sliceWidget = self.layoutManager.sliceWidget(sliceViewName)
+ self.sliceWidgets[sliceViewName] = sliceWidget
+
+ def updateViewAnnotations(self, caller, event):
+ if not self.sliceViewAnnotationsEnabled:
+ # when self.sliceViewAnnotationsEnabled is set to false
+ # then annotation and scalar bar gets hidden, therefore
+ # we have nothing to do here
+ return
+
+ layoutManager = self.layoutManager
+ if layoutManager is None:
+ return
+ sliceViewNames = layoutManager.sliceViewNames()
+ for sliceViewName in sliceViewNames:
+ if sliceViewName not in self.sliceViewNames:
+ self.sliceViewNames.append(sliceViewName)
+ self.addSliceViewObserver(sliceViewName)
+ self.createActors(sliceViewName)
+ self.updateSliceViewFromGUI()
+ self.makeAnnotationText(caller)
+
+ def updateCornerAnnotation(self, sliceLogic):
+
+ sliceNode = sliceLogic.GetBackgroundLayer().GetSliceNode()
+ sliceViewName = sliceNode.GetLayoutName()
+
+ enabled = self.sliceViewAnnotationsEnabled
+
+ cornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation()
+
+ if enabled:
+ # Font
+ cornerAnnotation.SetMaximumFontSize(self.fontSize)
+ cornerAnnotation.SetMinimumFontSize(self.fontSize)
+ cornerAnnotation.SetNonlinearFontScaleFactor(1)
+ textProperty = cornerAnnotation.GetTextProperty()
+ if self.fontFamily == 'Times':
+ textProperty.SetFontFamilyToTimes()
+ else:
+ textProperty.SetFontFamilyToArial()
+ # Text
+ self.makeAnnotationText(sliceLogic)
else:
- self.dicomVolumeNode = 0
-
- # Case III: Only foreground
- elif (foregroundVolume is not None):
- if self.bottomLeft:
- foregroundVolumeName = foregroundVolume.GetName()
- self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName
-
- uids = foregroundVolume.GetAttribute('DICOM.instanceUIDs')
- if uids:
- uid = uids.partition(' ')[0]
- # passed UID as bg
- self.makeDicomAnnotation(uid, None, sliceViewName)
- self.dicomVolumeNode = 1
+ # Clear Annotations
+ for position in range(4):
+ cornerAnnotation.SetText(position, "")
+
+ self.sliceViews[sliceViewName].scheduleRender()
+
+ def makeAnnotationText(self, sliceLogic):
+ self.resetTexts()
+ sliceCompositeNode = sliceLogic.GetSliceCompositeNode()
+ if not sliceCompositeNode:
+ return
+
+ # Get the layers
+ backgroundLayer = sliceLogic.GetBackgroundLayer()
+ foregroundLayer = sliceLogic.GetForegroundLayer()
+ labelLayer = sliceLogic.GetLabelLayer()
+
+ # Get the volumes
+ backgroundVolume = backgroundLayer.GetVolumeNode()
+ foregroundVolume = foregroundLayer.GetVolumeNode()
+ labelVolume = labelLayer.GetVolumeNode()
+
+ # Get slice view name
+ sliceNode = backgroundLayer.GetSliceNode()
+ if not sliceNode:
+ return
+ sliceViewName = sliceNode.GetLayoutName()
+
+ if self.sliceViews[sliceViewName]:
+ #
+ # Update slice corner annotations
+ #
+ # Case I: Both background and foregraound
+ if (backgroundVolume is not None and foregroundVolume is not None):
+ if self.bottomLeft:
+ foregroundOpacity = sliceCompositeNode.GetForegroundOpacity()
+ backgroundVolumeName = backgroundVolume.GetName()
+ foregroundVolumeName = foregroundVolume.GetName()
+ self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName
+ self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName + ' (' + str(
+ "%d" % (foregroundOpacity * 100)) + '%)'
+
+ bgUids = backgroundVolume.GetAttribute('DICOM.instanceUIDs')
+ fgUids = foregroundVolume.GetAttribute('DICOM.instanceUIDs')
+ if (bgUids and fgUids):
+ bgUid = bgUids.partition(' ')[0]
+ fgUid = fgUids.partition(' ')[0]
+ self.dicomVolumeNode = 1
+ self.makeDicomAnnotation(bgUid, fgUid, sliceViewName)
+ elif (bgUids and self.backgroundDICOMAnnotationsPersistence):
+ uid = bgUids.partition(' ')[0]
+ self.dicomVolumeNode = 1
+ self.makeDicomAnnotation(uid, None, sliceViewName)
+ else:
+ for key in self.cornerTexts[2]:
+ self.cornerTexts[2][key]['text'] = ''
+ self.dicomVolumeNode = 0
+
+ # Case II: Only background
+ elif (backgroundVolume is not None):
+ backgroundVolumeName = backgroundVolume.GetName()
+ if self.bottomLeft:
+ self.cornerTexts[0]['3-Background']['text'] = 'B: ' + backgroundVolumeName
+
+ uids = backgroundVolume.GetAttribute('DICOM.instanceUIDs')
+ if uids:
+ uid = uids.partition(' ')[0]
+ self.makeDicomAnnotation(uid, None, sliceViewName)
+ self.dicomVolumeNode = 1
+ else:
+ self.dicomVolumeNode = 0
+
+ # Case III: Only foreground
+ elif (foregroundVolume is not None):
+ if self.bottomLeft:
+ foregroundVolumeName = foregroundVolume.GetName()
+ self.cornerTexts[0]['2-Foreground']['text'] = 'F: ' + foregroundVolumeName
+
+ uids = foregroundVolume.GetAttribute('DICOM.instanceUIDs')
+ if uids:
+ uid = uids.partition(' ')[0]
+ # passed UID as bg
+ self.makeDicomAnnotation(uid, None, sliceViewName)
+ self.dicomVolumeNode = 1
+ else:
+ self.dicomVolumeNode = 0
+
+ if (labelVolume is not None) and self.bottomLeft:
+ labelOpacity = sliceCompositeNode.GetLabelOpacity()
+ labelVolumeName = labelVolume.GetName()
+ self.cornerTexts[0]['1-Label']['text'] = 'L: ' + labelVolumeName + ' (' + str(
+ "%d" % (labelOpacity * 100)) + '%)'
+
+ self.drawCornerAnnotations(sliceViewName)
+
+ def makeDicomAnnotation(self, bgUid, fgUid, sliceViewName):
+ # Do not attempt to retrieve dicom values if no local database exists
+ if not slicer.dicomDatabase.isOpen:
+ return
+ viewHeight = self.sliceViews[sliceViewName].height
+ if fgUid is not None and bgUid is not None:
+ backgroundDicomDic = self.extractDICOMValues(bgUid)
+ foregroundDicomDic = self.extractDICOMValues(fgUid)
+ # check if background and foreground are from different patients
+ # and remove the annotations
+
+ if self.topLeft and viewHeight > 150:
+ if backgroundDicomDic['Patient Name'] != foregroundDicomDic['Patient Name'
+ ] or backgroundDicomDic['Patient ID'] != foregroundDicomDic['Patient ID'
+ ] or backgroundDicomDic['Patient Birth Date'] != foregroundDicomDic['Patient Birth Date']:
+ for key in self.cornerTexts[2]:
+ self.cornerTexts[2][key]['text'] = ''
+ else:
+ if '1-PatientName' in self.cornerTexts[2]:
+ self.cornerTexts[2]['1-PatientName']['text'] = backgroundDicomDic['Patient Name'].replace('^', ', ')
+ if '2-PatientID' in self.cornerTexts[2]:
+ self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + backgroundDicomDic['Patient ID']
+ backgroundDicomDic['Patient Birth Date'] = self.formatDICOMDate(backgroundDicomDic['Patient Birth Date'])
+ if '3-PatientInfo' in self.cornerTexts[2]:
+ self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(backgroundDicomDic)
+
+ if (backgroundDicomDic['Series Date'] != foregroundDicomDic['Series Date']):
+ if '4-Bg-SeriesDate' in self.cornerTexts[2]:
+ self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = 'B: ' + self.formatDICOMDate(backgroundDicomDic['Series Date'])
+ if '5-Fg-SeriesDate' in self.cornerTexts[2]:
+ self.cornerTexts[2]['5-Fg-SeriesDate']['text'] = 'F: ' + self.formatDICOMDate(foregroundDicomDic['Series Date'])
+ else:
+ if '4-Bg-SeriesDate' in self.cornerTexts[2]:
+ self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(backgroundDicomDic['Series Date'])
+
+ if (backgroundDicomDic['Series Time'] != foregroundDicomDic['Series Time']):
+ if '6-Bg-SeriesTime' in self.cornerTexts[2]:
+ self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = 'B: ' + self.formatDICOMTime(backgroundDicomDic['Series Time'])
+ if '7-Fg-SeriesTime' in self.cornerTexts[2]:
+ self.cornerTexts[2]['7-Fg-SeriesTime']['text'] = 'F: ' + self.formatDICOMTime(foregroundDicomDic['Series Time'])
+ else:
+ if '6-Bg-SeriesTime' in self.cornerTexts[2]:
+ self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(backgroundDicomDic['Series Time'])
+
+ if (backgroundDicomDic['Series Description'] != foregroundDicomDic['Series Description']):
+ if '8-Bg-SeriesDescription' in self.cornerTexts[2]:
+ self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = 'B: ' + backgroundDicomDic['Series Description']
+ if '9-Fg-SeriesDescription' in self.cornerTexts[2]:
+ self.cornerTexts[2]['9-Fg-SeriesDescription']['text'] = 'F: ' + foregroundDicomDic['Series Description']
+ else:
+ if '8-Bg-SeriesDescription' in self.cornerTexts[2]:
+ self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = backgroundDicomDic['Series Description']
+
+ # Only Background or Only Foreground
else:
- self.dicomVolumeNode = 0
-
- if (labelVolume is not None) and self.bottomLeft:
- labelOpacity = sliceCompositeNode.GetLabelOpacity()
- labelVolumeName = labelVolume.GetName()
- self.cornerTexts[0]['1-Label']['text'] = 'L: ' + labelVolumeName + ' (' + str(
- "%d" % (labelOpacity * 100)) + '%)'
-
- self.drawCornerAnnotations(sliceViewName)
-
- def makeDicomAnnotation(self, bgUid, fgUid, sliceViewName):
- # Do not attempt to retrieve dicom values if no local database exists
- if not slicer.dicomDatabase.isOpen:
- return
- viewHeight = self.sliceViews[sliceViewName].height
- if fgUid is not None and bgUid is not None:
- backgroundDicomDic = self.extractDICOMValues(bgUid)
- foregroundDicomDic = self.extractDICOMValues(fgUid)
- # check if background and foreground are from different patients
- # and remove the annotations
-
- if self.topLeft and viewHeight > 150:
- if backgroundDicomDic['Patient Name'] != foregroundDicomDic['Patient Name'
- ] or backgroundDicomDic['Patient ID'] != foregroundDicomDic['Patient ID'
- ] or backgroundDicomDic['Patient Birth Date'] != foregroundDicomDic['Patient Birth Date']:
- for key in self.cornerTexts[2]:
- self.cornerTexts[2][key]['text'] = ''
+ uid = bgUid
+ dicomDic = self.extractDICOMValues(uid)
+
+ if self.topLeft and viewHeight > 150:
+ self.cornerTexts[2]['1-PatientName']['text'] = dicomDic['Patient Name'].replace('^', ', ')
+ self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + dicomDic['Patient ID']
+ dicomDic['Patient Birth Date'] = self.formatDICOMDate(dicomDic['Patient Birth Date'])
+ self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(dicomDic)
+ self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(dicomDic['Series Date'])
+ self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(dicomDic['Series Time'])
+ self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = dicomDic['Series Description']
+
+ # top right corner annotation would be hidden if view height is less than 260 pixels
+ if (self.topRight):
+ self.cornerTexts[3]['1-Institution-Name']['text'] = dicomDic['Institution Name']
+ self.cornerTexts[3]['2-Referring-Phisycian']['text'] = dicomDic['Referring Physician Name'].replace('^', ', ')
+ self.cornerTexts[3]['3-Manufacturer']['text'] = dicomDic['Manufacturer']
+ self.cornerTexts[3]['4-Model']['text'] = dicomDic['Model']
+ self.cornerTexts[3]['5-Patient-Position']['text'] = dicomDic['Patient Position']
+ modality = dicomDic['Modality']
+ if modality == 'MR':
+ self.cornerTexts[3]['6-TR']['text'] = 'TR ' + dicomDic['Repetition Time']
+ self.cornerTexts[3]['7-TE']['text'] = 'TE ' + dicomDic['Echo Time']
+
+ @staticmethod
+ def makePatientInfo(dicomDic):
+ # This will give an string of patient's birth date,
+ # patient's age and sex
+ patientInfo = dicomDic['Patient Birth Date'
+ ] + ', ' + dicomDic['Patient Age'
+ ] + ', ' + dicomDic['Patient Sex']
+ return patientInfo
+
+ @staticmethod
+ def formatDICOMDate(date):
+ standardDate = ''
+ if date != '':
+ date = date.rstrip()
+ # convert to ISO 8601 Date format
+ standardDate = date[:4] + '-' + date[4:6] + '-' + date[6:]
+ return standardDate
+
+ @staticmethod
+ def formatDICOMTime(time):
+ if time == '':
+ # time field is empty
+ return ''
+ studyH = time[:2]
+ if int(studyH) > 12:
+ studyH = str(int(studyH) - 12)
+ clockTime = ' PM'
else:
- if '1-PatientName' in self.cornerTexts[2]:
- self.cornerTexts[2]['1-PatientName']['text'] = backgroundDicomDic['Patient Name'].replace('^', ', ')
- if '2-PatientID' in self.cornerTexts[2]:
- self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + backgroundDicomDic['Patient ID']
- backgroundDicomDic['Patient Birth Date'] = self.formatDICOMDate(backgroundDicomDic['Patient Birth Date'])
- if '3-PatientInfo' in self.cornerTexts[2]:
- self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(backgroundDicomDic)
-
- if (backgroundDicomDic['Series Date'] != foregroundDicomDic['Series Date']):
- if '4-Bg-SeriesDate' in self.cornerTexts[2]:
- self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = 'B: ' + self.formatDICOMDate(backgroundDicomDic['Series Date'])
- if '5-Fg-SeriesDate' in self.cornerTexts[2]:
- self.cornerTexts[2]['5-Fg-SeriesDate']['text'] = 'F: ' + self.formatDICOMDate(foregroundDicomDic['Series Date'])
- else:
- if '4-Bg-SeriesDate' in self.cornerTexts[2]:
- self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(backgroundDicomDic['Series Date'])
-
- if (backgroundDicomDic['Series Time'] != foregroundDicomDic['Series Time']):
- if '6-Bg-SeriesTime' in self.cornerTexts[2]:
- self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = 'B: ' + self.formatDICOMTime(backgroundDicomDic['Series Time'])
- if '7-Fg-SeriesTime' in self.cornerTexts[2]:
- self.cornerTexts[2]['7-Fg-SeriesTime']['text'] = 'F: ' + self.formatDICOMTime(foregroundDicomDic['Series Time'])
- else:
- if '6-Bg-SeriesTime' in self.cornerTexts[2]:
- self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(backgroundDicomDic['Series Time'])
-
- if (backgroundDicomDic['Series Description'] != foregroundDicomDic['Series Description']):
- if '8-Bg-SeriesDescription' in self.cornerTexts[2]:
- self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = 'B: ' + backgroundDicomDic['Series Description']
- if '9-Fg-SeriesDescription' in self.cornerTexts[2]:
- self.cornerTexts[2]['9-Fg-SeriesDescription']['text'] = 'F: ' + foregroundDicomDic['Series Description']
- else:
- if '8-Bg-SeriesDescription' in self.cornerTexts[2]:
- self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = backgroundDicomDic['Series Description']
-
- # Only Background or Only Foreground
- else:
- uid = bgUid
- dicomDic = self.extractDICOMValues(uid)
-
- if self.topLeft and viewHeight > 150:
- self.cornerTexts[2]['1-PatientName']['text'] = dicomDic['Patient Name'].replace('^', ', ')
- self.cornerTexts[2]['2-PatientID']['text'] = 'ID: ' + dicomDic['Patient ID']
- dicomDic['Patient Birth Date'] = self.formatDICOMDate(dicomDic['Patient Birth Date'])
- self.cornerTexts[2]['3-PatientInfo']['text'] = self.makePatientInfo(dicomDic)
- self.cornerTexts[2]['4-Bg-SeriesDate']['text'] = self.formatDICOMDate(dicomDic['Series Date'])
- self.cornerTexts[2]['6-Bg-SeriesTime']['text'] = self.formatDICOMTime(dicomDic['Series Time'])
- self.cornerTexts[2]['8-Bg-SeriesDescription']['text'] = dicomDic['Series Description']
-
- # top right corner annotation would be hidden if view height is less than 260 pixels
- if (self.topRight):
- self.cornerTexts[3]['1-Institution-Name']['text'] = dicomDic['Institution Name']
- self.cornerTexts[3]['2-Referring-Phisycian']['text'] = dicomDic['Referring Physician Name'].replace('^', ', ')
- self.cornerTexts[3]['3-Manufacturer']['text'] = dicomDic['Manufacturer']
- self.cornerTexts[3]['4-Model']['text'] = dicomDic['Model']
- self.cornerTexts[3]['5-Patient-Position']['text'] = dicomDic['Patient Position']
- modality = dicomDic['Modality']
- if modality == 'MR':
- self.cornerTexts[3]['6-TR']['text'] = 'TR ' + dicomDic['Repetition Time']
- self.cornerTexts[3]['7-TE']['text'] = 'TE ' + dicomDic['Echo Time']
-
- @staticmethod
- def makePatientInfo(dicomDic):
- # This will give an string of patient's birth date,
- # patient's age and sex
- patientInfo = dicomDic['Patient Birth Date'
- ] + ', ' + dicomDic['Patient Age'
- ] + ', ' + dicomDic['Patient Sex']
- return patientInfo
-
- @staticmethod
- def formatDICOMDate(date):
- standardDate = ''
- if date != '':
- date = date.rstrip()
- # convert to ISO 8601 Date format
- standardDate = date[:4] + '-' + date[4:6] + '-' + date[6:]
- return standardDate
-
- @staticmethod
- def formatDICOMTime(time):
- if time == '':
- # time field is empty
- return ''
- studyH = time[:2]
- if int(studyH) > 12:
- studyH = str(int(studyH) - 12)
- clockTime = ' PM'
- else:
- studyH = studyH
- clockTime = ' AM'
- studyM = time[2:4]
- studyS = time[4:6]
- return studyH + ':' + studyM + ':' + studyS + clockTime
-
- @staticmethod
- def fitText(text, textSize):
- if len(text) > textSize:
- preSize = int(textSize / 2)
- postSize = preSize - 3
- text = text[:preSize] + "..." + text[-postSize:]
- return text
-
- def drawCornerAnnotations(self, sliceViewName):
- if not self.sliceViewAnnotationsEnabled:
- return
- # Auto-Adjust
- # adjust maximum text length based on fontsize and view width
- viewWidth = self.sliceViews[sliceViewName].width
- self.maximumTextLength = int((viewWidth - 40) / self.fontSize)
-
- for i, cornerText in enumerate(self.cornerTexts):
- keys = sorted(cornerText.keys())
- cornerAnnotation = ''
- for key in keys:
- text = cornerText[key]['text']
- if (text != ''):
- text = self.fitText(text, self.maximumTextLength)
- # level 1: All categories will be displayed
- if self.annotationsDisplayAmount == 0:
- cornerAnnotation = cornerAnnotation + text + '\n'
- # level 2: Category A and B will be displayed
- elif self.annotationsDisplayAmount == 1:
- if (cornerText[key]['category'] != 'C'):
- cornerAnnotation = cornerAnnotation + text + '\n'
- # level 3 only Category A will be displayed
- elif self.annotationsDisplayAmount == 2:
- if (cornerText[key]['category'] == 'A'):
- cornerAnnotation = cornerAnnotation + text + '\n'
- sliceCornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation()
- # encode to avoid 'unicode conversion error' for patient names containing international characters
- cornerAnnotation = cornerAnnotation
- sliceCornerAnnotation.SetText(i, cornerAnnotation)
- textProperty = sliceCornerAnnotation.GetTextProperty()
- textProperty.SetShadow(1)
-
- self.sliceViews[sliceViewName].scheduleRender()
-
- def resetTexts(self):
- for i, cornerText in enumerate(self.cornerTexts):
- for key in cornerText.keys():
- self.cornerTexts[i][key]['text'] = ''
-
- def extractDICOMValues(self, uid):
-
- # Used cached tags, if found.
- # DICOM objects are not allowed to be changed,
- # so if the UID matches then the content has to match as well
- if uid in self.extractedDICOMValuesCache.keys():
- return self.extractedDICOMValuesCache[uid]
-
- p = {}
- tags = {
- "0008,0021": "Series Date",
- "0008,0031": "Series Time",
- "0008,0060": "Modality",
- "0008,0070": "Manufacturer",
- "0008,0080": "Institution Name",
- "0008,0090": "Referring Physician Name",
- "0008,103e": "Series Description",
- "0008,1090": "Model",
- "0010,0010": "Patient Name",
- "0010,0020": "Patient ID",
- "0010,0030": "Patient Birth Date",
- "0010,0040": "Patient Sex",
- "0010,1010": "Patient Age",
- "0018,5100": "Patient Position",
- "0018,0080": "Repetition Time",
- "0018,0081": "Echo Time"
- }
- for tag in tags.keys():
- value = slicer.dicomDatabase.instanceValue(uid, tag)
- p[tags[tag]] = value
-
- # Store DICOM tags in cache
- self.extractedDICOMValuesCache[uid] = p
- if len(self.extractedDICOMValuesCache) > self.extractedDICOMValuesCacheSize:
- # cache is full, drop oldest item
- self.extractedDICOMValuesCache.popitem(last=False)
-
- return p
+ studyH = studyH
+ clockTime = ' AM'
+ studyM = time[2:4]
+ studyS = time[4:6]
+ return studyH + ':' + studyM + ':' + studyS + clockTime
+
+ @staticmethod
+ def fitText(text, textSize):
+ if len(text) > textSize:
+ preSize = int(textSize / 2)
+ postSize = preSize - 3
+ text = text[:preSize] + "..." + text[-postSize:]
+ return text
+
+ def drawCornerAnnotations(self, sliceViewName):
+ if not self.sliceViewAnnotationsEnabled:
+ return
+ # Auto-Adjust
+ # adjust maximum text length based on fontsize and view width
+ viewWidth = self.sliceViews[sliceViewName].width
+ self.maximumTextLength = int((viewWidth - 40) / self.fontSize)
+
+ for i, cornerText in enumerate(self.cornerTexts):
+ keys = sorted(cornerText.keys())
+ cornerAnnotation = ''
+ for key in keys:
+ text = cornerText[key]['text']
+ if (text != ''):
+ text = self.fitText(text, self.maximumTextLength)
+ # level 1: All categories will be displayed
+ if self.annotationsDisplayAmount == 0:
+ cornerAnnotation = cornerAnnotation + text + '\n'
+ # level 2: Category A and B will be displayed
+ elif self.annotationsDisplayAmount == 1:
+ if (cornerText[key]['category'] != 'C'):
+ cornerAnnotation = cornerAnnotation + text + '\n'
+ # level 3 only Category A will be displayed
+ elif self.annotationsDisplayAmount == 2:
+ if (cornerText[key]['category'] == 'A'):
+ cornerAnnotation = cornerAnnotation + text + '\n'
+ sliceCornerAnnotation = self.sliceViews[sliceViewName].cornerAnnotation()
+ # encode to avoid 'unicode conversion error' for patient names containing international characters
+ cornerAnnotation = cornerAnnotation
+ sliceCornerAnnotation.SetText(i, cornerAnnotation)
+ textProperty = sliceCornerAnnotation.GetTextProperty()
+ textProperty.SetShadow(1)
+
+ self.sliceViews[sliceViewName].scheduleRender()
+
+ def resetTexts(self):
+ for i, cornerText in enumerate(self.cornerTexts):
+ for key in cornerText.keys():
+ self.cornerTexts[i][key]['text'] = ''
+
+ def extractDICOMValues(self, uid):
+
+ # Used cached tags, if found.
+ # DICOM objects are not allowed to be changed,
+ # so if the UID matches then the content has to match as well
+ if uid in self.extractedDICOMValuesCache.keys():
+ return self.extractedDICOMValuesCache[uid]
+
+ p = {}
+ tags = {
+ "0008,0021": "Series Date",
+ "0008,0031": "Series Time",
+ "0008,0060": "Modality",
+ "0008,0070": "Manufacturer",
+ "0008,0080": "Institution Name",
+ "0008,0090": "Referring Physician Name",
+ "0008,103e": "Series Description",
+ "0008,1090": "Model",
+ "0010,0010": "Patient Name",
+ "0010,0020": "Patient ID",
+ "0010,0030": "Patient Birth Date",
+ "0010,0040": "Patient Sex",
+ "0010,1010": "Patient Age",
+ "0018,5100": "Patient Position",
+ "0018,0080": "Repetition Time",
+ "0018,0081": "Echo Time"
+ }
+ for tag in tags.keys():
+ value = slicer.dicomDatabase.instanceValue(uid, tag)
+ p[tags[tag]] = value
+
+ # Store DICOM tags in cache
+ self.extractedDICOMValuesCache[uid] = p
+ if len(self.extractedDICOMValuesCache) > self.extractedDICOMValuesCacheSize:
+ # cache is full, drop oldest item
+ self.extractedDICOMValuesCache.popitem(last=False)
+
+ return p
diff --git a/Modules/Scripted/Endoscopy/Endoscopy.py b/Modules/Scripted/Endoscopy/Endoscopy.py
index f90c6ab4cc0..c527ab897a4 100644
--- a/Modules/Scripted/Endoscopy/Endoscopy.py
+++ b/Modules/Scripted/Endoscopy/Endoscopy.py
@@ -12,17 +12,17 @@
#
class Endoscopy(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Endoscopy"
- self.parent.categories = ["Endoscopy"]
- self.parent.dependencies = []
- self.parent.contributors = ["Steve Pieper (Isomics)"]
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Endoscopy"
+ self.parent.categories = ["Endoscopy"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Steve Pieper (Isomics)"]
+ self.parent.helpText = """
Create a path model as a spline interpolation of a set of fiducial points.
Pick the Camera to be modified by the path and the Fiducial List defining the control points.
Clicking "Create path" will make a path model and enable the flythrough panel.
@@ -31,8 +31,8 @@ def __init__(self, parent):
The Frame Delay slider slows down the animation by adding more time between frames.
The View Angle provides is used to approximate the optics of an endoscopy system.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This work is supported by PAR-07-249: R01CA131718 NA-MIC Virtual Colonoscopy
(See https://www.na-mic.org/Wiki/index.php/NA-MIC_NCBC_Collaboration:NA-MIC_virtual_colonoscopy)
NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community.
@@ -44,558 +44,558 @@ def __init__(self, parent):
#
class EndoscopyWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent=None):
- ScriptedLoadableModuleWidget.__init__(self, parent)
- self.cameraNode = None
- self.cameraNodeObserverTag = None
- self.cameraObserverTag = None
- # Flythough variables
- self.transform = None
- self.path = None
- self.camera = None
- self.skip = 0
- self.timer = qt.QTimer()
- self.timer.setInterval(20)
- self.timer.connect('timeout()', self.flyToNext)
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Path collapsible button
- pathCollapsibleButton = ctk.ctkCollapsibleButton()
- pathCollapsibleButton.text = "Path"
- self.layout.addWidget(pathCollapsibleButton)
-
- # Layout within the path collapsible button
- pathFormLayout = qt.QFormLayout(pathCollapsibleButton)
-
- # Camera node selector
- cameraNodeSelector = slicer.qMRMLNodeComboBox()
- cameraNodeSelector.objectName = 'cameraNodeSelector'
- cameraNodeSelector.toolTip = "Select a camera that will fly along this path."
- cameraNodeSelector.nodeTypes = ['vtkMRMLCameraNode']
- cameraNodeSelector.noneEnabled = False
- cameraNodeSelector.addEnabled = False
- cameraNodeSelector.removeEnabled = False
- cameraNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
- cameraNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setCameraNode)
- pathFormLayout.addRow("Camera:", cameraNodeSelector)
-
- # Input fiducials node selector
- inputFiducialsNodeSelector = slicer.qMRMLNodeComboBox()
- inputFiducialsNodeSelector.objectName = 'inputFiducialsNodeSelector'
- inputFiducialsNodeSelector.toolTip = "Select a fiducial list to define control points for the path."
- inputFiducialsNodeSelector.nodeTypes = ['vtkMRMLMarkupsFiducialNode', 'vtkMRMLMarkupsCurveNode',
- 'vtkMRMLAnnotationHierarchyNode']
- inputFiducialsNodeSelector.noneEnabled = False
- inputFiducialsNodeSelector.addEnabled = False
- inputFiducialsNodeSelector.removeEnabled = False
- inputFiducialsNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
- pathFormLayout.addRow("Input Fiducials:", inputFiducialsNodeSelector)
-
- # Output path node selector
- outputPathNodeSelector = slicer.qMRMLNodeComboBox()
- outputPathNodeSelector.objectName = 'outputPathNodeSelector'
- outputPathNodeSelector.toolTip = "Select a fiducial list to define control points for the path."
- outputPathNodeSelector.nodeTypes = ['vtkMRMLModelNode']
- outputPathNodeSelector.noneEnabled = False
- outputPathNodeSelector.addEnabled = True
- outputPathNodeSelector.removeEnabled = True
- outputPathNodeSelector.renameEnabled = True
- outputPathNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
- pathFormLayout.addRow("Output Path:", outputPathNodeSelector)
-
- # CreatePath button
- createPathButton = qt.QPushButton("Create path")
- createPathButton.toolTip = "Create the path."
- createPathButton.enabled = False
- pathFormLayout.addRow(createPathButton)
- createPathButton.connect('clicked()', self.onCreatePathButtonClicked)
-
- # Flythrough collapsible button
- flythroughCollapsibleButton = ctk.ctkCollapsibleButton()
- flythroughCollapsibleButton.text = "Flythrough"
- flythroughCollapsibleButton.enabled = False
- self.layout.addWidget(flythroughCollapsibleButton)
-
- # Layout within the Flythrough collapsible button
- flythroughFormLayout = qt.QFormLayout(flythroughCollapsibleButton)
-
- # Frame slider
- frameSlider = ctk.ctkSliderWidget()
- frameSlider.connect('valueChanged(double)', self.frameSliderValueChanged)
- frameSlider.decimals = 0
- flythroughFormLayout.addRow("Frame:", frameSlider)
-
- # Frame skip slider
- frameSkipSlider = ctk.ctkSliderWidget()
- frameSkipSlider.connect('valueChanged(double)', self.frameSkipSliderValueChanged)
- frameSkipSlider.decimals = 0
- frameSkipSlider.minimum = 0
- frameSkipSlider.maximum = 50
- flythroughFormLayout.addRow("Frame skip:", frameSkipSlider)
-
- # Frame delay slider
- frameDelaySlider = ctk.ctkSliderWidget()
- frameDelaySlider.connect('valueChanged(double)', self.frameDelaySliderValueChanged)
- frameDelaySlider.decimals = 0
- frameDelaySlider.minimum = 5
- frameDelaySlider.maximum = 100
- frameDelaySlider.suffix = " ms"
- frameDelaySlider.value = 20
- flythroughFormLayout.addRow("Frame delay:", frameDelaySlider)
-
- # View angle slider
- viewAngleSlider = ctk.ctkSliderWidget()
- viewAngleSlider.connect('valueChanged(double)', self.viewAngleSliderValueChanged)
- viewAngleSlider.decimals = 0
- viewAngleSlider.minimum = 30
- viewAngleSlider.maximum = 180
- flythroughFormLayout.addRow("View Angle:", viewAngleSlider)
-
- # Play button
- playButton = qt.QPushButton("Play")
- playButton.toolTip = "Fly through path."
- playButton.checkable = True
- flythroughFormLayout.addRow(playButton)
- playButton.connect('toggled(bool)', self.onPlayButtonToggled)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- # Set local var as instance attribute
- self.cameraNodeSelector = cameraNodeSelector
- self.inputFiducialsNodeSelector = inputFiducialsNodeSelector
- self.outputPathNodeSelector = outputPathNodeSelector
- self.createPathButton = createPathButton
- self.flythroughCollapsibleButton = flythroughCollapsibleButton
- self.frameSlider = frameSlider
- self.viewAngleSlider = viewAngleSlider
- self.playButton = playButton
-
- cameraNodeSelector.setMRMLScene(slicer.mrmlScene)
- inputFiducialsNodeSelector.setMRMLScene(slicer.mrmlScene)
- outputPathNodeSelector.setMRMLScene(slicer.mrmlScene)
-
- def setCameraNode(self, newCameraNode):
- """Allow to set the current camera node.
- Connected to signal 'currentNodeChanged()' emitted by camera node selector."""
-
- # Remove previous observer
- if self.cameraNode and self.cameraNodeObserverTag:
- self.cameraNode.RemoveObserver(self.cameraNodeObserverTag)
- if self.camera and self.cameraObserverTag:
- self.camera.RemoveObserver(self.cameraObserverTag)
-
- newCamera = None
- if newCameraNode:
- newCamera = newCameraNode.GetCamera()
- # Add CameraNode ModifiedEvent observer
- self.cameraNodeObserverTag = newCameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified)
- # Add Camera ModifiedEvent observer
- self.cameraObserverTag = newCamera.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified)
-
- self.cameraNode = newCameraNode
- self.camera = newCamera
-
- # Update UI
- self.updateWidgetFromMRML()
-
- def updateWidgetFromMRML(self):
- if self.camera:
- self.viewAngleSlider.value = self.camera.GetViewAngle()
- if self.cameraNode:
- pass
-
- def onCameraModified(self, observer, eventid):
- self.updateWidgetFromMRML()
-
- def onCameraNodeModified(self, observer, eventid):
- self.updateWidgetFromMRML()
-
- def enableOrDisableCreateButton(self):
- """Connected to both the fiducial and camera node selector. It allows to
- enable or disable the 'create path' button."""
- self.createPathButton.enabled = (self.cameraNodeSelector.currentNode() is not None
- and self.inputFiducialsNodeSelector.currentNode() is not None
- and self.outputPathNodeSelector.currentNode() is not None)
-
- def onCreatePathButtonClicked(self):
- """Connected to 'create path' button. It allows to:
- - compute the path
- - create the associated model"""
-
- fiducialsNode = self.inputFiducialsNodeSelector.currentNode()
- outputPathNode = self.outputPathNodeSelector.currentNode()
- print("Calculating Path...")
- result = EndoscopyComputePath(fiducialsNode)
- print("-> Computed path contains %d elements" % len(result.path))
-
- print("Create Model...")
- model = EndoscopyPathModel(result.path, fiducialsNode, outputPathNode)
- print("-> Model created")
-
- # Update frame slider range
- self.frameSlider.maximum = len(result.path) - 2
-
- # Update flythrough variables
- self.camera = self.camera
- self.transform = model.transform
- self.pathPlaneNormal = model.planeNormal
- self.path = result.path
-
- # Enable / Disable flythrough button
- self.flythroughCollapsibleButton.enabled = len(result.path) > 0
-
- def frameSliderValueChanged(self, newValue):
- # print "frameSliderValueChanged:", newValue
- self.flyTo(newValue)
-
- def frameSkipSliderValueChanged(self, newValue):
- # print "frameSkipSliderValueChanged:", newValue
- self.skip = int(newValue)
-
- def frameDelaySliderValueChanged(self, newValue):
- # print "frameDelaySliderValueChanged:", newValue
- self.timer.interval = newValue
-
- def viewAngleSliderValueChanged(self, newValue):
- if not self.cameraNode:
- return
- # print "viewAngleSliderValueChanged:", newValue
- self.cameraNode.GetCamera().SetViewAngle(newValue)
-
- def onPlayButtonToggled(self, checked):
- if checked:
- self.timer.start()
- self.playButton.text = "Stop"
- else:
- self.timer.stop()
- self.playButton.text = "Play"
-
- def flyToNext(self):
- currentStep = self.frameSlider.value
- nextStep = currentStep + self.skip + 1
- if nextStep > len(self.path) - 2:
- nextStep = 0
- self.frameSlider.value = nextStep
-
- def flyTo(self, pathPointIndex):
- """ Apply the pathPointIndex-th step in the path to the global camera"""
-
- if self.path is None:
- return
-
- pathPointIndex = int(pathPointIndex)
- cameraPosition = self.path[pathPointIndex]
- wasModified = self.cameraNode.StartModify()
-
- self.camera.SetPosition(cameraPosition)
- focalPointPosition = self.path[pathPointIndex + 1]
- self.camera.SetFocalPoint(*focalPointPosition)
- self.camera.OrthogonalizeViewUp()
-
- toParent = vtk.vtkMatrix4x4()
- self.transform.GetMatrixTransformToParent(toParent)
- toParent.SetElement(0, 3, cameraPosition[0])
- toParent.SetElement(1, 3, cameraPosition[1])
- toParent.SetElement(2, 3, cameraPosition[2])
-
- # Set up transform orientation component so that
- # Z axis is aligned with view direction and
- # Y vector is aligned with the curve's plane normal.
- # This can be used for example to show a reformatted slice
- # using with SlicerIGT extension's VolumeResliceDriver module.
- import numpy as np
- zVec = (focalPointPosition - cameraPosition) / np.linalg.norm(focalPointPosition - cameraPosition)
- yVec = self.pathPlaneNormal
- xVec = np.cross(yVec, zVec)
- xVec /= np.linalg.norm(xVec)
- yVec = np.cross(zVec, xVec)
- toParent.SetElement(0, 0, xVec[0])
- toParent.SetElement(1, 0, xVec[1])
- toParent.SetElement(2, 0, xVec[2])
- toParent.SetElement(0, 1, yVec[0])
- toParent.SetElement(1, 1, yVec[1])
- toParent.SetElement(2, 1, yVec[2])
- toParent.SetElement(0, 2, zVec[0])
- toParent.SetElement(1, 2, zVec[1])
- toParent.SetElement(2, 2, zVec[2])
-
- self.transform.SetMatrixTransformToParent(toParent)
-
- self.cameraNode.EndModify(wasModified)
- self.cameraNode.ResetClippingRange()
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+ def __init__(self, parent=None):
+ ScriptedLoadableModuleWidget.__init__(self, parent)
+ self.cameraNode = None
+ self.cameraNodeObserverTag = None
+ self.cameraObserverTag = None
+ # Flythough variables
+ self.transform = None
+ self.path = None
+ self.camera = None
+ self.skip = 0
+ self.timer = qt.QTimer()
+ self.timer.setInterval(20)
+ self.timer.connect('timeout()', self.flyToNext)
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Path collapsible button
+ pathCollapsibleButton = ctk.ctkCollapsibleButton()
+ pathCollapsibleButton.text = "Path"
+ self.layout.addWidget(pathCollapsibleButton)
+
+ # Layout within the path collapsible button
+ pathFormLayout = qt.QFormLayout(pathCollapsibleButton)
+
+ # Camera node selector
+ cameraNodeSelector = slicer.qMRMLNodeComboBox()
+ cameraNodeSelector.objectName = 'cameraNodeSelector'
+ cameraNodeSelector.toolTip = "Select a camera that will fly along this path."
+ cameraNodeSelector.nodeTypes = ['vtkMRMLCameraNode']
+ cameraNodeSelector.noneEnabled = False
+ cameraNodeSelector.addEnabled = False
+ cameraNodeSelector.removeEnabled = False
+ cameraNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
+ cameraNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setCameraNode)
+ pathFormLayout.addRow("Camera:", cameraNodeSelector)
+
+ # Input fiducials node selector
+ inputFiducialsNodeSelector = slicer.qMRMLNodeComboBox()
+ inputFiducialsNodeSelector.objectName = 'inputFiducialsNodeSelector'
+ inputFiducialsNodeSelector.toolTip = "Select a fiducial list to define control points for the path."
+ inputFiducialsNodeSelector.nodeTypes = ['vtkMRMLMarkupsFiducialNode', 'vtkMRMLMarkupsCurveNode',
+ 'vtkMRMLAnnotationHierarchyNode']
+ inputFiducialsNodeSelector.noneEnabled = False
+ inputFiducialsNodeSelector.addEnabled = False
+ inputFiducialsNodeSelector.removeEnabled = False
+ inputFiducialsNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
+ pathFormLayout.addRow("Input Fiducials:", inputFiducialsNodeSelector)
+
+ # Output path node selector
+ outputPathNodeSelector = slicer.qMRMLNodeComboBox()
+ outputPathNodeSelector.objectName = 'outputPathNodeSelector'
+ outputPathNodeSelector.toolTip = "Select a fiducial list to define control points for the path."
+ outputPathNodeSelector.nodeTypes = ['vtkMRMLModelNode']
+ outputPathNodeSelector.noneEnabled = False
+ outputPathNodeSelector.addEnabled = True
+ outputPathNodeSelector.removeEnabled = True
+ outputPathNodeSelector.renameEnabled = True
+ outputPathNodeSelector.connect('currentNodeChanged(bool)', self.enableOrDisableCreateButton)
+ pathFormLayout.addRow("Output Path:", outputPathNodeSelector)
+
+ # CreatePath button
+ createPathButton = qt.QPushButton("Create path")
+ createPathButton.toolTip = "Create the path."
+ createPathButton.enabled = False
+ pathFormLayout.addRow(createPathButton)
+ createPathButton.connect('clicked()', self.onCreatePathButtonClicked)
+
+ # Flythrough collapsible button
+ flythroughCollapsibleButton = ctk.ctkCollapsibleButton()
+ flythroughCollapsibleButton.text = "Flythrough"
+ flythroughCollapsibleButton.enabled = False
+ self.layout.addWidget(flythroughCollapsibleButton)
+
+ # Layout within the Flythrough collapsible button
+ flythroughFormLayout = qt.QFormLayout(flythroughCollapsibleButton)
+
+ # Frame slider
+ frameSlider = ctk.ctkSliderWidget()
+ frameSlider.connect('valueChanged(double)', self.frameSliderValueChanged)
+ frameSlider.decimals = 0
+ flythroughFormLayout.addRow("Frame:", frameSlider)
+
+ # Frame skip slider
+ frameSkipSlider = ctk.ctkSliderWidget()
+ frameSkipSlider.connect('valueChanged(double)', self.frameSkipSliderValueChanged)
+ frameSkipSlider.decimals = 0
+ frameSkipSlider.minimum = 0
+ frameSkipSlider.maximum = 50
+ flythroughFormLayout.addRow("Frame skip:", frameSkipSlider)
+
+ # Frame delay slider
+ frameDelaySlider = ctk.ctkSliderWidget()
+ frameDelaySlider.connect('valueChanged(double)', self.frameDelaySliderValueChanged)
+ frameDelaySlider.decimals = 0
+ frameDelaySlider.minimum = 5
+ frameDelaySlider.maximum = 100
+ frameDelaySlider.suffix = " ms"
+ frameDelaySlider.value = 20
+ flythroughFormLayout.addRow("Frame delay:", frameDelaySlider)
+
+ # View angle slider
+ viewAngleSlider = ctk.ctkSliderWidget()
+ viewAngleSlider.connect('valueChanged(double)', self.viewAngleSliderValueChanged)
+ viewAngleSlider.decimals = 0
+ viewAngleSlider.minimum = 30
+ viewAngleSlider.maximum = 180
+ flythroughFormLayout.addRow("View Angle:", viewAngleSlider)
+
+ # Play button
+ playButton = qt.QPushButton("Play")
+ playButton.toolTip = "Fly through path."
+ playButton.checkable = True
+ flythroughFormLayout.addRow(playButton)
+ playButton.connect('toggled(bool)', self.onPlayButtonToggled)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ # Set local var as instance attribute
+ self.cameraNodeSelector = cameraNodeSelector
+ self.inputFiducialsNodeSelector = inputFiducialsNodeSelector
+ self.outputPathNodeSelector = outputPathNodeSelector
+ self.createPathButton = createPathButton
+ self.flythroughCollapsibleButton = flythroughCollapsibleButton
+ self.frameSlider = frameSlider
+ self.viewAngleSlider = viewAngleSlider
+ self.playButton = playButton
+
+ cameraNodeSelector.setMRMLScene(slicer.mrmlScene)
+ inputFiducialsNodeSelector.setMRMLScene(slicer.mrmlScene)
+ outputPathNodeSelector.setMRMLScene(slicer.mrmlScene)
+
+ def setCameraNode(self, newCameraNode):
+ """Allow to set the current camera node.
+ Connected to signal 'currentNodeChanged()' emitted by camera node selector."""
+
+ # Remove previous observer
+ if self.cameraNode and self.cameraNodeObserverTag:
+ self.cameraNode.RemoveObserver(self.cameraNodeObserverTag)
+ if self.camera and self.cameraObserverTag:
+ self.camera.RemoveObserver(self.cameraObserverTag)
+
+ newCamera = None
+ if newCameraNode:
+ newCamera = newCameraNode.GetCamera()
+ # Add CameraNode ModifiedEvent observer
+ self.cameraNodeObserverTag = newCameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified)
+ # Add Camera ModifiedEvent observer
+ self.cameraObserverTag = newCamera.AddObserver(vtk.vtkCommand.ModifiedEvent, self.onCameraNodeModified)
+
+ self.cameraNode = newCameraNode
+ self.camera = newCamera
+
+ # Update UI
+ self.updateWidgetFromMRML()
+
+ def updateWidgetFromMRML(self):
+ if self.camera:
+ self.viewAngleSlider.value = self.camera.GetViewAngle()
+ if self.cameraNode:
+ pass
+
+ def onCameraModified(self, observer, eventid):
+ self.updateWidgetFromMRML()
+
+ def onCameraNodeModified(self, observer, eventid):
+ self.updateWidgetFromMRML()
+
+ def enableOrDisableCreateButton(self):
+ """Connected to both the fiducial and camera node selector. It allows to
+ enable or disable the 'create path' button."""
+ self.createPathButton.enabled = (self.cameraNodeSelector.currentNode() is not None
+ and self.inputFiducialsNodeSelector.currentNode() is not None
+ and self.outputPathNodeSelector.currentNode() is not None)
+
+ def onCreatePathButtonClicked(self):
+ """Connected to 'create path' button. It allows to:
+ - compute the path
+ - create the associated model"""
+
+ fiducialsNode = self.inputFiducialsNodeSelector.currentNode()
+ outputPathNode = self.outputPathNodeSelector.currentNode()
+ print("Calculating Path...")
+ result = EndoscopyComputePath(fiducialsNode)
+ print("-> Computed path contains %d elements" % len(result.path))
+
+ print("Create Model...")
+ model = EndoscopyPathModel(result.path, fiducialsNode, outputPathNode)
+ print("-> Model created")
+
+ # Update frame slider range
+ self.frameSlider.maximum = len(result.path) - 2
+
+ # Update flythrough variables
+ self.camera = self.camera
+ self.transform = model.transform
+ self.pathPlaneNormal = model.planeNormal
+ self.path = result.path
+
+ # Enable / Disable flythrough button
+ self.flythroughCollapsibleButton.enabled = len(result.path) > 0
+
+ def frameSliderValueChanged(self, newValue):
+ # print "frameSliderValueChanged:", newValue
+ self.flyTo(newValue)
+
+ def frameSkipSliderValueChanged(self, newValue):
+ # print "frameSkipSliderValueChanged:", newValue
+ self.skip = int(newValue)
+
+ def frameDelaySliderValueChanged(self, newValue):
+ # print "frameDelaySliderValueChanged:", newValue
+ self.timer.interval = newValue
+
+ def viewAngleSliderValueChanged(self, newValue):
+ if not self.cameraNode:
+ return
+ # print "viewAngleSliderValueChanged:", newValue
+ self.cameraNode.GetCamera().SetViewAngle(newValue)
+
+ def onPlayButtonToggled(self, checked):
+ if checked:
+ self.timer.start()
+ self.playButton.text = "Stop"
+ else:
+ self.timer.stop()
+ self.playButton.text = "Play"
+
+ def flyToNext(self):
+ currentStep = self.frameSlider.value
+ nextStep = currentStep + self.skip + 1
+ if nextStep > len(self.path) - 2:
+ nextStep = 0
+ self.frameSlider.value = nextStep
+
+ def flyTo(self, pathPointIndex):
+ """ Apply the pathPointIndex-th step in the path to the global camera"""
+
+ if self.path is None:
+ return
+
+ pathPointIndex = int(pathPointIndex)
+ cameraPosition = self.path[pathPointIndex]
+ wasModified = self.cameraNode.StartModify()
+
+ self.camera.SetPosition(cameraPosition)
+ focalPointPosition = self.path[pathPointIndex + 1]
+ self.camera.SetFocalPoint(*focalPointPosition)
+ self.camera.OrthogonalizeViewUp()
+
+ toParent = vtk.vtkMatrix4x4()
+ self.transform.GetMatrixTransformToParent(toParent)
+ toParent.SetElement(0, 3, cameraPosition[0])
+ toParent.SetElement(1, 3, cameraPosition[1])
+ toParent.SetElement(2, 3, cameraPosition[2])
+
+ # Set up transform orientation component so that
+ # Z axis is aligned with view direction and
+ # Y vector is aligned with the curve's plane normal.
+ # This can be used for example to show a reformatted slice
+ # using with SlicerIGT extension's VolumeResliceDriver module.
+ import numpy as np
+ zVec = (focalPointPosition - cameraPosition) / np.linalg.norm(focalPointPosition - cameraPosition)
+ yVec = self.pathPlaneNormal
+ xVec = np.cross(yVec, zVec)
+ xVec /= np.linalg.norm(xVec)
+ yVec = np.cross(zVec, xVec)
+ toParent.SetElement(0, 0, xVec[0])
+ toParent.SetElement(1, 0, xVec[1])
+ toParent.SetElement(2, 0, xVec[2])
+ toParent.SetElement(0, 1, yVec[0])
+ toParent.SetElement(1, 1, yVec[1])
+ toParent.SetElement(2, 1, yVec[2])
+ toParent.SetElement(0, 2, zVec[0])
+ toParent.SetElement(1, 2, zVec[1])
+ toParent.SetElement(2, 2, zVec[2])
+
+ self.transform.SetMatrixTransformToParent(toParent)
+
+ self.cameraNode.EndModify(wasModified)
+ self.cameraNode.ResetClippingRange()
-class EndoscopyComputePath:
- """Compute path given a list of fiducials.
- Path is stored in 'path' member variable as a numpy array.
- If a point list is received then curve points are generated using Hermite spline interpolation.
- See https://en.wikipedia.org/wiki/Cubic_Hermite_spline
-
- Example:
- result = EndoscopyComputePath(fiducialListNode)
- print "computer path has %d elements" % len(result.path)
-
- """
-
- def __init__(self, fiducialListNode, dl=0.5):
- import numpy
- self.dl = dl # desired world space step size (in mm)
- self.dt = dl # current guess of parametric stepsize
- self.fids = fiducialListNode
-
- # Already a curve, just get the points, sampled at equal distances.
- if (self.fids.GetClassName() == "vtkMRMLMarkupsCurveNode"
- or self.fids.GetClassName() == "vtkMRMLMarkupsClosedCurveNode"):
- # Temporarily increase the number of points per segment, to get a very smooth curve
- pointsPerSegment = int(self.fids.GetCurveLengthWorld() / self.dl / self.fids.GetNumberOfControlPoints()) + 1
- originalPointsPerSegment = self.fids.GetNumberOfPointsPerInterpolatingSegment()
- if originalPointsPerSegment < pointsPerSegment:
- self.fids.SetNumberOfPointsPerInterpolatingSegment(pointsPerSegment)
- # Get equidistant points
- resampledPoints = vtk.vtkPoints()
- slicer.vtkMRMLMarkupsCurveNode.ResamplePoints(self.fids.GetCurvePointsWorld(), resampledPoints, self.dl, self.fids.GetCurveClosed())
- # Restore original number of pointsPerSegment
- if originalPointsPerSegment < pointsPerSegment:
- self.fids.SetNumberOfPointsPerInterpolatingSegment(originalPointsPerSegment)
- # Get it as a numpy array as an independent copy
- self.path = vtk.util.numpy_support.vtk_to_numpy(resampledPoints.GetData())
- return
-
- # hermite interpolation functions
- self.h00 = lambda t: 2 * t**3 - 3 * t**2 + 1
- self.h10 = lambda t: t**3 - 2 * t**2 + t
- self.h01 = lambda t: -2 * t**3 + 3 * t**2
- self.h11 = lambda t: t**3 - t**2
-
- # n is the number of control points in the piecewise curve
-
- if self.fids.GetClassName() == "vtkMRMLAnnotationHierarchyNode":
- # slicer4 style hierarchy nodes
- collection = vtk.vtkCollection()
- self.fids.GetChildrenDisplayableNodes(collection)
- self.n = collection.GetNumberOfItems()
- if self.n == 0:
- return
- self.p = numpy.zeros((self.n, 3))
- for i in range(self.n):
- f = collection.GetItemAsObject(i)
- coords = [0, 0, 0]
- f.GetFiducialCoordinates(coords)
- self.p[i] = coords
- elif self.fids.GetClassName() == "vtkMRMLMarkupsFiducialNode":
- # slicer4 Markups node
- self.n = self.fids.GetNumberOfControlPoints()
- n = self.n
- if n == 0:
- return
- # get fiducial positions
- # sets self.p
- self.p = numpy.zeros((n, 3))
- for i in range(n):
- coord = [0.0, 0.0, 0.0]
- self.fids.GetNthControlPointPositionWorld(i, coord)
- self.p[i] = coord
- else:
- # slicer3 style fiducial lists
- self.n = self.fids.GetNumberOfFiducials()
- n = self.n
- if n == 0:
- return
- # get control point data
- # sets self.p
- self.p = numpy.zeros((n, 3))
- for i in range(n):
- self.p[i] = self.fids.GetNthFiducialXYZ(i)
-
- # calculate the tangent vectors
- # - fm is forward difference
- # - m is average of in and out vectors
- # - first tangent is out vector, last is in vector
- # - sets self.m
- n = self.n
- fm = numpy.zeros((n, 3))
- for i in range(0, n - 1):
- fm[i] = self.p[i + 1] - self.p[i]
- self.m = numpy.zeros((n, 3))
- for i in range(1, n - 1):
- self.m[i] = (fm[i - 1] + fm[i]) / 2.
- self.m[0] = fm[0]
- self.m[n - 1] = fm[n - 2]
-
- self.path = [self.p[0]]
- self.calculatePath()
-
- def calculatePath(self):
- """ Generate a flight path for of steps of length dl """
- #
- # calculate the actual path
- # - take steps of self.dl in world space
- # -- if dl steps into next segment, take a step of size "remainder" in the new segment
- # - put resulting points into self.path
- #
- n = self.n
- segment = 0 # which first point of current segment
- t = 0 # parametric current parametric increment
- remainder = 0 # how much of dl isn't included in current step
- while segment < n - 1:
- t, p, remainder = self.step(segment, t, self.dl)
- if remainder != 0 or t == 1.:
- segment += 1
- t = 0
- if segment < n - 1:
- t, p, remainder = self.step(segment, t, remainder)
- self.path.append(p)
-
- def point(self, segment, t):
- return (self.h00(t) * self.p[segment] +
- self.h10(t) * self.m[segment] +
- self.h01(t) * self.p[segment + 1] +
- self.h11(t) * self.m[segment + 1])
-
- def step(self, segment, t, dl):
- """ Take a step of dl and return the path point and new t
- return:
- t = new parametric coordinate after step
- p = point after step
- remainder = if step results in parametric coordinate > 1.0, then
- this is the amount of world space not covered by step
- """
- import numpy.linalg
- p0 = self.path[self.path.__len__() - 1] # last element in path
- remainder = 0
- ratio = 100
- count = 0
- while abs(1. - ratio) > 0.05:
- t1 = t + self.dt
- pguess = self.point(segment, t1)
- dist = numpy.linalg.norm(pguess - p0)
- ratio = self.dl / dist
- self.dt *= ratio
- if self.dt < 0.00000001:
- return
- count += 1
- if count > 500:
- return (t1, pguess, 0)
- if t1 > 1.:
- t1 = 1.
- p1 = self.point(segment, t1)
- remainder = numpy.linalg.norm(p1 - pguess)
- pguess = p1
- return (t1, pguess, remainder)
+class EndoscopyComputePath:
+ """Compute path given a list of fiducials.
+ Path is stored in 'path' member variable as a numpy array.
+ If a point list is received then curve points are generated using Hermite spline interpolation.
+ See https://en.wikipedia.org/wiki/Cubic_Hermite_spline
-class EndoscopyPathModel:
- """Create a vtkPolyData for a polyline:
- - Add one point per path point.
- - Add a single polyline
- """
+ Example:
+ result = EndoscopyComputePath(fiducialListNode)
+ print "computer path has %d elements" % len(result.path)
- def __init__(self, path, fiducialListNode, outputPathNode=None, cursorType=None):
- """
- :param path: path points as numpy array.
- :param fiducialListNode: input node, just used for naming the output node.
- :param outputPathNode: output model node that stores the path points.
- :param cursorType: can be 'markups' or 'model'. Markups has a number of advantages (radius it is easier to change the size,
- can jump to views by clicking on it, has more visualization options, can be scaled to fixed display size),
- but if some applications relied on having a model node as cursor then this argument can be used to achieve that.
"""
- fids = fiducialListNode
- scene = slicer.mrmlScene
-
- self.cursorType = "markups" if cursorType is None else cursorType
-
- points = vtk.vtkPoints()
- polyData = vtk.vtkPolyData()
- polyData.SetPoints(points)
-
- lines = vtk.vtkCellArray()
- polyData.SetLines(lines)
- linesIDArray = lines.GetData()
- linesIDArray.Reset()
- linesIDArray.InsertNextTuple1(0)
-
- polygons = vtk.vtkCellArray()
- polyData.SetPolys(polygons)
- idArray = polygons.GetData()
- idArray.Reset()
- idArray.InsertNextTuple1(0)
-
- for point in path:
- pointIndex = points.InsertNextPoint(*point)
- linesIDArray.InsertNextTuple1(pointIndex)
- linesIDArray.SetTuple1(0, linesIDArray.GetNumberOfTuples() - 1)
- lines.SetNumberOfCells(1)
-
- pointsArray = vtk.util.numpy_support.vtk_to_numpy(points.GetData())
- self.planePosition, self.planeNormal = self.planeFit(pointsArray.T)
-
- # Create model node
- model = outputPathNode
- if not model:
- model = scene.AddNewNodeByClass("vtkMRMLModelNode", scene.GenerateUniqueName("Path-%s" % fids.GetName()))
- model.CreateDefaultDisplayNodes()
- model.GetDisplayNode().SetColor(1, 1, 0) # yellow
-
- model.SetAndObservePolyData(polyData)
-
- # Camera cursor
- cursor = model.GetNodeReference("CameraCursor")
- if not cursor:
-
- if self.cursorType == "markups":
- # Markups cursor
- cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName()))
- cursor.CreateDefaultDisplayNodes()
- cursor.GetDisplayNode().SetSelectedColor(1, 0, 0) # red
- cursor.GetDisplayNode().SetSliceProjection(True)
- cursor.AddControlPoint(vtk.vtkVector3d(0, 0, 0), " ") # do not show any visible label
- cursor.SetNthControlPointLocked(0, True)
- else:
- # Model cursor
- cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsModelNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName()))
- cursor.CreateDefaultDisplayNodes()
- cursor.GetDisplayNode().SetColor(1, 0, 0) # red
- cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside
- # Add a sphere as cursor
- sphere = vtk.vtkSphereSource()
- sphere.Update()
- cursor.SetPolyDataConnection(sphere.GetOutputPort())
-
- model.SetNodeReferenceID("CameraCursor", cursor.GetID())
-
- # Transform node
- transform = model.GetNodeReference("CameraTransform")
- if not transform:
- transform = scene.AddNewNodeByClass("vtkMRMLLinearTransformNode", scene.GenerateUniqueName("Transform-%s" % fids.GetName()))
- model.SetNodeReferenceID("CameraTransform", transform.GetID())
- cursor.SetAndObserveTransformNodeID(transform.GetID())
-
- self.transform = transform
-
- # source: https://stackoverflow.com/questions/12299540/plane-fitting-to-4-or-more-xyz-points
- def planeFit(self, points):
- """
- p, n = planeFit(points)
+ def __init__(self, fiducialListNode, dl=0.5):
+ import numpy
+ self.dl = dl # desired world space step size (in mm)
+ self.dt = dl # current guess of parametric stepsize
+ self.fids = fiducialListNode
+
+ # Already a curve, just get the points, sampled at equal distances.
+ if (self.fids.GetClassName() == "vtkMRMLMarkupsCurveNode"
+ or self.fids.GetClassName() == "vtkMRMLMarkupsClosedCurveNode"):
+ # Temporarily increase the number of points per segment, to get a very smooth curve
+ pointsPerSegment = int(self.fids.GetCurveLengthWorld() / self.dl / self.fids.GetNumberOfControlPoints()) + 1
+ originalPointsPerSegment = self.fids.GetNumberOfPointsPerInterpolatingSegment()
+ if originalPointsPerSegment < pointsPerSegment:
+ self.fids.SetNumberOfPointsPerInterpolatingSegment(pointsPerSegment)
+ # Get equidistant points
+ resampledPoints = vtk.vtkPoints()
+ slicer.vtkMRMLMarkupsCurveNode.ResamplePoints(self.fids.GetCurvePointsWorld(), resampledPoints, self.dl, self.fids.GetCurveClosed())
+ # Restore original number of pointsPerSegment
+ if originalPointsPerSegment < pointsPerSegment:
+ self.fids.SetNumberOfPointsPerInterpolatingSegment(originalPointsPerSegment)
+ # Get it as a numpy array as an independent copy
+ self.path = vtk.util.numpy_support.vtk_to_numpy(resampledPoints.GetData())
+ return
+
+ # hermite interpolation functions
+ self.h00 = lambda t: 2 * t**3 - 3 * t**2 + 1
+ self.h10 = lambda t: t**3 - 2 * t**2 + t
+ self.h01 = lambda t: -2 * t**3 + 3 * t**2
+ self.h11 = lambda t: t**3 - t**2
+
+ # n is the number of control points in the piecewise curve
+
+ if self.fids.GetClassName() == "vtkMRMLAnnotationHierarchyNode":
+ # slicer4 style hierarchy nodes
+ collection = vtk.vtkCollection()
+ self.fids.GetChildrenDisplayableNodes(collection)
+ self.n = collection.GetNumberOfItems()
+ if self.n == 0:
+ return
+ self.p = numpy.zeros((self.n, 3))
+ for i in range(self.n):
+ f = collection.GetItemAsObject(i)
+ coords = [0, 0, 0]
+ f.GetFiducialCoordinates(coords)
+ self.p[i] = coords
+ elif self.fids.GetClassName() == "vtkMRMLMarkupsFiducialNode":
+ # slicer4 Markups node
+ self.n = self.fids.GetNumberOfControlPoints()
+ n = self.n
+ if n == 0:
+ return
+ # get fiducial positions
+ # sets self.p
+ self.p = numpy.zeros((n, 3))
+ for i in range(n):
+ coord = [0.0, 0.0, 0.0]
+ self.fids.GetNthControlPointPositionWorld(i, coord)
+ self.p[i] = coord
+ else:
+ # slicer3 style fiducial lists
+ self.n = self.fids.GetNumberOfFiducials()
+ n = self.n
+ if n == 0:
+ return
+ # get control point data
+ # sets self.p
+ self.p = numpy.zeros((n, 3))
+ for i in range(n):
+ self.p[i] = self.fids.GetNthFiducialXYZ(i)
+
+ # calculate the tangent vectors
+ # - fm is forward difference
+ # - m is average of in and out vectors
+ # - first tangent is out vector, last is in vector
+ # - sets self.m
+ n = self.n
+ fm = numpy.zeros((n, 3))
+ for i in range(0, n - 1):
+ fm[i] = self.p[i + 1] - self.p[i]
+ self.m = numpy.zeros((n, 3))
+ for i in range(1, n - 1):
+ self.m[i] = (fm[i - 1] + fm[i]) / 2.
+ self.m[0] = fm[0]
+ self.m[n - 1] = fm[n - 2]
+
+ self.path = [self.p[0]]
+ self.calculatePath()
+
+ def calculatePath(self):
+ """ Generate a flight path for of steps of length dl """
+ #
+ # calculate the actual path
+ # - take steps of self.dl in world space
+ # -- if dl steps into next segment, take a step of size "remainder" in the new segment
+ # - put resulting points into self.path
+ #
+ n = self.n
+ segment = 0 # which first point of current segment
+ t = 0 # parametric current parametric increment
+ remainder = 0 # how much of dl isn't included in current step
+ while segment < n - 1:
+ t, p, remainder = self.step(segment, t, self.dl)
+ if remainder != 0 or t == 1.:
+ segment += 1
+ t = 0
+ if segment < n - 1:
+ t, p, remainder = self.step(segment, t, remainder)
+ self.path.append(p)
+
+ def point(self, segment, t):
+ return (self.h00(t) * self.p[segment] +
+ self.h10(t) * self.m[segment] +
+ self.h01(t) * self.p[segment + 1] +
+ self.h11(t) * self.m[segment + 1])
+
+ def step(self, segment, t, dl):
+ """ Take a step of dl and return the path point and new t
+ return:
+ t = new parametric coordinate after step
+ p = point after step
+ remainder = if step results in parametric coordinate > 1.0, then
+ this is the amount of world space not covered by step
+ """
+ import numpy.linalg
+ p0 = self.path[self.path.__len__() - 1] # last element in path
+ remainder = 0
+ ratio = 100
+ count = 0
+ while abs(1. - ratio) > 0.05:
+ t1 = t + self.dt
+ pguess = self.point(segment, t1)
+ dist = numpy.linalg.norm(pguess - p0)
+ ratio = self.dl / dist
+ self.dt *= ratio
+ if self.dt < 0.00000001:
+ return
+ count += 1
+ if count > 500:
+ return (t1, pguess, 0)
+ if t1 > 1.:
+ t1 = 1.
+ p1 = self.point(segment, t1)
+ remainder = numpy.linalg.norm(p1 - pguess)
+ pguess = p1
+ return (t1, pguess, remainder)
- Given an array, points, of shape (d,...)
- representing points in d-dimensional space,
- fit an d-dimensional plane to the points.
- Return a point, p, on the plane (the point-cloud centroid),
- and the normal, n.
+
+class EndoscopyPathModel:
+ """Create a vtkPolyData for a polyline:
+ - Add one point per path point.
+ - Add a single polyline
"""
- import numpy as np
- from numpy.linalg import svd
- points = np.reshape(points, (np.shape(points)[0], -1)) # Collapse trialing dimensions
- assert points.shape[0] <= points.shape[1], f"There are only {points.shape[1]} points in {points.shape[0]} dimensions."
- ctr = points.mean(axis=1)
- x = points - ctr[:, np.newaxis]
- M = np.dot(x, x.T) # Could also use np.cov(x) here.
- return ctr, svd(M)[0][:, -1]
+
+ def __init__(self, path, fiducialListNode, outputPathNode=None, cursorType=None):
+ """
+ :param path: path points as numpy array.
+ :param fiducialListNode: input node, just used for naming the output node.
+ :param outputPathNode: output model node that stores the path points.
+ :param cursorType: can be 'markups' or 'model'. Markups has a number of advantages (radius it is easier to change the size,
+ can jump to views by clicking on it, has more visualization options, can be scaled to fixed display size),
+ but if some applications relied on having a model node as cursor then this argument can be used to achieve that.
+ """
+
+ fids = fiducialListNode
+ scene = slicer.mrmlScene
+
+ self.cursorType = "markups" if cursorType is None else cursorType
+
+ points = vtk.vtkPoints()
+ polyData = vtk.vtkPolyData()
+ polyData.SetPoints(points)
+
+ lines = vtk.vtkCellArray()
+ polyData.SetLines(lines)
+ linesIDArray = lines.GetData()
+ linesIDArray.Reset()
+ linesIDArray.InsertNextTuple1(0)
+
+ polygons = vtk.vtkCellArray()
+ polyData.SetPolys(polygons)
+ idArray = polygons.GetData()
+ idArray.Reset()
+ idArray.InsertNextTuple1(0)
+
+ for point in path:
+ pointIndex = points.InsertNextPoint(*point)
+ linesIDArray.InsertNextTuple1(pointIndex)
+ linesIDArray.SetTuple1(0, linesIDArray.GetNumberOfTuples() - 1)
+ lines.SetNumberOfCells(1)
+
+ pointsArray = vtk.util.numpy_support.vtk_to_numpy(points.GetData())
+ self.planePosition, self.planeNormal = self.planeFit(pointsArray.T)
+
+ # Create model node
+ model = outputPathNode
+ if not model:
+ model = scene.AddNewNodeByClass("vtkMRMLModelNode", scene.GenerateUniqueName("Path-%s" % fids.GetName()))
+ model.CreateDefaultDisplayNodes()
+ model.GetDisplayNode().SetColor(1, 1, 0) # yellow
+
+ model.SetAndObservePolyData(polyData)
+
+ # Camera cursor
+ cursor = model.GetNodeReference("CameraCursor")
+ if not cursor:
+
+ if self.cursorType == "markups":
+ # Markups cursor
+ cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName()))
+ cursor.CreateDefaultDisplayNodes()
+ cursor.GetDisplayNode().SetSelectedColor(1, 0, 0) # red
+ cursor.GetDisplayNode().SetSliceProjection(True)
+ cursor.AddControlPoint(vtk.vtkVector3d(0, 0, 0), " ") # do not show any visible label
+ cursor.SetNthControlPointLocked(0, True)
+ else:
+ # Model cursor
+ cursor = scene.AddNewNodeByClass("vtkMRMLMarkupsModelNode", scene.GenerateUniqueName("Cursor-%s" % fids.GetName()))
+ cursor.CreateDefaultDisplayNodes()
+ cursor.GetDisplayNode().SetColor(1, 0, 0) # red
+ cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside
+ # Add a sphere as cursor
+ sphere = vtk.vtkSphereSource()
+ sphere.Update()
+ cursor.SetPolyDataConnection(sphere.GetOutputPort())
+
+ model.SetNodeReferenceID("CameraCursor", cursor.GetID())
+
+ # Transform node
+ transform = model.GetNodeReference("CameraTransform")
+ if not transform:
+ transform = scene.AddNewNodeByClass("vtkMRMLLinearTransformNode", scene.GenerateUniqueName("Transform-%s" % fids.GetName()))
+ model.SetNodeReferenceID("CameraTransform", transform.GetID())
+ cursor.SetAndObserveTransformNodeID(transform.GetID())
+
+ self.transform = transform
+
+ # source: https://stackoverflow.com/questions/12299540/plane-fitting-to-4-or-more-xyz-points
+ def planeFit(self, points):
+ """
+ p, n = planeFit(points)
+
+ Given an array, points, of shape (d,...)
+ representing points in d-dimensional space,
+ fit an d-dimensional plane to the points.
+ Return a point, p, on the plane (the point-cloud centroid),
+ and the normal, n.
+ """
+ import numpy as np
+ from numpy.linalg import svd
+ points = np.reshape(points, (np.shape(points)[0], -1)) # Collapse trialing dimensions
+ assert points.shape[0] <= points.shape[1], f"There are only {points.shape[1]} points in {points.shape[0]} dimensions."
+ ctr = points.mean(axis=1)
+ x = points - ctr[:, np.newaxis]
+ M = np.dot(x, x.T) # Could also use np.cov(x) here.
+ return ctr, svd(M)[0][:, -1]
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizard.py b/Modules/Scripted/ExtensionWizard/ExtensionWizard.py
index cc2a55c097d..e2a3783f438 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizard.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizard.py
@@ -16,21 +16,21 @@
# -----------------------------------------------------------------------------
def _settingsList(settings, key, convertToAbsolutePaths=False):
- # Return a settings value as a list (even if empty or a single value)
-
- value = settings.value(key)
- if value is None:
- return []
- if isinstance(value, str):
- value = [value]
-
- if convertToAbsolutePaths:
- absolutePaths = []
- for path in value:
- absolutePaths.append(slicer.app.toSlicerHomeAbsolutePath(path))
- return absolutePaths
- else:
- return value
+ # Return a settings value as a list (even if empty or a single value)
+
+ value = settings.value(key)
+ if value is None:
+ return []
+ if isinstance(value, str):
+ value = [value]
+
+ if convertToAbsolutePaths:
+ absolutePaths = []
+ for path in value:
+ absolutePaths.append(slicer.app.toSlicerHomeAbsolutePath(path))
+ return absolutePaths
+ else:
+ return value
# =============================================================================
@@ -39,23 +39,23 @@ def _settingsList(settings, key, convertToAbsolutePaths=False):
#
# =============================================================================
class ExtensionWizard:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- parent.title = "Extension Wizard"
- parent.icon = qt.QIcon(":/Icons/Medium/ExtensionWizard.png")
- parent.categories = ["Developer Tools"]
- parent.dependencies = []
- parent.contributors = ["Matthew Woehlke (Kitware)"]
- parent.helpText = """
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ parent.title = "Extension Wizard"
+ parent.icon = qt.QIcon(":/Icons/Medium/ExtensionWizard.png")
+ parent.categories = ["Developer Tools"]
+ parent.dependencies = []
+ parent.contributors = ["Matthew Woehlke (Kitware)"]
+ parent.helpText = """
This module provides tools to create and manage extensions from within Slicer.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This work is supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community.
"""
- self.parent = parent
+ self.parent = parent
- self.settingsPanel = SettingsPanel()
- slicer.app.settingsDialog().addPanel("Extension Wizard", self.settingsPanel)
+ self.settingsPanel = SettingsPanel()
+ slicer.app.settingsDialog().addPanel("Extension Wizard", self.settingsPanel)
# =============================================================================
@@ -64,410 +64,410 @@ def __init__(self, parent):
#
# =============================================================================
class ExtensionWizardWidget:
- # ---------------------------------------------------------------------------
- def __init__(self, parent=None):
- if not parent:
- self.parent = qt.QWidget()
- self.parent.setLayout(qt.QVBoxLayout())
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent=None):
+ if not parent:
+ self.parent = qt.QWidget()
+ self.parent.setLayout(qt.QVBoxLayout())
- else:
- self.parent = parent
-
- self.layout = self.parent.layout()
-
- if not parent:
- self.setup()
- self.parent.show()
-
- self.extensionProject = None
- self.extensionDescription = None
- self.extensionLocation = None
-
- self.templateManager = None
- self.setupTemplates()
-
- # ---------------------------------------------------------------------------
- def setup(self):
- # Instantiate and connect widgets ...
-
- icon = self.parent.style().standardIcon(qt.QStyle.SP_ArrowForward)
- iconSize = qt.QSize(22, 22)
-
- def createToolButton(text):
- tb = qt.QToolButton()
-
- tb.text = text
- tb.icon = icon
-
- font = tb.font
- font.setBold(True)
- font.setPixelSize(14)
- tb.font = font
-
- tb.iconSize = iconSize
- tb.toolButtonStyle = qt.Qt.ToolButtonTextBesideIcon
- tb.autoRaise = True
-
- return tb
-
- def createReadOnlyLineEdit():
- le = qt.QLineEdit()
- le.readOnly = True
- le.frame = False
- le.styleSheet = "QLineEdit { background:transparent; }"
- le.cursor = qt.QCursor(qt.Qt.IBeamCursor)
- return le
-
- #
- # Tools Area
- #
- self.toolsCollapsibleButton = ctk.ctkCollapsibleButton()
- self.toolsCollapsibleButton.text = "Extension Tools"
- self.layout.addWidget(self.toolsCollapsibleButton)
-
- self.createExtensionButton = createToolButton("Create Extension")
- self.createExtensionButton.connect('clicked(bool)', self.createExtension)
-
- self.selectExtensionButton = createToolButton("Select Extension")
- self.selectExtensionButton.connect('clicked(bool)', self.selectExtension)
-
- toolsLayout = qt.QVBoxLayout(self.toolsCollapsibleButton)
- toolsLayout.addWidget(self.createExtensionButton)
- toolsLayout.addWidget(self.selectExtensionButton)
-
- #
- # Editor Area
- #
- self.editorCollapsibleButton = ctk.ctkCollapsibleButton()
- self.editorCollapsibleButton.text = "Extension Editor"
- self.editorCollapsibleButton.enabled = False
- self.editorCollapsibleButton.collapsed = True
- self.layout.addWidget(self.editorCollapsibleButton)
-
- self.extensionNameField = createReadOnlyLineEdit()
- self.extensionLocationField = createReadOnlyLineEdit()
- self.extensionRepositoryField = createReadOnlyLineEdit()
-
- self.extensionContentsModel = qt.QFileSystemModel()
- self.extensionContentsView = qt.QTreeView()
- self.extensionContentsView.setModel(self.extensionContentsModel)
- self.extensionContentsView.sortingEnabled = True
- self.extensionContentsView.hideColumn(3)
-
- self.createExtensionModuleButton = createToolButton("Add Module to Extension")
- self.createExtensionModuleButton.connect('clicked(bool)',
- self.createExtensionModule)
-
- self.editExtensionMetadataButton = createToolButton("Edit Extension Metadata")
- self.editExtensionMetadataButton.connect('clicked(bool)',
- self.editExtensionMetadata)
-
- editorLayout = qt.QFormLayout(self.editorCollapsibleButton)
- editorLayout.addRow("Name:", self.extensionNameField)
- editorLayout.addRow("Location:", self.extensionLocationField)
- editorLayout.addRow("Repository:", self.extensionRepositoryField)
- editorLayout.addRow("Contents:", self.extensionContentsView)
- editorLayout.addRow(self.createExtensionModuleButton)
- editorLayout.addRow(self.editExtensionMetadataButton)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- # ---------------------------------------------------------------------------
- def cleanup(self):
- pass
-
- # ---------------------------------------------------------------------------
- def setupTemplates(self):
- self.templateManager = SlicerWizard.TemplateManager()
-
- builtinPath = builtinTemplatePath()
- if builtinPath is not None:
- try:
- self.templateManager.addPath(builtinPath)
- except:
- qt.qWarning("failed to add built-in template path %r" % builtinPath)
- qt.qWarning(traceback.format_exc())
-
- # Read base template paths
- s = qt.QSettings()
- for path in _settingsList(s, userTemplatePathKey(), convertToAbsolutePaths=True):
- try:
- self.templateManager.addPath(path)
- except:
- qt.qWarning("failed to add template path %r" % path)
- qt.qWarning(traceback.format_exc())
-
- # Read per-category template paths
- s.beginGroup(userTemplatePathKey())
- for c in s.allKeys():
- for path in _settingsList(s, c, convertToAbsolutePaths=True):
+ else:
+ self.parent = parent
+
+ self.layout = self.parent.layout()
+
+ if not parent:
+ self.setup()
+ self.parent.show()
+
+ self.extensionProject = None
+ self.extensionDescription = None
+ self.extensionLocation = None
+
+ self.templateManager = None
+ self.setupTemplates()
+
+ # ---------------------------------------------------------------------------
+ def setup(self):
+ # Instantiate and connect widgets ...
+
+ icon = self.parent.style().standardIcon(qt.QStyle.SP_ArrowForward)
+ iconSize = qt.QSize(22, 22)
+
+ def createToolButton(text):
+ tb = qt.QToolButton()
+
+ tb.text = text
+ tb.icon = icon
+
+ font = tb.font
+ font.setBold(True)
+ font.setPixelSize(14)
+ tb.font = font
+
+ tb.iconSize = iconSize
+ tb.toolButtonStyle = qt.Qt.ToolButtonTextBesideIcon
+ tb.autoRaise = True
+
+ return tb
+
+ def createReadOnlyLineEdit():
+ le = qt.QLineEdit()
+ le.readOnly = True
+ le.frame = False
+ le.styleSheet = "QLineEdit { background:transparent; }"
+ le.cursor = qt.QCursor(qt.Qt.IBeamCursor)
+ return le
+
+ #
+ # Tools Area
+ #
+ self.toolsCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.toolsCollapsibleButton.text = "Extension Tools"
+ self.layout.addWidget(self.toolsCollapsibleButton)
+
+ self.createExtensionButton = createToolButton("Create Extension")
+ self.createExtensionButton.connect('clicked(bool)', self.createExtension)
+
+ self.selectExtensionButton = createToolButton("Select Extension")
+ self.selectExtensionButton.connect('clicked(bool)', self.selectExtension)
+
+ toolsLayout = qt.QVBoxLayout(self.toolsCollapsibleButton)
+ toolsLayout.addWidget(self.createExtensionButton)
+ toolsLayout.addWidget(self.selectExtensionButton)
+
+ #
+ # Editor Area
+ #
+ self.editorCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.editorCollapsibleButton.text = "Extension Editor"
+ self.editorCollapsibleButton.enabled = False
+ self.editorCollapsibleButton.collapsed = True
+ self.layout.addWidget(self.editorCollapsibleButton)
+
+ self.extensionNameField = createReadOnlyLineEdit()
+ self.extensionLocationField = createReadOnlyLineEdit()
+ self.extensionRepositoryField = createReadOnlyLineEdit()
+
+ self.extensionContentsModel = qt.QFileSystemModel()
+ self.extensionContentsView = qt.QTreeView()
+ self.extensionContentsView.setModel(self.extensionContentsModel)
+ self.extensionContentsView.sortingEnabled = True
+ self.extensionContentsView.hideColumn(3)
+
+ self.createExtensionModuleButton = createToolButton("Add Module to Extension")
+ self.createExtensionModuleButton.connect('clicked(bool)',
+ self.createExtensionModule)
+
+ self.editExtensionMetadataButton = createToolButton("Edit Extension Metadata")
+ self.editExtensionMetadataButton.connect('clicked(bool)',
+ self.editExtensionMetadata)
+
+ editorLayout = qt.QFormLayout(self.editorCollapsibleButton)
+ editorLayout.addRow("Name:", self.extensionNameField)
+ editorLayout.addRow("Location:", self.extensionLocationField)
+ editorLayout.addRow("Repository:", self.extensionRepositoryField)
+ editorLayout.addRow("Contents:", self.extensionContentsView)
+ editorLayout.addRow(self.createExtensionModuleButton)
+ editorLayout.addRow(self.editExtensionMetadataButton)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ # ---------------------------------------------------------------------------
+ def cleanup(self):
+ pass
+
+ # ---------------------------------------------------------------------------
+ def setupTemplates(self):
+ self.templateManager = SlicerWizard.TemplateManager()
+
+ builtinPath = builtinTemplatePath()
+ if builtinPath is not None:
+ try:
+ self.templateManager.addPath(builtinPath)
+ except:
+ qt.qWarning("failed to add built-in template path %r" % builtinPath)
+ qt.qWarning(traceback.format_exc())
+
+ # Read base template paths
+ s = qt.QSettings()
+ for path in _settingsList(s, userTemplatePathKey(), convertToAbsolutePaths=True):
+ try:
+ self.templateManager.addPath(path)
+ except:
+ qt.qWarning("failed to add template path %r" % path)
+ qt.qWarning(traceback.format_exc())
+
+ # Read per-category template paths
+ s.beginGroup(userTemplatePathKey())
+ for c in s.allKeys():
+ for path in _settingsList(s, c, convertToAbsolutePaths=True):
+ try:
+ self.templateManager.addCategoryPath(c, path)
+ except:
+ mp = (c, path)
+ qt.qWarning("failed to add template path %r for category %r" % mp)
+ qt.qWarning(traceback.format_exc())
+
+ # ---------------------------------------------------------------------------
+ def createExtension(self):
+ dlg = CreateComponentDialog("extension", self.parent.window())
+ dlg.setTemplates(self.templateManager.templates("extensions"))
+
+ while dlg.exec_() == qt.QDialog.Accepted:
+
+ # If the selected destination is in a repository then use the root of that repository
+ # as destination
+ try:
+ repo = SlicerWizard.Utilities.getRepo(dlg.destination)
+
+ createInSubdirectory = True
+ requireEmptyDirectory = True
+
+ if repo is None:
+ destination = os.path.join(dlg.destination, dlg.componentName)
+ if os.path.exists(destination):
+ raise OSError("create extension: refusing to overwrite"
+ " existing directory '%s'" % destination)
+ createInSubdirectory = False
+
+ else:
+ destination = SlicerWizard.Utilities.localRoot(repo)
+ cmakeFile = os.path.join(destination, "CMakeLists.txt")
+ createInSubdirectory = False # create the files in the destination directory
+ requireEmptyDirectory = False # we only check if no CMakeLists.txt file exists
+ if os.path.exists(cmakeFile):
+ raise OSError("create extension: refusing to overwrite"
+ " directory containing CMakeLists.txt file at '%s'" % dlg.destination)
+
+ path = self.templateManager.copyTemplate(
+ destination, "extensions",
+ dlg.componentType, dlg.componentName,
+ createInSubdirectory, requireEmptyDirectory)
+
+ except:
+ if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the extension.",
+ parent=self.parent.window(), detailedText=traceback.format_exc()):
+ return
+
+ continue
+
+ if self.selectExtension(path):
+ self.editExtensionMetadata()
+
+ return
+
+ # ---------------------------------------------------------------------------
+ def selectExtension(self, path=None):
+ if path is None or isinstance(path, bool):
+ path = qt.QFileDialog.getExistingDirectory(
+ self.parent.window(), "Select Extension...",
+ self.extensionLocation)
+
+ if not len(path):
+ return False
+
+ # Attempt to open extension
try:
- self.templateManager.addCategoryPath(c, path)
- except:
- mp = (c, path)
- qt.qWarning("failed to add template path %r for category %r" % mp)
- qt.qWarning(traceback.format_exc())
-
- # ---------------------------------------------------------------------------
- def createExtension(self):
- dlg = CreateComponentDialog("extension", self.parent.window())
- dlg.setTemplates(self.templateManager.templates("extensions"))
-
- while dlg.exec_() == qt.QDialog.Accepted:
+ repo = SlicerWizard.Utilities.getRepo(path)
- # If the selected destination is in a repository then use the root of that repository
- # as destination
- try:
- repo = SlicerWizard.Utilities.getRepo(dlg.destination)
+ xd = None
+ if repo:
+ try:
+ xd = SlicerWizard.ExtensionDescription(repo=repo)
+ path = SlicerWizard.Utilities.localRoot(repo)
+ except:
+ # Failed to determine repository path automatically (git is not installed, etc.)
+ # Continue with assuming that the user selected the top-level directory of the extension.
+ pass
- createInSubdirectory = True
- requireEmptyDirectory = True
+ if not xd:
+ xd = SlicerWizard.ExtensionDescription(sourcedir=path)
- if repo is None:
- destination = os.path.join(dlg.destination, dlg.componentName)
- if os.path.exists(destination):
- raise OSError("create extension: refusing to overwrite"
- " existing directory '%s'" % destination)
- createInSubdirectory = False
+ xp = SlicerWizard.ExtensionProject(path)
- else:
- destination = SlicerWizard.Utilities.localRoot(repo)
- cmakeFile = os.path.join(destination, "CMakeLists.txt")
- createInSubdirectory = False # create the files in the destination directory
- requireEmptyDirectory = False # we only check if no CMakeLists.txt file exists
- if os.path.exists(cmakeFile):
- raise OSError("create extension: refusing to overwrite"
- " directory containing CMakeLists.txt file at '%s'" % dlg.destination)
-
- path = self.templateManager.copyTemplate(
- destination, "extensions",
- dlg.componentType, dlg.componentName,
- createInSubdirectory, requireEmptyDirectory)
-
- except:
- if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the extension.",
- parent=self.parent.window(), detailedText=traceback.format_exc()):
- return
-
- continue
-
- if self.selectExtension(path):
- self.editExtensionMetadata()
-
- return
-
- # ---------------------------------------------------------------------------
- def selectExtension(self, path=None):
- if path is None or isinstance(path, bool):
- path = qt.QFileDialog.getExistingDirectory(
- self.parent.window(), "Select Extension...",
- self.extensionLocation)
-
- if not len(path):
- return False
-
- # Attempt to open extension
- try:
- repo = SlicerWizard.Utilities.getRepo(path)
-
- xd = None
- if repo:
- try:
- xd = SlicerWizard.ExtensionDescription(repo=repo)
- path = SlicerWizard.Utilities.localRoot(repo)
except:
- # Failed to determine repository path automatically (git is not installed, etc.)
- # Continue with assuming that the user selected the top-level directory of the extension.
- pass
-
- if not xd:
- xd = SlicerWizard.ExtensionDescription(sourcedir=path)
-
- xp = SlicerWizard.ExtensionProject(path)
+ slicer.util.errorDisplay("Failed to open extension '%s'." % path, parent=self.parent.window(),
+ detailedText=traceback.format_exc(), standardButtons=qt.QMessageBox.Close)
+ return False
- except:
- slicer.util.errorDisplay("Failed to open extension '%s'." % path, parent=self.parent.window(),
- detailedText=traceback.format_exc(), standardButtons=qt.QMessageBox.Close)
- return False
+ # Enable and show edit section
+ self.editorCollapsibleButton.enabled = True
+ self.editorCollapsibleButton.collapsed = False
- # Enable and show edit section
- self.editorCollapsibleButton.enabled = True
- self.editorCollapsibleButton.collapsed = False
+ # Populate edit information
+ self.extensionNameField.text = xp.project
+ self.extensionLocationField.text = path
- # Populate edit information
- self.extensionNameField.text = xp.project
- self.extensionLocationField.text = path
+ if xd.scmurl == "NA":
+ if repo is None:
+ repoText = "(none)"
+ elif hasattr(repo, "remotes"):
+ repoText = "(local git repository)"
+ else:
+ repoText = "(unknown local repository)"
- if xd.scmurl == "NA":
- if repo is None:
- repoText = "(none)"
- elif hasattr(repo, "remotes"):
- repoText = "(local git repository)"
- else:
- repoText = "(unknown local repository)"
+ self.extensionRepositoryField.clear()
+ self.extensionRepositoryField.placeholderText = repoText
- self.extensionRepositoryField.clear()
- self.extensionRepositoryField.placeholderText = repoText
-
- else:
- self.extensionRepositoryField.text = xd.scmurl
-
- ri = self.extensionContentsModel.setRootPath(path)
- self.extensionContentsView.setRootIndex(ri)
-
- w = self.extensionContentsView.width
- self.extensionContentsView.setColumnWidth(0, int((w * 4) / 9))
-
- # Prompt to load scripted modules from extension
- self.loadModules(path)
-
- # Store extension location, project and description for later use
- self.extensionProject = xp
- self.extensionDescription = xd
- self.extensionLocation = path
- return True
-
- # ---------------------------------------------------------------------------
- def loadModules(self, path, depth=1):
- # Get list of modules in specified path
- modules = ModuleInfo.findModules(path, depth)
-
- # Determine which modules in above are not already loaded
- factory = slicer.app.moduleManager().factoryManager()
- loadedModules = factory.instantiatedModuleNames()
-
- candidates = [m for m in modules if m.key not in loadedModules]
-
- # Prompt to load additional module(s)
- if len(candidates):
- dlg = LoadModulesDialog(self.parent.window())
- dlg.setModules(candidates)
-
- if dlg.exec_() == qt.QDialog.Accepted:
- modulesToLoad = dlg.selectedModules
-
- # Add module(s) to permanent search paths, if requested
- if dlg.addToSearchPaths:
- settings = slicer.app.revisionUserSettings()
- rawSearchPaths = list(_settingsList(settings, "Modules/AdditionalPaths", convertToAbsolutePaths=True))
- searchPaths = [qt.QDir(path) for path in rawSearchPaths]
- modified = False
-
- for module in modulesToLoad:
- rawPath = os.path.dirname(module.path)
- path = qt.QDir(rawPath)
- if not path in searchPaths:
- searchPaths.append(path)
- rawSearchPaths.append(rawPath)
- modified = True
-
- if modified:
- settings.setValue("Modules/AdditionalPaths", slicer.app.toSlicerHomeRelativePaths(rawSearchPaths))
-
- # Enable developer mode (shows Reload&Test section, etc.), if requested
- if dlg.enableDeveloperMode:
- qt.QSettings().setValue('Developer/DeveloperMode', 'true')
-
- # Register requested module(s)
- failed = []
-
- for module in modulesToLoad:
- factory.registerModule(qt.QFileInfo(module.path))
- if not factory.isRegistered(module.key):
- failed.append(module)
-
- if len(failed):
-
- if len(failed) > 1:
- text = "The following modules could not be registered:"
- else:
- text = "The '%s' module could not be registered:" % failed[0].key
-
- failedFormat = ""
- detailedInformation = "".join(
- [failedFormat % m.__dict__ for m in failed])
-
- slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Module loading failed",
- standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation)
-
- return
-
- # Instantiate and load requested module(s)
- if not factory.loadModules([module.key for module in modulesToLoad]):
- text = ("The module factory manager reported an error. "
- "One or more of the requested module(s) and/or "
- "dependencies thereof may not have been loaded.")
- slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Error loading module(s)",
- standardButtons=qt.QMessageBox.Close)
-
- # ---------------------------------------------------------------------------
- def createExtensionModule(self):
- if (self.extensionLocation is None):
- # Action shouldn't be enabled if no extension is selected, but guard
- # against that just in case...
- return
-
- dlg = CreateComponentDialog("module", self.parent.window())
- dlg.setTemplates(self.templateManager.templates("modules"),
- default="scripted")
- dlg.showDestination = False
-
- while dlg.exec_() == qt.QDialog.Accepted:
- name = dlg.componentName
-
- try:
- self.templateManager.copyTemplate(self.extensionLocation, "modules",
- dlg.componentType, name)
-
- except:
- if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the module.",
- parent=self.parent.window(),
- detailedText=traceback.format_exc()):
- return
-
- continue
-
- try:
- self.extensionProject.addModule(name)
- self.extensionProject.save()
-
- except:
- text = "An error occurred while adding the module to the extension."
- detailedInformation = "The module has been created, but the extension" \
- " CMakeLists.txt could not be updated. In order" \
- " to include the module in the extension build," \
- " you will need to update the extension" \
- " CMakeLists.txt by hand."
- slicer.util.errorDisplay(text, parent=self.parent.window(), detailedText=traceback.format_exc(),
- standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation)
-
- self.loadModules(os.path.join(self.extensionLocation, name), depth=0)
- return
-
- # ---------------------------------------------------------------------------
- def editExtensionMetadata(self):
- xd = self.extensionDescription
- xp = self.extensionProject
-
- dlg = EditExtensionMetadataDialog(self.parent.window())
- dlg.project = xp.project
- dlg.category = xd.category
- dlg.description = xd.description
- dlg.contributors = xd.contributors
-
- if dlg.exec_() == qt.QDialog.Accepted:
- # Update cached metadata
- xd.category = dlg.category
- xd.description = dlg.description
- xd.contributors = dlg.contributors
-
- # Write changes to extension project file (CMakeLists.txt)
- xp.project = dlg.project
- xp.setValue("EXTENSION_CATEGORY", xd.category)
- xp.setValue("EXTENSION_DESCRIPTION", xd.description)
- xp.setValue("EXTENSION_CONTRIBUTORS", xd.contributors)
- xp.save()
-
- # Update the displayed extension name
- self.extensionNameField.text = xp.project
+ else:
+ self.extensionRepositoryField.text = xd.scmurl
+
+ ri = self.extensionContentsModel.setRootPath(path)
+ self.extensionContentsView.setRootIndex(ri)
+
+ w = self.extensionContentsView.width
+ self.extensionContentsView.setColumnWidth(0, int((w * 4) / 9))
+
+ # Prompt to load scripted modules from extension
+ self.loadModules(path)
+
+ # Store extension location, project and description for later use
+ self.extensionProject = xp
+ self.extensionDescription = xd
+ self.extensionLocation = path
+ return True
+
+ # ---------------------------------------------------------------------------
+ def loadModules(self, path, depth=1):
+ # Get list of modules in specified path
+ modules = ModuleInfo.findModules(path, depth)
+
+ # Determine which modules in above are not already loaded
+ factory = slicer.app.moduleManager().factoryManager()
+ loadedModules = factory.instantiatedModuleNames()
+
+ candidates = [m for m in modules if m.key not in loadedModules]
+
+ # Prompt to load additional module(s)
+ if len(candidates):
+ dlg = LoadModulesDialog(self.parent.window())
+ dlg.setModules(candidates)
+
+ if dlg.exec_() == qt.QDialog.Accepted:
+ modulesToLoad = dlg.selectedModules
+
+ # Add module(s) to permanent search paths, if requested
+ if dlg.addToSearchPaths:
+ settings = slicer.app.revisionUserSettings()
+ rawSearchPaths = list(_settingsList(settings, "Modules/AdditionalPaths", convertToAbsolutePaths=True))
+ searchPaths = [qt.QDir(path) for path in rawSearchPaths]
+ modified = False
+
+ for module in modulesToLoad:
+ rawPath = os.path.dirname(module.path)
+ path = qt.QDir(rawPath)
+ if not path in searchPaths:
+ searchPaths.append(path)
+ rawSearchPaths.append(rawPath)
+ modified = True
+
+ if modified:
+ settings.setValue("Modules/AdditionalPaths", slicer.app.toSlicerHomeRelativePaths(rawSearchPaths))
+
+ # Enable developer mode (shows Reload&Test section, etc.), if requested
+ if dlg.enableDeveloperMode:
+ qt.QSettings().setValue('Developer/DeveloperMode', 'true')
+
+ # Register requested module(s)
+ failed = []
+
+ for module in modulesToLoad:
+ factory.registerModule(qt.QFileInfo(module.path))
+ if not factory.isRegistered(module.key):
+ failed.append(module)
+
+ if len(failed):
+
+ if len(failed) > 1:
+ text = "The following modules could not be registered:"
+ else:
+ text = "The '%s' module could not be registered:" % failed[0].key
+
+ failedFormat = ""
+ detailedInformation = "".join(
+ [failedFormat % m.__dict__ for m in failed])
+
+ slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Module loading failed",
+ standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation)
+
+ return
+
+ # Instantiate and load requested module(s)
+ if not factory.loadModules([module.key for module in modulesToLoad]):
+ text = ("The module factory manager reported an error. "
+ "One or more of the requested module(s) and/or "
+ "dependencies thereof may not have been loaded.")
+ slicer.util.errorDisplay(text, parent=self.parent.window(), windowTitle="Error loading module(s)",
+ standardButtons=qt.QMessageBox.Close)
+
+ # ---------------------------------------------------------------------------
+ def createExtensionModule(self):
+ if (self.extensionLocation is None):
+ # Action shouldn't be enabled if no extension is selected, but guard
+ # against that just in case...
+ return
+
+ dlg = CreateComponentDialog("module", self.parent.window())
+ dlg.setTemplates(self.templateManager.templates("modules"),
+ default="scripted")
+ dlg.showDestination = False
+
+ while dlg.exec_() == qt.QDialog.Accepted:
+ name = dlg.componentName
+
+ try:
+ self.templateManager.copyTemplate(self.extensionLocation, "modules",
+ dlg.componentType, name)
+
+ except:
+ if not slicer.util.confirmRetryCloseDisplay("An error occurred while trying to create the module.",
+ parent=self.parent.window(),
+ detailedText=traceback.format_exc()):
+ return
+
+ continue
+
+ try:
+ self.extensionProject.addModule(name)
+ self.extensionProject.save()
+
+ except:
+ text = "An error occurred while adding the module to the extension."
+ detailedInformation = "The module has been created, but the extension" \
+ " CMakeLists.txt could not be updated. In order" \
+ " to include the module in the extension build," \
+ " you will need to update the extension" \
+ " CMakeLists.txt by hand."
+ slicer.util.errorDisplay(text, parent=self.parent.window(), detailedText=traceback.format_exc(),
+ standardButtons=qt.QMessageBox.Close, informativeText=detailedInformation)
+
+ self.loadModules(os.path.join(self.extensionLocation, name), depth=0)
+ return
+
+ # ---------------------------------------------------------------------------
+ def editExtensionMetadata(self):
+ xd = self.extensionDescription
+ xp = self.extensionProject
+
+ dlg = EditExtensionMetadataDialog(self.parent.window())
+ dlg.project = xp.project
+ dlg.category = xd.category
+ dlg.description = xd.description
+ dlg.contributors = xd.contributors
+
+ if dlg.exec_() == qt.QDialog.Accepted:
+ # Update cached metadata
+ xd.category = dlg.category
+ xd.description = dlg.description
+ xd.contributors = dlg.contributors
+
+ # Write changes to extension project file (CMakeLists.txt)
+ xp.project = dlg.project
+ xp.setValue("EXTENSION_CATEGORY", xd.category)
+ xp.setValue("EXTENSION_DESCRIPTION", xd.description)
+ xp.setValue("EXTENSION_CONTRIBUTORS", xd.contributors)
+ xp.save()
+
+ # Update the displayed extension name
+ self.extensionNameField.text = xp.project
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py
index 5b6e1980ca8..a392ef3f8d6 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/CreateComponentDialog.py
@@ -12,32 +12,32 @@
#
# =============================================================================
class _ui_CreateComponentDialog:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- self.vLayout = qt.QVBoxLayout(parent)
- self.formLayout = qt.QFormLayout()
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ self.vLayout = qt.QVBoxLayout(parent)
+ self.formLayout = qt.QFormLayout()
- self.componentName = qt.QLineEdit()
- self.formLayout.addRow("Name:", self.componentName)
+ self.componentName = qt.QLineEdit()
+ self.formLayout.addRow("Name:", self.componentName)
- self.componentNameValidator = qt.QRegExpValidator(
- qt.QRegExp(r"^[a-zA-Z_][a-zA-Z0-9_]*$"))
- self.componentName.setValidator(self.componentNameValidator)
+ self.componentNameValidator = qt.QRegExpValidator(
+ qt.QRegExp(r"^[a-zA-Z_][a-zA-Z0-9_]*$"))
+ self.componentName.setValidator(self.componentNameValidator)
- self.componentType = qt.QComboBox()
- self.formLayout.addRow("Type:", self.componentType)
+ self.componentType = qt.QComboBox()
+ self.formLayout.addRow("Type:", self.componentType)
- self.destination = ctk.ctkPathLineEdit()
- self.destination.filters = ctk.ctkPathLineEdit.Dirs
- self.formLayout.addRow("Destination:", self.destination)
+ self.destination = ctk.ctkPathLineEdit()
+ self.destination.filters = ctk.ctkPathLineEdit.Dirs
+ self.formLayout.addRow("Destination:", self.destination)
- self.vLayout.addLayout(self.formLayout)
- self.vLayout.addStretch(1)
+ self.vLayout.addLayout(self.formLayout)
+ self.vLayout.addStretch(1)
- self.buttonBox = qt.QDialogButtonBox()
- self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok |
- qt.QDialogButtonBox.Cancel)
- self.vLayout.addWidget(self.buttonBox)
+ self.buttonBox = qt.QDialogButtonBox()
+ self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok |
+ qt.QDialogButtonBox.Cancel)
+ self.vLayout.addWidget(self.buttonBox)
# =============================================================================
@@ -46,72 +46,72 @@ def __init__(self, parent):
#
# =============================================================================
class CreateComponentDialog:
- # ---------------------------------------------------------------------------
- def __init__(self, componenttype, parent):
- self.dialog = qt.QDialog(parent)
- self.ui = _ui_CreateComponentDialog(self.dialog)
-
- self.ui.buttonBox.connect("accepted()", self.accept)
- self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
-
- self._typelc = componenttype.lower()
- self._typetc = componenttype.title()
-
- # ---------------------------------------------------------------------------
- def accept(self):
- if not len(self.componentName):
- slicer.util.errorDisplay("%s name may not be empty." % self._typetc,
- windowTitle="Cannot create %s" % self._typelc, parent=self.dialog)
- return
-
- if self.showDestination:
- dest = self.destination
- if not len(dest) or not os.path.exists(dest):
- slicer.util.errorDisplay("Destination must be an existing directory.",
- windowTitle="Cannot create %s" % self._typelc, parent=self.dialog)
- return
-
- self.dialog.accept()
-
- # ---------------------------------------------------------------------------
- def setTemplates(self, templates, default="default"):
- self.ui.componentType.clear()
- self.ui.componentType.addItems(templates)
-
- try:
- self.ui.componentType.currentIndex = templates.index(default)
- except ValueError:
- pass
-
- # ---------------------------------------------------------------------------
- def exec_(self):
- return self.dialog.exec_()
-
- # ---------------------------------------------------------------------------
- @property
- def showDestination(self):
- return self.ui.destination.visible
-
- # ---------------------------------------------------------------------------
- @showDestination.setter
- def showDestination(self, value):
- field = self.ui.destination
- label = self.ui.formLayout.labelForField(field)
-
- label.visible = value
- field.visible = value
-
- # ---------------------------------------------------------------------------
- @property
- def componentName(self):
- return self.ui.componentName.text
-
- # ---------------------------------------------------------------------------
- @property
- def componentType(self):
- return self.ui.componentType.currentText
-
- # ---------------------------------------------------------------------------
- @property
- def destination(self):
- return self.ui.destination.currentPath
+ # ---------------------------------------------------------------------------
+ def __init__(self, componenttype, parent):
+ self.dialog = qt.QDialog(parent)
+ self.ui = _ui_CreateComponentDialog(self.dialog)
+
+ self.ui.buttonBox.connect("accepted()", self.accept)
+ self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
+
+ self._typelc = componenttype.lower()
+ self._typetc = componenttype.title()
+
+ # ---------------------------------------------------------------------------
+ def accept(self):
+ if not len(self.componentName):
+ slicer.util.errorDisplay("%s name may not be empty." % self._typetc,
+ windowTitle="Cannot create %s" % self._typelc, parent=self.dialog)
+ return
+
+ if self.showDestination:
+ dest = self.destination
+ if not len(dest) or not os.path.exists(dest):
+ slicer.util.errorDisplay("Destination must be an existing directory.",
+ windowTitle="Cannot create %s" % self._typelc, parent=self.dialog)
+ return
+
+ self.dialog.accept()
+
+ # ---------------------------------------------------------------------------
+ def setTemplates(self, templates, default="default"):
+ self.ui.componentType.clear()
+ self.ui.componentType.addItems(templates)
+
+ try:
+ self.ui.componentType.currentIndex = templates.index(default)
+ except ValueError:
+ pass
+
+ # ---------------------------------------------------------------------------
+ def exec_(self):
+ return self.dialog.exec_()
+
+ # ---------------------------------------------------------------------------
+ @property
+ def showDestination(self):
+ return self.ui.destination.visible
+
+ # ---------------------------------------------------------------------------
+ @showDestination.setter
+ def showDestination(self, value):
+ field = self.ui.destination
+ label = self.ui.formLayout.labelForField(field)
+
+ label.visible = value
+ field.visible = value
+
+ # ---------------------------------------------------------------------------
+ @property
+ def componentName(self):
+ return self.ui.componentName.text
+
+ # ---------------------------------------------------------------------------
+ @property
+ def componentType(self):
+ return self.ui.componentType.currentText
+
+ # ---------------------------------------------------------------------------
+ @property
+ def destination(self):
+ return self.ui.destination.currentPath
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py
index 991004de891..be5dbd27aeb 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/DirectoryListWidget.py
@@ -9,22 +9,22 @@
#
# =============================================================================
class _ui_DirectoryListWidget:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- layout = qt.QGridLayout(parent)
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ layout = qt.QGridLayout(parent)
- self.pathList = slicer.qSlicerDirectoryListView()
- layout.addWidget(self.pathList, 0, 0, 3, 1)
+ self.pathList = slicer.qSlicerDirectoryListView()
+ layout.addWidget(self.pathList, 0, 0, 3, 1)
- self.addPathButton = qt.QToolButton()
- self.addPathButton.icon = qt.QIcon.fromTheme("list-add")
- self.addPathButton.text = "Add"
- layout.addWidget(self.addPathButton, 0, 1)
+ self.addPathButton = qt.QToolButton()
+ self.addPathButton.icon = qt.QIcon.fromTheme("list-add")
+ self.addPathButton.text = "Add"
+ layout.addWidget(self.addPathButton, 0, 1)
- self.removePathButton = qt.QToolButton()
- self.removePathButton.icon = qt.QIcon.fromTheme("list-remove")
- self.removePathButton.text = "Remove"
- layout.addWidget(self.removePathButton, 1, 1)
+ self.removePathButton = qt.QToolButton()
+ self.removePathButton.icon = qt.QIcon.fromTheme("list-remove")
+ self.removePathButton.text = "Remove"
+ layout.addWidget(self.removePathButton, 1, 1)
# =============================================================================
@@ -33,17 +33,17 @@ def __init__(self, parent):
#
# =============================================================================
class DirectoryListWidget(qt.QWidget):
- # ---------------------------------------------------------------------------
- def __init__(self, *args, **kwargs):
- qt.QWidget.__init__(self, *args, **kwargs)
- self.ui = _ui_DirectoryListWidget(self)
-
- self.ui.addPathButton.connect('clicked()', self.addDirectory)
- self.ui.removePathButton.connect('clicked()', self.ui.pathList,
- 'removeSelectedDirectories()')
-
- # ---------------------------------------------------------------------------
- def addDirectory(self):
- path = qt.QFileDialog.getExistingDirectory(self.window(), "Select folder")
- if len(path):
- self.ui.pathList.addDirectory(path)
+ # ---------------------------------------------------------------------------
+ def __init__(self, *args, **kwargs):
+ qt.QWidget.__init__(self, *args, **kwargs)
+ self.ui = _ui_DirectoryListWidget(self)
+
+ self.ui.addPathButton.connect('clicked()', self.addDirectory)
+ self.ui.removePathButton.connect('clicked()', self.ui.pathList,
+ 'removeSelectedDirectories()')
+
+ # ---------------------------------------------------------------------------
+ def addDirectory(self):
+ path = qt.QFileDialog.getExistingDirectory(self.window(), "Select folder")
+ if len(path):
+ self.ui.pathList.addDirectory(path)
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py
index 62d9d307b26..94d532819b7 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditExtensionMetadataDialog.py
@@ -9,8 +9,8 @@
# -----------------------------------------------------------------------------
def _map_property(objfunc, name):
- return property(lambda self: getattr(objfunc(self), name),
- lambda self, value: setattr(objfunc(self), name, value))
+ return property(lambda self: getattr(objfunc(self), name),
+ lambda self, value: setattr(objfunc(self), name, value))
# =============================================================================
@@ -19,35 +19,35 @@ def _map_property(objfunc, name):
#
# =============================================================================
class _ui_EditExtensionMetadataDialog:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- vLayout = qt.QVBoxLayout(parent)
- formLayout = qt.QFormLayout()
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ vLayout = qt.QVBoxLayout(parent)
+ formLayout = qt.QFormLayout()
- self.nameEdit = qt.QLineEdit()
- formLayout.addRow("Name:", self.nameEdit)
+ self.nameEdit = qt.QLineEdit()
+ formLayout.addRow("Name:", self.nameEdit)
- self.categoryEdit = qt.QLineEdit()
- formLayout.addRow("Category:", self.categoryEdit)
+ self.categoryEdit = qt.QLineEdit()
+ formLayout.addRow("Category:", self.categoryEdit)
- self.descriptionEdit = qt.QTextEdit()
- self.descriptionEdit.acceptRichText = False
- formLayout.addRow("Description:", self.descriptionEdit)
+ self.descriptionEdit = qt.QTextEdit()
+ self.descriptionEdit.acceptRichText = False
+ formLayout.addRow("Description:", self.descriptionEdit)
- self.contributorsList = EditableTreeWidget()
- self.contributorsList.rootIsDecorated = False
- self.contributorsList.selectionBehavior = qt.QAbstractItemView.SelectRows
- self.contributorsList.selectionMode = qt.QAbstractItemView.ExtendedSelection
- self.contributorsList.setHeaderLabels(["Name", "Organization"])
- formLayout.addRow("Contributors:", self.contributorsList)
+ self.contributorsList = EditableTreeWidget()
+ self.contributorsList.rootIsDecorated = False
+ self.contributorsList.selectionBehavior = qt.QAbstractItemView.SelectRows
+ self.contributorsList.selectionMode = qt.QAbstractItemView.ExtendedSelection
+ self.contributorsList.setHeaderLabels(["Name", "Organization"])
+ formLayout.addRow("Contributors:", self.contributorsList)
- vLayout.addLayout(formLayout)
- vLayout.addStretch(1)
+ vLayout.addLayout(formLayout)
+ vLayout.addStretch(1)
- self.buttonBox = qt.QDialogButtonBox()
- self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok |
- qt.QDialogButtonBox.Cancel)
- vLayout.addWidget(self.buttonBox)
+ self.buttonBox = qt.QDialogButtonBox()
+ self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Ok |
+ qt.QDialogButtonBox.Cancel)
+ vLayout.addWidget(self.buttonBox)
# =============================================================================
@@ -56,64 +56,64 @@ def __init__(self, parent):
#
# =============================================================================
class EditExtensionMetadataDialog:
- project = _map_property(lambda self: self.ui.nameEdit, "text")
- category = _map_property(lambda self: self.ui.categoryEdit, "text")
- description = _map_property(lambda self: self.ui.descriptionEdit, "plainText")
-
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- self.dialog = qt.QDialog(parent)
- self.ui = _ui_EditExtensionMetadataDialog(self.dialog)
-
- self.ui.buttonBox.connect("accepted()", self.accept)
- self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
-
- # ---------------------------------------------------------------------------
- def accept(self):
- if not len(self.project):
- slicer.util.errorDisplay("Extension name may not be empty.", windowTitle="Invalid metadata", parent=self.dialog)
- return
-
- if not len(self.description):
- slicer.util.errorDisplay("Extension description may not be empty.",
- windowTitle="Invalid metadata", parent=self.dialog)
- return
-
- self.dialog.accept()
-
- # ---------------------------------------------------------------------------
- def exec_(self):
- return self.dialog.exec_()
-
- # ---------------------------------------------------------------------------
- @property
- def contributors(self):
- result = []
- for row in range(self.ui.contributorsList.itemCount):
- item = self.ui.contributorsList.topLevelItem(row)
- name = item.text(0)
- organization = item.text(1)
- if len(organization):
- result.append(f"{name} ({organization})")
- else:
- result.append(name)
- return ", ".join(result)
-
- # ---------------------------------------------------------------------------
- @contributors.setter
- def contributors(self, value):
- self.ui.contributorsList.clear()
- for c in re.split(r"(?<=[)])\s*,", value):
- c = c.strip()
- item = qt.QTreeWidgetItem()
-
- try:
- n = c.index("(")
- item.setText(0, c[:n].strip())
- item.setText(1, c[n + 1:-1].strip())
-
- except ValueError:
- qt.qWarning("%r: badly formatted contributor" % c)
- item.setText(0, c)
-
- self.ui.contributorsList.addItem(item)
+ project = _map_property(lambda self: self.ui.nameEdit, "text")
+ category = _map_property(lambda self: self.ui.categoryEdit, "text")
+ description = _map_property(lambda self: self.ui.descriptionEdit, "plainText")
+
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ self.dialog = qt.QDialog(parent)
+ self.ui = _ui_EditExtensionMetadataDialog(self.dialog)
+
+ self.ui.buttonBox.connect("accepted()", self.accept)
+ self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
+
+ # ---------------------------------------------------------------------------
+ def accept(self):
+ if not len(self.project):
+ slicer.util.errorDisplay("Extension name may not be empty.", windowTitle="Invalid metadata", parent=self.dialog)
+ return
+
+ if not len(self.description):
+ slicer.util.errorDisplay("Extension description may not be empty.",
+ windowTitle="Invalid metadata", parent=self.dialog)
+ return
+
+ self.dialog.accept()
+
+ # ---------------------------------------------------------------------------
+ def exec_(self):
+ return self.dialog.exec_()
+
+ # ---------------------------------------------------------------------------
+ @property
+ def contributors(self):
+ result = []
+ for row in range(self.ui.contributorsList.itemCount):
+ item = self.ui.contributorsList.topLevelItem(row)
+ name = item.text(0)
+ organization = item.text(1)
+ if len(organization):
+ result.append(f"{name} ({organization})")
+ else:
+ result.append(name)
+ return ", ".join(result)
+
+ # ---------------------------------------------------------------------------
+ @contributors.setter
+ def contributors(self, value):
+ self.ui.contributorsList.clear()
+ for c in re.split(r"(?<=[)])\s*,", value):
+ c = c.strip()
+ item = qt.QTreeWidgetItem()
+
+ try:
+ n = c.index("(")
+ item.setText(0, c[:n].strip())
+ item.setText(1, c[n + 1:-1].strip())
+
+ except ValueError:
+ qt.qWarning("%r: badly formatted contributor" % c)
+ item.setText(0, c)
+
+ self.ui.contributorsList.addItem(item)
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py
index 6491f4e4aa9..aa0a8615af6 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/EditableTreeWidget.py
@@ -3,34 +3,34 @@
# -----------------------------------------------------------------------------
def _makeAction(parent, text, icon=None, shortcut=None, slot=None):
- action = qt.QAction(text, parent)
+ action = qt.QAction(text, parent)
- if icon is not None:
- action.setIcon(qt.QIcon.fromTheme(icon))
+ if icon is not None:
+ action.setIcon(qt.QIcon.fromTheme(icon))
- if shortcut is not None:
- action.shortcut = qt.QKeySequence.fromString(shortcut)
- action.shortcutContext = qt.Qt.WidgetWithChildrenShortcut
+ if shortcut is not None:
+ action.shortcut = qt.QKeySequence.fromString(shortcut)
+ action.shortcutContext = qt.Qt.WidgetWithChildrenShortcut
- if slot is not None:
- action.connect('triggered(bool)', slot)
+ if slot is not None:
+ action.connect('triggered(bool)', slot)
- parent.addAction(action)
+ parent.addAction(action)
- return action
+ return action
# -----------------------------------------------------------------------------
def _newItemPlaceholderItem(parent):
- palette = parent.palette
- color = qt.QColor(palette.text().color())
- color.setAlphaF(0.5)
+ palette = parent.palette
+ color = qt.QColor(palette.text().color())
+ color.setAlphaF(0.5)
- item = qt.QTreeWidgetItem()
- item.setText(0, "(New item)")
- item.setForeground(0, qt.QBrush(color))
+ item = qt.QTreeWidgetItem()
+ item.setText(0, "(New item)")
+ item.setForeground(0, qt.QBrush(color))
- return item
+ return item
# =============================================================================
@@ -39,122 +39,122 @@ def _newItemPlaceholderItem(parent):
#
# =============================================================================
class EditableTreeWidget(qt.QTreeWidget):
- # ---------------------------------------------------------------------------
- def __init__(self, *args, **kwargs):
- qt.QTreeWidget.__init__(self, *args, **kwargs)
-
- # Create initial placeholder item
- self._items = []
- self.addItem(_newItemPlaceholderItem(self), placeholder=True)
-
- # Set up context menu
- self._shiftUpAction = _makeAction(self, text="Move &Up",
- icon="arrow-up",
- shortcut="ctrl+shift+up",
- slot=self.shiftSelectionUp)
-
- self._shiftDownAction = _makeAction(self, text="Move &Down",
- icon="arrow-down",
- shortcut="ctrl+shift+down",
- slot=self.shiftSelectionDown)
-
- self._deleteAction = _makeAction(self, text="&Delete", icon="edit-delete",
- shortcut="del", slot=self.deleteSelection)
-
- self.contextMenuPolicy = qt.Qt.ActionsContextMenu
-
- # Connect internal slots
- self.connect('itemChanged(QTreeWidgetItem*,int)', self.updateItemData)
- self.connect('itemSelectionChanged()', self.updateActions)
-
- # ---------------------------------------------------------------------------
- def addItem(self, item, placeholder=False):
- item.setFlags(item.flags() | qt.Qt.ItemIsEditable)
-
- if placeholder:
- self._items.append(item)
- self.addTopLevelItem(item)
- else:
- pos = len(self._items) - 1
- self._items.insert(pos, item)
- self.insertTopLevelItem(pos, item)
-
- # ---------------------------------------------------------------------------
- def clear(self):
- # Delete all but placeholder item
- while len(self._items) > 1:
- del self._items[0]
-
- # ---------------------------------------------------------------------------
- @property
- def itemCount(self):
- return self.topLevelItemCount - 1
-
- # ---------------------------------------------------------------------------
- def selectedRows(self):
- placeholder = self._items[-1]
- items = self.selectedItems()
- return [self.indexOfTopLevelItem(i) for i in items if i is not placeholder]
-
- # ---------------------------------------------------------------------------
- def setSelectedRows(self, rows):
- sm = self.selectionModel()
- sm.clear()
-
- for item in (self.topLevelItem(row) for row in rows):
- item.setSelected(True)
-
- # ---------------------------------------------------------------------------
- def updateActions(self):
- placeholder = self._items[-1]
-
- items = self.selectedItems()
- rows = self.selectedRows()
-
- last = self.topLevelItemCount - 2
-
- self._shiftUpAction.enabled = len(rows) and not 0 in rows
- self._shiftDownAction.enabled = len(rows) and not last in rows
- self._deleteAction.enabled = True if len(rows) else False
-
- # ---------------------------------------------------------------------------
- def updateItemData(self, item, column):
- # Create new placeholder item if edited item is current placeholder
- if item is self._items[-1]:
- self.addItem(_newItemPlaceholderItem(self), placeholder=True)
-
- # Remove placeholder effect from new item
- item.setData(0, qt.Qt.ForegroundRole, None)
- if column != 0:
- item.setText(0, "Anonymous")
-
- # Update actions so new item can be moved/deleted
- self.updateActions()
-
- # ---------------------------------------------------------------------------
- def shiftSelection(self, delta):
- current = self.currentItem()
-
- rows = sorted(self.selectedRows())
- for row in rows if delta < 0 else reversed(rows):
- item = self.takeTopLevelItem(row)
- self._items.pop(row)
- self._items.insert(row + delta, item)
- self.insertTopLevelItem(row + delta, item)
-
- self.setSelectedRows(row + delta for row in rows)
- self.setCurrentItem(current)
-
- # ---------------------------------------------------------------------------
- def shiftSelectionUp(self):
- self.shiftSelection(-1)
-
- # ---------------------------------------------------------------------------
- def shiftSelectionDown(self):
- self.shiftSelection(+1)
-
- # ---------------------------------------------------------------------------
- def deleteSelection(self):
- rows = self.selectedRows()
- for row in reversed(sorted(rows)):
- del self._items[row]
+ # ---------------------------------------------------------------------------
+ def __init__(self, *args, **kwargs):
+ qt.QTreeWidget.__init__(self, *args, **kwargs)
+
+ # Create initial placeholder item
+ self._items = []
+ self.addItem(_newItemPlaceholderItem(self), placeholder=True)
+
+ # Set up context menu
+ self._shiftUpAction = _makeAction(self, text="Move &Up",
+ icon="arrow-up",
+ shortcut="ctrl+shift+up",
+ slot=self.shiftSelectionUp)
+
+ self._shiftDownAction = _makeAction(self, text="Move &Down",
+ icon="arrow-down",
+ shortcut="ctrl+shift+down",
+ slot=self.shiftSelectionDown)
+
+ self._deleteAction = _makeAction(self, text="&Delete", icon="edit-delete",
+ shortcut="del", slot=self.deleteSelection)
+
+ self.contextMenuPolicy = qt.Qt.ActionsContextMenu
+
+ # Connect internal slots
+ self.connect('itemChanged(QTreeWidgetItem*,int)', self.updateItemData)
+ self.connect('itemSelectionChanged()', self.updateActions)
+
+ # ---------------------------------------------------------------------------
+ def addItem(self, item, placeholder=False):
+ item.setFlags(item.flags() | qt.Qt.ItemIsEditable)
+
+ if placeholder:
+ self._items.append(item)
+ self.addTopLevelItem(item)
+ else:
+ pos = len(self._items) - 1
+ self._items.insert(pos, item)
+ self.insertTopLevelItem(pos, item)
+
+ # ---------------------------------------------------------------------------
+ def clear(self):
+ # Delete all but placeholder item
+ while len(self._items) > 1:
+ del self._items[0]
+
+ # ---------------------------------------------------------------------------
+ @property
+ def itemCount(self):
+ return self.topLevelItemCount - 1
+
+ # ---------------------------------------------------------------------------
+ def selectedRows(self):
+ placeholder = self._items[-1]
+ items = self.selectedItems()
+ return [self.indexOfTopLevelItem(i) for i in items if i is not placeholder]
+
+ # ---------------------------------------------------------------------------
+ def setSelectedRows(self, rows):
+ sm = self.selectionModel()
+ sm.clear()
+
+ for item in (self.topLevelItem(row) for row in rows):
+ item.setSelected(True)
+
+ # ---------------------------------------------------------------------------
+ def updateActions(self):
+ placeholder = self._items[-1]
+
+ items = self.selectedItems()
+ rows = self.selectedRows()
+
+ last = self.topLevelItemCount - 2
+
+ self._shiftUpAction.enabled = len(rows) and not 0 in rows
+ self._shiftDownAction.enabled = len(rows) and not last in rows
+ self._deleteAction.enabled = True if len(rows) else False
+
+ # ---------------------------------------------------------------------------
+ def updateItemData(self, item, column):
+ # Create new placeholder item if edited item is current placeholder
+ if item is self._items[-1]:
+ self.addItem(_newItemPlaceholderItem(self), placeholder=True)
+
+ # Remove placeholder effect from new item
+ item.setData(0, qt.Qt.ForegroundRole, None)
+ if column != 0:
+ item.setText(0, "Anonymous")
+
+ # Update actions so new item can be moved/deleted
+ self.updateActions()
+
+ # ---------------------------------------------------------------------------
+ def shiftSelection(self, delta):
+ current = self.currentItem()
+
+ rows = sorted(self.selectedRows())
+ for row in rows if delta < 0 else reversed(rows):
+ item = self.takeTopLevelItem(row)
+ self._items.pop(row)
+ self._items.insert(row + delta, item)
+ self.insertTopLevelItem(row + delta, item)
+
+ self.setSelectedRows(row + delta for row in rows)
+ self.setCurrentItem(current)
+
+ # ---------------------------------------------------------------------------
+ def shiftSelectionUp(self):
+ self.shiftSelection(-1)
+
+ # ---------------------------------------------------------------------------
+ def shiftSelectionDown(self):
+ self.shiftSelection(+1)
+
+ # ---------------------------------------------------------------------------
+ def deleteSelection(self):
+ rows = self.selectedRows()
+ for row in reversed(sorted(rows)):
+ del self._items[row]
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py
index d31ae2a3e95..d9498691dcc 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/LoadModulesDialog.py
@@ -5,9 +5,9 @@
# -----------------------------------------------------------------------------
def _dialogIcon(icon):
- s = slicer.app.style()
- i = s.standardIcon(icon)
- return i.pixmap(qt.QSize(64, 64))
+ s = slicer.app.style()
+ i = s.standardIcon(icon)
+ return i.pixmap(qt.QSize(64, 64))
# =============================================================================
@@ -16,39 +16,39 @@ def _dialogIcon(icon):
#
# =============================================================================
class _ui_LoadModulesDialog:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- vLayout = qt.QVBoxLayout(parent)
- hLayout = qt.QHBoxLayout()
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ vLayout = qt.QVBoxLayout(parent)
+ hLayout = qt.QHBoxLayout()
- self.icon = qt.QLabel()
- self.icon.setPixmap(_dialogIcon(qt.QStyle.SP_MessageBoxQuestion))
- hLayout.addWidget(self.icon, 0)
+ self.icon = qt.QLabel()
+ self.icon.setPixmap(_dialogIcon(qt.QStyle.SP_MessageBoxQuestion))
+ hLayout.addWidget(self.icon, 0)
- self.label = qt.QLabel()
- self.label.wordWrap = True
- hLayout.addWidget(self.label, 1)
+ self.label = qt.QLabel()
+ self.label.wordWrap = True
+ hLayout.addWidget(self.label, 1)
- vLayout.addLayout(hLayout)
+ vLayout.addLayout(hLayout)
- self.moduleList = qt.QListWidget()
- self.moduleList.selectionMode = qt.QAbstractItemView.NoSelection
- vLayout.addWidget(self.moduleList)
+ self.moduleList = qt.QListWidget()
+ self.moduleList.selectionMode = qt.QAbstractItemView.NoSelection
+ vLayout.addWidget(self.moduleList)
- self.addToSearchPaths = qt.QCheckBox()
- vLayout.addWidget(self.addToSearchPaths)
- self.addToSearchPaths.checked = True
+ self.addToSearchPaths = qt.QCheckBox()
+ vLayout.addWidget(self.addToSearchPaths)
+ self.addToSearchPaths.checked = True
- self.enableDeveloperMode = qt.QCheckBox()
- self.enableDeveloperMode.text = "Enable developer mode"
- self.enableDeveloperMode.toolTip = "Sets the 'Developer mode' application option to enabled. Enabling developer mode is recommended while developing scripted modules, as it makes the Reload and Testing section displayed in the module user interface."
- self.enableDeveloperMode.checked = True
- vLayout.addWidget(self.enableDeveloperMode)
+ self.enableDeveloperMode = qt.QCheckBox()
+ self.enableDeveloperMode.text = "Enable developer mode"
+ self.enableDeveloperMode.toolTip = "Sets the 'Developer mode' application option to enabled. Enabling developer mode is recommended while developing scripted modules, as it makes the Reload and Testing section displayed in the module user interface."
+ self.enableDeveloperMode.checked = True
+ vLayout.addWidget(self.enableDeveloperMode)
- self.buttonBox = qt.QDialogButtonBox()
- self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Yes |
- qt.QDialogButtonBox.No)
- vLayout.addWidget(self.buttonBox)
+ self.buttonBox = qt.QDialogButtonBox()
+ self.buttonBox.setStandardButtons(qt.QDialogButtonBox.Yes |
+ qt.QDialogButtonBox.No)
+ vLayout.addWidget(self.buttonBox)
# =============================================================================
@@ -57,88 +57,88 @@ def __init__(self, parent):
#
# =============================================================================
class LoadModulesDialog:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- self.dialog = qt.QDialog(parent)
- self.ui = _ui_LoadModulesDialog(self.dialog)
-
- self.ui.buttonBox.connect("accepted()", self.dialog, "accept()")
- self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
- self.ui.moduleList.connect("itemChanged(QListWidgetItem*)", self.validate)
-
- # ---------------------------------------------------------------------------
- def validate(self):
- moduleCount = len(self.selectedModules)
-
- if moduleCount == 0:
- self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = False
- self.ui.addToSearchPaths.enabled = False
-
- moduleCount = len(self._moduleItems)
-
- else:
- self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = True
- self.ui.addToSearchPaths.enabled = True
-
- if moduleCount == 1:
- self.ui.addToSearchPaths.text = "Add selected module to search paths"
- else:
- self.ui.addToSearchPaths.text = "Add selected modules to search paths"
-
- # If developer mode is already enabled then don't even show the option
- developerModeAlreadyEnabled = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool)
- if developerModeAlreadyEnabled:
- self.ui.enableDeveloperMode.visible = False
- self.ui.enableDeveloperMode.checked = False
-
- # ---------------------------------------------------------------------------
- def exec_(self):
- return self.dialog.exec_()
-
- # ---------------------------------------------------------------------------
- def setModules(self, modules):
- self.ui.moduleList.clear()
- self._moduleItems = {}
-
- for module in modules:
- item = qt.QListWidgetItem(module.key)
- item.setFlags(item.flags() | qt.Qt.ItemIsUserCheckable)
- item.setCheckState(qt.Qt.Checked)
- self.ui.moduleList.addItem(item)
- self._moduleItems[item] = module
-
- if len(modules) > 1:
- self.ui.label.text = (
- "The following modules can be loaded. "
- "Would you like to load them now?")
-
- elif len(modules) == 1:
- self.ui.label.text = (
- "The following module can be loaded. "
- "Would you like to load it now?")
-
- else:
- raise ValueError("At least one module must be provided")
-
- self.validate()
-
- # ---------------------------------------------------------------------------
- @property
- def addToSearchPaths(self):
- return self.ui.addToSearchPaths.checked
-
- # ---------------------------------------------------------------------------
- @property
- def enableDeveloperMode(self):
- return self.ui.enableDeveloperMode.checked
-
- # ---------------------------------------------------------------------------
- @property
- def selectedModules(self):
- result = []
-
- for item, module in self._moduleItems.items():
- if item.checkState():
- result.append(module)
-
- return result
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ self.dialog = qt.QDialog(parent)
+ self.ui = _ui_LoadModulesDialog(self.dialog)
+
+ self.ui.buttonBox.connect("accepted()", self.dialog, "accept()")
+ self.ui.buttonBox.connect("rejected()", self.dialog, "reject()")
+ self.ui.moduleList.connect("itemChanged(QListWidgetItem*)", self.validate)
+
+ # ---------------------------------------------------------------------------
+ def validate(self):
+ moduleCount = len(self.selectedModules)
+
+ if moduleCount == 0:
+ self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = False
+ self.ui.addToSearchPaths.enabled = False
+
+ moduleCount = len(self._moduleItems)
+
+ else:
+ self.ui.buttonBox.button(qt.QDialogButtonBox.Yes).enabled = True
+ self.ui.addToSearchPaths.enabled = True
+
+ if moduleCount == 1:
+ self.ui.addToSearchPaths.text = "Add selected module to search paths"
+ else:
+ self.ui.addToSearchPaths.text = "Add selected modules to search paths"
+
+ # If developer mode is already enabled then don't even show the option
+ developerModeAlreadyEnabled = slicer.util.settingsValue('Developer/DeveloperMode', False, converter=slicer.util.toBool)
+ if developerModeAlreadyEnabled:
+ self.ui.enableDeveloperMode.visible = False
+ self.ui.enableDeveloperMode.checked = False
+
+ # ---------------------------------------------------------------------------
+ def exec_(self):
+ return self.dialog.exec_()
+
+ # ---------------------------------------------------------------------------
+ def setModules(self, modules):
+ self.ui.moduleList.clear()
+ self._moduleItems = {}
+
+ for module in modules:
+ item = qt.QListWidgetItem(module.key)
+ item.setFlags(item.flags() | qt.Qt.ItemIsUserCheckable)
+ item.setCheckState(qt.Qt.Checked)
+ self.ui.moduleList.addItem(item)
+ self._moduleItems[item] = module
+
+ if len(modules) > 1:
+ self.ui.label.text = (
+ "The following modules can be loaded. "
+ "Would you like to load them now?")
+
+ elif len(modules) == 1:
+ self.ui.label.text = (
+ "The following module can be loaded. "
+ "Would you like to load it now?")
+
+ else:
+ raise ValueError("At least one module must be provided")
+
+ self.validate()
+
+ # ---------------------------------------------------------------------------
+ @property
+ def addToSearchPaths(self):
+ return self.ui.addToSearchPaths.checked
+
+ # ---------------------------------------------------------------------------
+ @property
+ def enableDeveloperMode(self):
+ return self.ui.enableDeveloperMode.checked
+
+ # ---------------------------------------------------------------------------
+ @property
+ def selectedModules(self):
+ result = []
+
+ for item, module in self._moduleItems.items():
+ if item.checkState():
+ result.append(module)
+
+ return result
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py
index d7b0fe15945..6f67ef67509 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/ModuleInfo.py
@@ -12,38 +12,38 @@
#
# =============================================================================
class ModuleInfo:
- # ---------------------------------------------------------------------------
- def __init__(self, path, key=None):
- self.path = path
- self.searchPath = os.path.dirname(path)
-
- if key is None:
- self.key = os.path.splitext(os.path.basename(path))[0]
- else:
- self.key = key
-
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return "ModuleInfo(key=%(key)r, path=%(path)r)" % self.__dict__
-
- # ---------------------------------------------------------------------------
- def __str__(self):
- return self.path
-
- # ---------------------------------------------------------------------------
- @staticmethod
- def findModules(path, depth):
- result = []
- entries = [os.path.join(path, entry) for entry in os.listdir(path)]
-
- if depth > 0:
- for entry in filter(os.path.isdir, entries):
- result += ModuleInfo.findModules(entry, depth - 1)
-
- for entry in filter(os.path.isfile, entries):
- # __init__.py is not a module but an embedded Python library
- # that a module will load.
- if entry.endswith(".py") and not entry.endswith("__init__.py"):
- result.append(ModuleInfo(entry))
-
- return result
+ # ---------------------------------------------------------------------------
+ def __init__(self, path, key=None):
+ self.path = path
+ self.searchPath = os.path.dirname(path)
+
+ if key is None:
+ self.key = os.path.splitext(os.path.basename(path))[0]
+ else:
+ self.key = key
+
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "ModuleInfo(key=%(key)r, path=%(path)r)" % self.__dict__
+
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return self.path
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def findModules(path, depth):
+ result = []
+ entries = [os.path.join(path, entry) for entry in os.listdir(path)]
+
+ if depth > 0:
+ for entry in filter(os.path.isdir, entries):
+ result += ModuleInfo.findModules(entry, depth - 1)
+
+ for entry in filter(os.path.isfile, entries):
+ # __init__.py is not a module but an embedded Python library
+ # that a module will load.
+ if entry.endswith(".py") and not entry.endswith("__init__.py"):
+ result.append(ModuleInfo(entry))
+
+ return result
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py
index cf065dcda88..be8768d249b 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/SettingsPanel.py
@@ -13,35 +13,35 @@
#
# =============================================================================
class _ui_SettingsPanel:
- # ---------------------------------------------------------------------------
- def __init__(self, parent):
- self.formLayout = qt.QFormLayout(parent)
+ # ---------------------------------------------------------------------------
+ def __init__(self, parent):
+ self.formLayout = qt.QFormLayout(parent)
- self.builtinPath = qt.QLineEdit()
- builtinPath = builtinTemplatePath()
- if (builtinPath):
- self.builtinPath.text = builtinPath
- else:
- self.builtinPath.text = "(Unavailable)"
- self.builtinPath.enabled = False
- self.builtinPath.readOnly = True
- self.addRow("Built-in template path:", self.builtinPath)
+ self.builtinPath = qt.QLineEdit()
+ builtinPath = builtinTemplatePath()
+ if (builtinPath):
+ self.builtinPath.text = builtinPath
+ else:
+ self.builtinPath.text = "(Unavailable)"
+ self.builtinPath.enabled = False
+ self.builtinPath.readOnly = True
+ self.addRow("Built-in template path:", self.builtinPath)
- self.genericPaths = DirectoryListWidget()
- self.addRow("Additional template\npaths:", self.genericPaths)
+ self.genericPaths = DirectoryListWidget()
+ self.addRow("Additional template\npaths:", self.genericPaths)
- self.paths = {}
+ self.paths = {}
- for category in SlicerWizard.TemplateManager.categories():
- self.paths[category] = DirectoryListWidget()
- self.addRow("Additional template\npaths for %s:" % category,
- self.paths[category])
+ for category in SlicerWizard.TemplateManager.categories():
+ self.paths[category] = DirectoryListWidget()
+ self.addRow("Additional template\npaths for %s:" % category,
+ self.paths[category])
- # ---------------------------------------------------------------------------
- def addRow(self, label, widget):
- self.formLayout.addRow(label, widget)
- label = self.formLayout.labelForField(widget)
- label.alignment = self.formLayout.labelAlignment
+ # ---------------------------------------------------------------------------
+ def addRow(self, label, widget):
+ self.formLayout.addRow(label, widget)
+ label = self.formLayout.labelForField(widget)
+ label.alignment = self.formLayout.labelAlignment
# =============================================================================
@@ -50,19 +50,19 @@ def addRow(self, label, widget):
#
# =============================================================================
class SettingsPanel(ctk.ctkSettingsPanel):
- # ---------------------------------------------------------------------------
- def __init__(self, *args, **kwargs):
- ctk.ctkSettingsPanel.__init__(self, *args, **kwargs)
- self.ui = _ui_SettingsPanel(self)
+ # ---------------------------------------------------------------------------
+ def __init__(self, *args, **kwargs):
+ ctk.ctkSettingsPanel.__init__(self, *args, **kwargs)
+ self.ui = _ui_SettingsPanel(self)
- self.registerProperty(
- userTemplatePathKey(), self.ui.genericPaths.ui.pathList,
- "directoryList", str(qt.SIGNAL("directoryListChanged()")),
- "Additional template paths", ctk.ctkSettingsPanel.OptionRequireRestart)
+ self.registerProperty(
+ userTemplatePathKey(), self.ui.genericPaths.ui.pathList,
+ "directoryList", str(qt.SIGNAL("directoryListChanged()")),
+ "Additional template paths", ctk.ctkSettingsPanel.OptionRequireRestart)
- for category in self.ui.paths.keys():
- self.registerProperty(
- userTemplatePathKey(category), self.ui.paths[category].ui.pathList,
- "directoryList", str(qt.SIGNAL("directoryListChanged()")),
- "Additional template paths for %s" % category,
- ctk.ctkSettingsPanel.OptionRequireRestart)
+ for category in self.ui.paths.keys():
+ self.registerProperty(
+ userTemplatePathKey(category), self.ui.paths[category].ui.pathList,
+ "directoryList", str(qt.SIGNAL("directoryListChanged()")),
+ "Additional template paths for %s" % category,
+ ctk.ctkSettingsPanel.OptionRequireRestart)
diff --git a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py
index 9d5d49c62a8..63e0aaae87d 100644
--- a/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py
+++ b/Modules/Scripted/ExtensionWizard/ExtensionWizardLib/TemplatePathUtilities.py
@@ -7,29 +7,29 @@
# -----------------------------------------------------------------------------
def userTemplatePathKey(category=None):
- if category is None:
- return _userTemplatePathKey
- else:
- return f"{_userTemplatePathKey}/{category}"
+ if category is None:
+ return _userTemplatePathKey
+ else:
+ return f"{_userTemplatePathKey}/{category}"
# -----------------------------------------------------------------------------
def builtinTemplatePath():
- # Look for templates in source directory first
- path = slicer.util.sourceDir()
+ # Look for templates in source directory first
+ path = slicer.util.sourceDir()
- if path is not None:
- path = os.path.join(path, "Utilities", "Templates")
+ if path is not None:
+ path = os.path.join(path, "Utilities", "Templates")
- if os.path.exists(path):
- return path
+ if os.path.exists(path):
+ return path
- # Look for installed templates
- path = os.path.join(slicer.app.slicerHome, slicer.app.slicerSharePath,
- "Wizard", "Templates")
+ # Look for installed templates
+ path = os.path.join(slicer.app.slicerHome, slicer.app.slicerSharePath,
+ "Wizard", "Templates")
- if os.path.exists(path):
- return path
+ if os.path.exists(path):
+ return path
- # No templates found
- return None
+ # No templates found
+ return None
diff --git a/Modules/Scripted/PerformanceTests/PerformanceTests.py b/Modules/Scripted/PerformanceTests/PerformanceTests.py
index a93ad575e9e..82f14ee6d4b 100644
--- a/Modules/Scripted/PerformanceTests/PerformanceTests.py
+++ b/Modules/Scripted/PerformanceTests/PerformanceTests.py
@@ -9,19 +9,19 @@
#
class PerformanceTests(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "Performance Tests"
- parent.categories = ["Testing.TestCases"]
- parent.contributors = ["Steve Pieper (Isomics)"]
- parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "Performance Tests"
+ parent.categories = ["Testing.TestCases"]
+ parent.contributors = ["Steve Pieper (Isomics)"]
+ parent.helpText = """
Module to run interactive performance tests on the core of slicer.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This file was based on work originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
and others. This work was partially funded by NIH grant 3P41RR013218-12S1.
"""
- self.parent = parent
+ self.parent = parent
#
@@ -29,212 +29,212 @@ def __init__(self, parent):
#
class PerformanceTestsWidget(ScriptedLoadableModuleWidget):
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
- tests = (
- ('Get Sample Data', self.downloadMRHead),
- ('Reslicing', self.reslicing),
- ('Crosshair Jump', self.crosshairJump),
- ('Web View Test', self.webViewTest),
- ('Fill Out Web Form Test', self.webViewFormTest),
- ('Memory Check', self.memoryCheck),
- )
-
- for test in tests:
- b = qt.QPushButton(test[0])
- self.layout.addWidget(b)
- b.connect('clicked()', test[1])
-
- self.log = qt.QTextEdit()
- self.log.readOnly = True
- self.layout.addWidget(self.log)
- self.log.insertHtml('
Status: Idle\n')
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
-
- # Add spacer to layout
- self.layout.addStretch(1)
-
- def downloadMRHead(self):
- import SampleData
- self.log.insertHtml('Requesting downloading MRHead')
- self.log.repaint()
- mrHeadVolume = SampleData.downloadSample("MRHead")
- if mrHeadVolume:
- self.log.insertHtml('finished.\n')
- self.log.insertPlainText('\n')
- self.log.repaint()
- else:
- self.log.insertHtml('Download failed!\n')
- self.log.insertPlainText('\n')
- self.log.repaint()
- self.log.ensureCursorVisible()
-
- def timeSteps(self, iters, f):
- import time
- elapsedTime = 0
- for i in range(iters):
- startTime = time.time()
- f()
- slicer.app.processEvents()
- endTime = time.time()
- elapsedTime += (endTime - startTime)
- fps = int(iters / elapsedTime)
- result = f"fps = {fps:g} ({1000./fps:g} ms per frame)"
- print(result)
- self.log.insertHtml('%s' % result)
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
- self.log.repaint()
-
- def reslicing(self, iters=100):
- """ go into a loop that stresses the reslice performance
- """
- import time
- import math
- import numpy as np
- sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed')
- dims = sliceNode.GetDimensions()
- elapsedTime = 0
- sliceOffset = 5
- offsetSteps = 10
- numerOfSweeps = int(math.ceil(iters / offsetSteps))
- renderingTimesSec = np.zeros(numerOfSweeps * offsetSteps * 2)
- sampleIndex = 0
- startOffset = sliceNode.GetSliceOffset()
- for i in range(numerOfSweeps):
- for offset in ([sliceOffset] * offsetSteps + [-sliceOffset] * offsetSteps):
- startTime = time.time()
- sliceNode.SetSliceOffset(sliceNode.GetSliceOffset() + offset)
- slicer.app.processEvents()
- endTime = time.time()
- renderingTimesSec[sampleIndex] = (endTime - startTime)
- sampleIndex += 1
- sliceNode.SetSliceOffset(startOffset)
-
- resultTableName = slicer.mrmlScene.GetUniqueNameByString("Reslice performance")
- resultTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", resultTableName)
- slicer.util.updateTableFromArray(resultTableNode, renderingTimesSec, "Rendering time [s]")
-
- renderingTimeMean = np.mean(renderingTimesSec)
- renderingTimeStd = np.std(renderingTimesSec)
- result = ("%d x %d, fps = %.1f (%.1f +/- %.2f ms per frame) - see details in table '%s'"
- % (dims[0], dims[1], 1.0 / renderingTimeMean, 1000. * renderingTimeMean, 1000. * renderingTimeStd, resultTableNode.GetName()))
- print(result)
- self.log.insertHtml('%s' % result)
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
- self.log.repaint()
-
- def crosshairJump(self, iters=15):
- """ go into a loop that stresses jumping to slices by moving crosshair
- """
- import time
- sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed')
- dims = sliceNode.GetDimensions()
- layoutManager = slicer.app.layoutManager()
- sliceViewNames = layoutManager.sliceViewNames()
- # Order of slice view names is random, prefer 'Red' slice to make results more predictable
- firstSliceViewName = 'Red' if 'Red' in sliceViewNames else sliceViewNames[0]
- firstSliceWidget = layoutManager.sliceWidget(firstSliceViewName)
- elapsedTime = 0
- startPoint = (int(dims[0] * 0.3), int(dims[1] * 0.3))
- endPoint = (int(dims[0] * 0.6), int(dims[1] * 0.6))
- for i in range(iters):
- startTime = time.time()
- slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=startPoint, end=endPoint, steps=2)
- slicer.app.processEvents()
- endTime1 = time.time()
- slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=endPoint, end=startPoint, steps=2)
- slicer.app.processEvents()
- endTime2 = time.time()
- delta = ((endTime1 - startTime) + (endTime2 - endTime1)) / 2.
- elapsedTime += delta
- fps = int(iters / elapsedTime)
- result = "number of slice views = %d, fps = %g (%g ms per frame)" % (len(sliceViewNames), fps, 1000. / fps)
- print(result)
- self.log.insertHtml('%s' % result)
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
- self.log.repaint()
-
- def webViewCallback(self, qurl):
- url = qurl.toString()
- print(url)
- if url == 'reslicing':
- self.reslicing()
- if url == 'chart':
- self.chartTest()
- pass
-
- def webViewTest(self):
- self.webView = qt.QWebView()
- html = """
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+ tests = (
+ ('Get Sample Data', self.downloadMRHead),
+ ('Reslicing', self.reslicing),
+ ('Crosshair Jump', self.crosshairJump),
+ ('Web View Test', self.webViewTest),
+ ('Fill Out Web Form Test', self.webViewFormTest),
+ ('Memory Check', self.memoryCheck),
+ )
+
+ for test in tests:
+ b = qt.QPushButton(test[0])
+ self.layout.addWidget(b)
+ b.connect('clicked()', test[1])
+
+ self.log = qt.QTextEdit()
+ self.log.readOnly = True
+ self.layout.addWidget(self.log)
+ self.log.insertHtml('
Status: Idle\n')
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+
+ # Add spacer to layout
+ self.layout.addStretch(1)
+
+ def downloadMRHead(self):
+ import SampleData
+ self.log.insertHtml('Requesting downloading MRHead')
+ self.log.repaint()
+ mrHeadVolume = SampleData.downloadSample("MRHead")
+ if mrHeadVolume:
+ self.log.insertHtml('finished.\n')
+ self.log.insertPlainText('\n')
+ self.log.repaint()
+ else:
+ self.log.insertHtml('Download failed!\n')
+ self.log.insertPlainText('\n')
+ self.log.repaint()
+ self.log.ensureCursorVisible()
+
+ def timeSteps(self, iters, f):
+ import time
+ elapsedTime = 0
+ for i in range(iters):
+ startTime = time.time()
+ f()
+ slicer.app.processEvents()
+ endTime = time.time()
+ elapsedTime += (endTime - startTime)
+ fps = int(iters / elapsedTime)
+ result = f"fps = {fps:g} ({1000./fps:g} ms per frame)"
+ print(result)
+ self.log.insertHtml('%s' % result)
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+ self.log.repaint()
+
+ def reslicing(self, iters=100):
+ """ go into a loop that stresses the reslice performance
+ """
+ import time
+ import math
+ import numpy as np
+ sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed')
+ dims = sliceNode.GetDimensions()
+ elapsedTime = 0
+ sliceOffset = 5
+ offsetSteps = 10
+ numerOfSweeps = int(math.ceil(iters / offsetSteps))
+ renderingTimesSec = np.zeros(numerOfSweeps * offsetSteps * 2)
+ sampleIndex = 0
+ startOffset = sliceNode.GetSliceOffset()
+ for i in range(numerOfSweeps):
+ for offset in ([sliceOffset] * offsetSteps + [-sliceOffset] * offsetSteps):
+ startTime = time.time()
+ sliceNode.SetSliceOffset(sliceNode.GetSliceOffset() + offset)
+ slicer.app.processEvents()
+ endTime = time.time()
+ renderingTimesSec[sampleIndex] = (endTime - startTime)
+ sampleIndex += 1
+ sliceNode.SetSliceOffset(startOffset)
+
+ resultTableName = slicer.mrmlScene.GetUniqueNameByString("Reslice performance")
+ resultTableNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode", resultTableName)
+ slicer.util.updateTableFromArray(resultTableNode, renderingTimesSec, "Rendering time [s]")
+
+ renderingTimeMean = np.mean(renderingTimesSec)
+ renderingTimeStd = np.std(renderingTimesSec)
+ result = ("%d x %d, fps = %.1f (%.1f +/- %.2f ms per frame) - see details in table '%s'"
+ % (dims[0], dims[1], 1.0 / renderingTimeMean, 1000. * renderingTimeMean, 1000. * renderingTimeStd, resultTableNode.GetName()))
+ print(result)
+ self.log.insertHtml('%s' % result)
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+ self.log.repaint()
+
+ def crosshairJump(self, iters=15):
+ """ go into a loop that stresses jumping to slices by moving crosshair
+ """
+ import time
+ sliceNode = slicer.util.getNode('vtkMRMLSliceNodeRed')
+ dims = sliceNode.GetDimensions()
+ layoutManager = slicer.app.layoutManager()
+ sliceViewNames = layoutManager.sliceViewNames()
+ # Order of slice view names is random, prefer 'Red' slice to make results more predictable
+ firstSliceViewName = 'Red' if 'Red' in sliceViewNames else sliceViewNames[0]
+ firstSliceWidget = layoutManager.sliceWidget(firstSliceViewName)
+ elapsedTime = 0
+ startPoint = (int(dims[0] * 0.3), int(dims[1] * 0.3))
+ endPoint = (int(dims[0] * 0.6), int(dims[1] * 0.6))
+ for i in range(iters):
+ startTime = time.time()
+ slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=startPoint, end=endPoint, steps=2)
+ slicer.app.processEvents()
+ endTime1 = time.time()
+ slicer.util.clickAndDrag(firstSliceWidget, button=None, modifiers=['Shift'], start=endPoint, end=startPoint, steps=2)
+ slicer.app.processEvents()
+ endTime2 = time.time()
+ delta = ((endTime1 - startTime) + (endTime2 - endTime1)) / 2.
+ elapsedTime += delta
+ fps = int(iters / elapsedTime)
+ result = "number of slice views = %d, fps = %g (%g ms per frame)" % (len(sliceViewNames), fps, 1000. / fps)
+ print(result)
+ self.log.insertHtml('%s' % result)
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+ self.log.repaint()
+
+ def webViewCallback(self, qurl):
+ url = qurl.toString()
+ print(url)
+ if url == 'reslicing':
+ self.reslicing()
+ if url == 'chart':
+ self.chartTest()
+ pass
+
+ def webViewTest(self):
+ self.webView = qt.QWebView()
+ html = """
Run reslicing test
Run chart test
"""
- self.webView.setHtml(html)
- self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True)
- self.webView.page().setLinkDelegationPolicy(qt.QWebPage.DelegateAllLinks)
- self.webView.connect('linkClicked(QUrl)', self.webViewCallback)
- self.webView.show()
-
- def webViewFormTest(self):
- """Just as a demo, load a google search in a web view
- and use the qt api to fill in a search term"""
- self.webView = qt.QWebView()
- self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True)
- self.webView.connect('loadFinished(bool)', self.webViewFormLoadedCallback)
- self.webView.show()
- u = qt.QUrl('https://www.google.com')
- self.webView.setUrl(u)
-
- def webViewFormLoadedCallback(self, ok):
- if not ok:
- print('page did not load')
- return
- page = self.webView.page()
- frame = page.mainFrame()
- document = frame.documentElement()
- element = document.findFirst('.lst')
- element.setAttribute("value", "where can I learn more about this 3D Slicer program?")
-
- def memoryCallback(self):
- if self.sysInfoWindow.visible:
- self.sysInfo.RunMemoryCheck()
- self.sysInfoWindow.append('p: %d of %d, v: %d of %d' %
- (self.sysInfo.GetAvailablePhysicalMemory(),
- self.sysInfo.GetTotalPhysicalMemory(),
- self.sysInfo.GetAvailableVirtualMemory(),
- self.sysInfo.GetTotalVirtualMemory(),
- ))
- qt.QTimer.singleShot(1000, self.memoryCallback)
-
- def memoryCheck(self):
- """Run a periodic memory check in a window"""
- if not hasattr(self, 'sysInfo'):
- self.sysInfo = slicer.vtkSystemInformation()
- self.sysInfoWindow = qt.QTextBrowser()
- if self.sysInfoWindow.visible:
- return
- self.sysInfoWindow.show()
- self.memoryCallback()
+ self.webView.setHtml(html)
+ self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True)
+ self.webView.page().setLinkDelegationPolicy(qt.QWebPage.DelegateAllLinks)
+ self.webView.connect('linkClicked(QUrl)', self.webViewCallback)
+ self.webView.show()
+
+ def webViewFormTest(self):
+ """Just as a demo, load a google search in a web view
+ and use the qt api to fill in a search term"""
+ self.webView = qt.QWebView()
+ self.webView.settings().setAttribute(qt.QWebSettings.DeveloperExtrasEnabled, True)
+ self.webView.connect('loadFinished(bool)', self.webViewFormLoadedCallback)
+ self.webView.show()
+ u = qt.QUrl('https://www.google.com')
+ self.webView.setUrl(u)
+
+ def webViewFormLoadedCallback(self, ok):
+ if not ok:
+ print('page did not load')
+ return
+ page = self.webView.page()
+ frame = page.mainFrame()
+ document = frame.documentElement()
+ element = document.findFirst('.lst')
+ element.setAttribute("value", "where can I learn more about this 3D Slicer program?")
+
+ def memoryCallback(self):
+ if self.sysInfoWindow.visible:
+ self.sysInfo.RunMemoryCheck()
+ self.sysInfoWindow.append('p: %d of %d, v: %d of %d' %
+ (self.sysInfo.GetAvailablePhysicalMemory(),
+ self.sysInfo.GetTotalPhysicalMemory(),
+ self.sysInfo.GetAvailableVirtualMemory(),
+ self.sysInfo.GetTotalVirtualMemory(),
+ ))
+ qt.QTimer.singleShot(1000, self.memoryCallback)
+
+ def memoryCheck(self):
+ """Run a periodic memory check in a window"""
+ if not hasattr(self, 'sysInfo'):
+ self.sysInfo = slicer.vtkSystemInformation()
+ self.sysInfoWindow = qt.QTextBrowser()
+ if self.sysInfoWindow.visible:
+ return
+ self.sysInfoWindow.show()
+ self.memoryCallback()
class sliceLogicTest:
- def __init__(self):
- self.step = 0
- self.sliceLogic = slicer.vtkMRMLSliceLayerLogic()
- self.sliceLogic.SetMRMLScene(slicer.mrmlScene)
- self.sliceNode = slicer.vtkMRMLSliceNode()
- self.sliceNode.SetLayoutName("Black")
- slicer.mrmlScene.AddNode(self.sliceNode)
- self.sliceLogic.SetSliceNode(self.sliceNode)
-
- def stepSliceLogic(self):
- self.sliceNode.SetSliceOffset(-1 * self.step * 10)
- self.step = 1 ^ self.step
-
- def testSliceLogic(self, iters):
- timeSteps(iters, self.stepSliceLogic)
+ def __init__(self):
+ self.step = 0
+ self.sliceLogic = slicer.vtkMRMLSliceLayerLogic()
+ self.sliceLogic.SetMRMLScene(slicer.mrmlScene)
+ self.sliceNode = slicer.vtkMRMLSliceNode()
+ self.sliceNode.SetLayoutName("Black")
+ slicer.mrmlScene.AddNode(self.sliceNode)
+ self.sliceLogic.SetSliceNode(self.sliceNode)
+
+ def stepSliceLogic(self):
+ self.sliceNode.SetSliceOffset(-1 * self.step * 10)
+ self.step = 1 ^ self.step
+
+ def testSliceLogic(self, iters):
+ timeSteps(iters, self.stepSliceLogic)
diff --git a/Modules/Scripted/SampleData/SampleData.py b/Modules/Scripted/SampleData/SampleData.py
index 4c3903cb61d..53b63eab395 100644
--- a/Modules/Scripted/SampleData/SampleData.py
+++ b/Modules/Scripted/SampleData/SampleData.py
@@ -16,46 +16,46 @@
#
def downloadFromURL(uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None,
- customDownloader=None, loadFileTypes=None, loadFileProperties={}):
- """Download and optionally load data into the application.
+ customDownloader=None, loadFileTypes=None, loadFileProperties={}):
+ """Download and optionally load data into the application.
- :param uris: Download URL(s).
- :param fileNames: File name(s) that will be downloaded (and loaded).
- :param nodeNames: Node name(s) in the scene.
- :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
- :param customDownloader: Custom function for downloading.
- :param loadFileTypes: file format name(s) ('VolumeFile' by default).
- :param loadFileProperties: custom properties passed to the IO plugin.
+ :param uris: Download URL(s).
+ :param fileNames: File name(s) that will be downloaded (and loaded).
+ :param nodeNames: Node name(s) in the scene.
+ :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
+ :param customDownloader: Custom function for downloading.
+ :param loadFileTypes: file format name(s) ('VolumeFile' by default).
+ :param loadFileProperties: custom properties passed to the IO plugin.
- If the given ``fileNames`` are not found in the application cache directory, they
- are downloaded using the associated URIs.
- See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
+ If the given ``fileNames`` are not found in the application cache directory, they
+ are downloaded using the associated URIs.
+ See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
- If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
- guessed based on the corresponding filename extensions.
+ If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
+ guessed based on the corresponding filename extensions.
- If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
- by default. To ensure the file is loaded, ``loadFiles`` must be set.
+ If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
+ by default. To ensure the file is loaded, ``loadFiles`` must be set.
- The ``loadFileProperties`` are common for all files. If different properties
- need to be associated with files of different types, downloadFromURL must
- be called for each.
- """
- return SampleDataLogic().downloadFromURL(
- uris, fileNames, nodeNames, checksums, loadFiles, customDownloader, loadFileTypes, loadFileProperties)
+ The ``loadFileProperties`` are common for all files. If different properties
+ need to be associated with files of different types, downloadFromURL must
+ be called for each.
+ """
+ return SampleDataLogic().downloadFromURL(
+ uris, fileNames, nodeNames, checksums, loadFiles, customDownloader, loadFileTypes, loadFileProperties)
def downloadSample(sampleName):
- """For a given sample name this will search the available sources
- and load it if it is available. Returns the first loaded node."""
- return SampleDataLogic().downloadSamples(sampleName)[0]
+ """For a given sample name this will search the available sources
+ and load it if it is available. Returns the first loaded node."""
+ return SampleDataLogic().downloadSamples(sampleName)[0]
def downloadSamples(sampleName):
- """For a given sample name this will search the available sources
- and load it if it is available. Returns the loaded nodes."""
- return SampleDataLogic().downloadSamples(sampleName)
+ """For a given sample name this will search the available sources
+ and load it if it is available. Returns the loaded nodes."""
+ return SampleDataLogic().downloadSamples(sampleName)
#
@@ -63,21 +63,21 @@ def downloadSamples(sampleName):
#
class SampleData(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Sample Data"
- self.parent.categories = ["Informatics"]
- self.parent.dependencies = []
- self.parent.contributors = ["Steve Pieper (Isomics), Benjamin Long (Kitware), Jean-Christophe Fillion-Robin (Kitware)"]
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Sample Data"
+ self.parent.categories = ["Informatics"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Steve Pieper (Isomics), Benjamin Long (Kitware), Jean-Christophe Fillion-Robin (Kitware)"]
+ self.parent.helpText = """
The SampleData module can be used to download data for working with in slicer. Use of this module requires an active network connection.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This work was was funded by Cancer Care Ontario
and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO)
@@ -91,154 +91,154 @@ def __init__(self, parent):
use it for commercial purposes.
"""
- if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
- slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
+ if slicer.mrmlScene.GetTagByClassName("vtkMRMLScriptedModuleNode") != 'ScriptedModule':
+ slicer.mrmlScene.RegisterNodeClass(vtkMRMLScriptedModuleNode())
- # Trigger the menu to be added when application has started up
- if not slicer.app.commandOptions().noMainWindow:
- slicer.app.connect("startupCompleted()", self.addMenu)
+ # Trigger the menu to be added when application has started up
+ if not slicer.app.commandOptions().noMainWindow:
+ slicer.app.connect("startupCompleted()", self.addMenu)
- # allow other modules to register sample data sources by appending
- # instances or subclasses SampleDataSource objects on this list
- try:
- slicer.modules.sampleDataSources
- except AttributeError:
- slicer.modules.sampleDataSources = {}
+ # allow other modules to register sample data sources by appending
+ # instances or subclasses SampleDataSource objects on this list
+ try:
+ slicer.modules.sampleDataSources
+ except AttributeError:
+ slicer.modules.sampleDataSources = {}
- def addMenu(self):
- a = qt.QAction('Download Sample Data', slicer.util.mainWindow())
- a.setToolTip('Go to the SampleData module to download data from the network')
- a.connect('triggered()', self.select)
+ def addMenu(self):
+ a = qt.QAction('Download Sample Data', slicer.util.mainWindow())
+ a.setToolTip('Go to the SampleData module to download data from the network')
+ a.connect('triggered()', self.select)
- fileMenu = slicer.util.lookupTopLevelWidget('FileMenu')
- if fileMenu:
- for action in fileMenu.actions():
- if action.objectName == "FileSaveSceneAction":
- fileMenu.insertAction(action, a)
- fileMenu.insertSeparator(action)
+ fileMenu = slicer.util.lookupTopLevelWidget('FileMenu')
+ if fileMenu:
+ for action in fileMenu.actions():
+ if action.objectName == "FileSaveSceneAction":
+ fileMenu.insertAction(action, a)
+ fileMenu.insertSeparator(action)
- def select(self):
- m = slicer.util.mainWindow()
- m.moduleSelector().selectModule('SampleData')
+ def select(self):
+ m = slicer.util.mainWindow()
+ m.moduleSelector().selectModule('SampleData')
#
# SampleDataSource
#
class SampleDataSource:
- """Describe a set of sample data associated with one or multiple URIs and filenames.
-
- Example::
-
- import SampleData
- from slicer.util import TESTING_DATA_URL
- dataSource = SampleData.SampleDataSource(
- nodeNames='fixed',
- fileNames='fixed.nrrd',
- uris=TESTING_DATA_URL + 'SHA256/b757f9c61c1b939f104e5d7861130bb28d90f33267a012eb8bb763a435f29d37')
- loadedNode = SampleData.SampleDataLogic().downloadFromSource(dataSource)[0]
- """
-
- def __init__(self, sampleName=None, sampleDescription=None, uris=None, fileNames=None, nodeNames=None,
- checksums=None, loadFiles=None,
- customDownloader=None, thumbnailFileName=None,
- loadFileType=None, loadFileProperties=None):
- """
- :param sampleName: Name identifying the data set.
- :param sampleDescription: Displayed name of data set in SampleData module GUI. (default is ``sampleName``)
- :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
- :param uris: Download URL(s).
- :param fileNames: File name(s) that will be downloaded (and loaded).
- :param nodeNames: Node name(s) in the scene.
- :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- :param loadFiles: Boolean indicating if file(s) should be loaded.
- :param customDownloader: Custom function for downloading.
- :param loadFileType: file format name(s) ('VolumeFile' by default if node name is specified).
- :param loadFileProperties: custom properties passed to the IO plugin.
+ """Describe a set of sample data associated with one or multiple URIs and filenames.
+
+ Example::
+
+ import SampleData
+ from slicer.util import TESTING_DATA_URL
+ dataSource = SampleData.SampleDataSource(
+ nodeNames='fixed',
+ fileNames='fixed.nrrd',
+ uris=TESTING_DATA_URL + 'SHA256/b757f9c61c1b939f104e5d7861130bb28d90f33267a012eb8bb763a435f29d37')
+ loadedNode = SampleData.SampleDataLogic().downloadFromSource(dataSource)[0]
"""
- self.sampleName = sampleName
- if sampleDescription is None:
- sampleDescription = sampleName
- self.sampleDescription = sampleDescription
- if (isinstance(uris, list) or isinstance(uris, tuple)):
- if isinstance(loadFileType, str) or loadFileType is None:
- loadFileType = [loadFileType] * len(uris)
- if nodeNames is None:
- nodeNames = [None] * len(uris)
- if loadFiles is None:
- loadFiles = [None] * len(uris)
- if checksums is None:
- checksums = [None] * len(uris)
- elif isinstance(uris, str):
- uris = [uris, ]
- fileNames = [fileNames, ]
- nodeNames = [nodeNames, ]
- loadFiles = [loadFiles, ]
- loadFileType = [loadFileType, ]
- checksums = [checksums, ]
-
- updatedFileType = []
- for fileName, nodeName, fileType in zip(fileNames, nodeNames, loadFileType):
- # If not explicitly specified, attempt to guess fileType
- if fileType is None:
- if nodeName is not None:
- # TODO: Use method from Slicer IO logic ?
- fileType = "VolumeFile"
- else:
- ext = os.path.splitext(fileName.lower())[1]
- if ext in [".mrml", ".mrb"]:
- fileType = "SceneFile"
- elif ext in [".zip"]:
- fileType = "ZipFile"
- updatedFileType.append(fileType)
-
- if loadFileProperties is None:
- loadFileProperties = {}
-
- self.uris = uris
- self.fileNames = fileNames
- self.nodeNames = nodeNames
- self.loadFiles = loadFiles
- self.customDownloader = customDownloader
- self.thumbnailFileName = thumbnailFileName
- self.loadFileType = updatedFileType
- self.loadFileProperties = loadFileProperties
- self.checksums = checksums
- if not len(uris) == len(fileNames) == len(nodeNames) == len(loadFiles) == len(updatedFileType) == len(checksums):
- raise ValueError(
- f"All fields of sample data source must have the same length\n"
- f" uris : {uris}\n"
- f" len(uris) : {len(uris)}\n"
- f" len(fileNames) : {len(fileNames)}\n"
- f" len(nodeNames) : {len(nodeNames)}\n"
- f" len(loadFiles) : {len(loadFiles)}\n"
- f" len(updatedFileType) : {len(updatedFileType)}\n"
- f" len(checksums) : {len(checksums)}\n"
- )
- def __eq__(self, other):
- return str(self) == str(other)
-
- def __str__(self):
- output = [
- "sampleName : %s" % self.sampleName,
- "sampleDescription : %s" % self.sampleDescription,
- "thumbnailFileName : %s" % self.thumbnailFileName,
- "loadFileProperties: %s" % self.loadFileProperties,
- "customDownloader : %s" % self.customDownloader,
- ""
- ]
- for fileName, uri, nodeName, loadFile, fileType, checksum in zip(self.fileNames, self.uris, self.nodeNames, self.loadFiles, self.loadFileType, self.checksums):
- output.extend([
- " fileName : %s" % fileName,
- " uri : %s" % uri,
- " checksum : %s" % checksum,
- " nodeName : %s" % nodeName,
- " loadFile : %s" % loadFile,
- " loadFileType: %s" % fileType,
- ""
- ])
- return "\n".join(output)
+ def __init__(self, sampleName=None, sampleDescription=None, uris=None, fileNames=None, nodeNames=None,
+ checksums=None, loadFiles=None,
+ customDownloader=None, thumbnailFileName=None,
+ loadFileType=None, loadFileProperties=None):
+ """
+ :param sampleName: Name identifying the data set.
+ :param sampleDescription: Displayed name of data set in SampleData module GUI. (default is ``sampleName``)
+ :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
+ :param uris: Download URL(s).
+ :param fileNames: File name(s) that will be downloaded (and loaded).
+ :param nodeNames: Node name(s) in the scene.
+ :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ :param loadFiles: Boolean indicating if file(s) should be loaded.
+ :param customDownloader: Custom function for downloading.
+ :param loadFileType: file format name(s) ('VolumeFile' by default if node name is specified).
+ :param loadFileProperties: custom properties passed to the IO plugin.
+ """
+ self.sampleName = sampleName
+ if sampleDescription is None:
+ sampleDescription = sampleName
+ self.sampleDescription = sampleDescription
+ if (isinstance(uris, list) or isinstance(uris, tuple)):
+ if isinstance(loadFileType, str) or loadFileType is None:
+ loadFileType = [loadFileType] * len(uris)
+ if nodeNames is None:
+ nodeNames = [None] * len(uris)
+ if loadFiles is None:
+ loadFiles = [None] * len(uris)
+ if checksums is None:
+ checksums = [None] * len(uris)
+ elif isinstance(uris, str):
+ uris = [uris, ]
+ fileNames = [fileNames, ]
+ nodeNames = [nodeNames, ]
+ loadFiles = [loadFiles, ]
+ loadFileType = [loadFileType, ]
+ checksums = [checksums, ]
+
+ updatedFileType = []
+ for fileName, nodeName, fileType in zip(fileNames, nodeNames, loadFileType):
+ # If not explicitly specified, attempt to guess fileType
+ if fileType is None:
+ if nodeName is not None:
+ # TODO: Use method from Slicer IO logic ?
+ fileType = "VolumeFile"
+ else:
+ ext = os.path.splitext(fileName.lower())[1]
+ if ext in [".mrml", ".mrb"]:
+ fileType = "SceneFile"
+ elif ext in [".zip"]:
+ fileType = "ZipFile"
+ updatedFileType.append(fileType)
+
+ if loadFileProperties is None:
+ loadFileProperties = {}
+
+ self.uris = uris
+ self.fileNames = fileNames
+ self.nodeNames = nodeNames
+ self.loadFiles = loadFiles
+ self.customDownloader = customDownloader
+ self.thumbnailFileName = thumbnailFileName
+ self.loadFileType = updatedFileType
+ self.loadFileProperties = loadFileProperties
+ self.checksums = checksums
+ if not len(uris) == len(fileNames) == len(nodeNames) == len(loadFiles) == len(updatedFileType) == len(checksums):
+ raise ValueError(
+ f"All fields of sample data source must have the same length\n"
+ f" uris : {uris}\n"
+ f" len(uris) : {len(uris)}\n"
+ f" len(fileNames) : {len(fileNames)}\n"
+ f" len(nodeNames) : {len(nodeNames)}\n"
+ f" len(loadFiles) : {len(loadFiles)}\n"
+ f" len(updatedFileType) : {len(updatedFileType)}\n"
+ f" len(checksums) : {len(checksums)}\n"
+ )
+
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+ def __str__(self):
+ output = [
+ "sampleName : %s" % self.sampleName,
+ "sampleDescription : %s" % self.sampleDescription,
+ "thumbnailFileName : %s" % self.thumbnailFileName,
+ "loadFileProperties: %s" % self.loadFileProperties,
+ "customDownloader : %s" % self.customDownloader,
+ ""
+ ]
+ for fileName, uri, nodeName, loadFile, fileType, checksum in zip(self.fileNames, self.uris, self.nodeNames, self.loadFiles, self.loadFileType, self.checksums):
+ output.extend([
+ " fileName : %s" % fileName,
+ " uri : %s" % uri,
+ " checksum : %s" % checksum,
+ " nodeName : %s" % nodeName,
+ " loadFile : %s" % loadFile,
+ " loadFileType: %s" % fileType,
+ ""
+ ])
+ return "\n".join(output)
#
@@ -246,160 +246,160 @@ def __str__(self):
#
class SampleDataWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # This module is often used in developer mode, therefore
- # collapse reload & test section by default.
- if hasattr(self, "reloadCollapsibleButton"):
- self.reloadCollapsibleButton.collapsed = True
-
- self.logic = SampleDataLogic(self.logMessage)
-
- self.categoryLayout = qt.QVBoxLayout()
- self.categoryLayout.setContentsMargins(0, 0, 0, 0)
- self.layout.addLayout(self.categoryLayout)
-
- SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, slicer.modules.sampleDataSources, self.logic)
- if self.developerMode is False:
- self.setCategoryVisible(self.logic.developmentCategoryName, False)
-
- self.log = qt.QTextEdit()
- self.log.readOnly = True
- self.layout.addWidget(self.log)
- self.logMessage('Status: Idle
')
-
- # Add spacer to layout
- self.layout.addStretch(1)
-
- def cleanup(self):
- SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, {}, self.logic)
-
- @staticmethod
- def removeCategories(categoryLayout):
- """Remove all categories from the given category layout.
- """
- while categoryLayout.count() > 0:
- frame = categoryLayout.itemAt(0).widget()
- frame.visible = False
- categoryLayout.removeWidget(frame)
- frame.setParent(0)
- del frame
-
- @staticmethod
- def setCategoriesFromSampleDataSources(categoryLayout, dataSources, logic):
- """Update categoryLayout adding buttons for downloading dataSources.
-
- Download buttons are organized in collapsible GroupBox with one GroupBox
- per category.
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons')
- mainWindow = slicer.util.mainWindow()
- if mainWindow:
- iconSize = qt.QSize(int(mainWindow.width / 8), int(mainWindow.height / 6))
- else:
- # There is no main window in the automated tests
- desktop = qt.QDesktopWidget()
- mainScreenSize = desktop.availableGeometry(desktop.primaryScreen)
- iconSize = qt.QSize(int(mainScreenSize.width() / 15), int(mainScreenSize.height() / 10))
-
- categories = sorted(dataSources.keys())
-
- # Ensure "builtIn" catergory is always first
- if logic.builtInCategoryName in categories:
- categories.remove(logic.builtInCategoryName)
- categories.insert(0, logic.builtInCategoryName)
-
- # Clear category layout
- SampleDataWidget.removeCategories(categoryLayout)
-
- # Populate category layout
- for category in categories:
- frame = ctk.ctkCollapsibleGroupBox(categoryLayout.parentWidget())
- categoryLayout.addWidget(frame)
- frame.title = category
- frame.name = '%sCollapsibleGroupBox' % category
- layout = ctk.ctkFlowLayout()
- layout.preferredExpandingDirections = qt.Qt.Vertical
- frame.setLayout(layout)
- for source in dataSources[category]:
- name = source.sampleDescription
- if not name:
- name = source.nodeNames[0]
-
- b = qt.QToolButton()
- b.setText(name)
-
- # Set thumbnail
- if source.thumbnailFileName:
- # Thumbnail provided
- thumbnailImage = source.thumbnailFileName
- else:
- # Look for thumbnail image with the name of any node name with .png extension
- thumbnailImage = None
- for nodeName in source.nodeNames:
- if not nodeName:
- continue
- thumbnailImageAttempt = os.path.join(iconPath, nodeName + '.png')
- if os.path.exists(thumbnailImageAttempt):
- thumbnailImage = thumbnailImageAttempt
- break
- if thumbnailImage and os.path.exists(thumbnailImage):
- b.setIcon(qt.QIcon(thumbnailImage))
-
- b.setIconSize(iconSize)
- b.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon)
- qSize = qt.QSizePolicy()
- qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
- b.setSizePolicy(qSize)
-
- b.name = '%sPushButton' % name
- layout.addWidget(b)
- if source.customDownloader:
- b.connect('clicked()', lambda s=source: s.customDownloader(s))
- else:
- b.connect('clicked()', lambda s=source: logic.downloadFromSource(s))
-
- def logMessage(self, message, logLevel=logging.DEBUG):
- # Set text color based on log level
- if logLevel >= logging.ERROR:
- message = '' + message + ''
- elif logLevel >= logging.WARNING:
- message = '' + message + ''
- # Show message in status bar
- doc = qt.QTextDocument()
- doc.setHtml(message)
- slicer.util.showStatusMessage(doc.toPlainText(), 3000)
- # Show message in log window at the bottom of the module widget
- self.log.insertHtml(message)
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
- self.log.repaint()
- logging.log(logLevel, message)
- slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
-
- def isCategoryVisible(self, category):
- """Check the visibility of a SampleData category given its name.
-
- Returns False if the category is not visible or if it does not exist,
- otherwise returns True.
- """
- if not SampleDataLogic.sampleDataSourcesByCategory(category):
- return False
- return slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).isVisible()
-
- def setCategoryVisible(self, category, visible):
- """Update visibility of a SampleData category given its name.
- The function is a no-op if the category does not exist.
- """
- if not SampleDataLogic.sampleDataSourcesByCategory(category):
- return
- slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).setVisible(visible)
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # This module is often used in developer mode, therefore
+ # collapse reload & test section by default.
+ if hasattr(self, "reloadCollapsibleButton"):
+ self.reloadCollapsibleButton.collapsed = True
+
+ self.logic = SampleDataLogic(self.logMessage)
+
+ self.categoryLayout = qt.QVBoxLayout()
+ self.categoryLayout.setContentsMargins(0, 0, 0, 0)
+ self.layout.addLayout(self.categoryLayout)
+
+ SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, slicer.modules.sampleDataSources, self.logic)
+ if self.developerMode is False:
+ self.setCategoryVisible(self.logic.developmentCategoryName, False)
+
+ self.log = qt.QTextEdit()
+ self.log.readOnly = True
+ self.layout.addWidget(self.log)
+ self.logMessage('Status: Idle
')
+
+ # Add spacer to layout
+ self.layout.addStretch(1)
+
+ def cleanup(self):
+ SampleDataWidget.setCategoriesFromSampleDataSources(self.categoryLayout, {}, self.logic)
+
+ @staticmethod
+ def removeCategories(categoryLayout):
+ """Remove all categories from the given category layout.
+ """
+ while categoryLayout.count() > 0:
+ frame = categoryLayout.itemAt(0).widget()
+ frame.visible = False
+ categoryLayout.removeWidget(frame)
+ frame.setParent(0)
+ del frame
+
+ @staticmethod
+ def setCategoriesFromSampleDataSources(categoryLayout, dataSources, logic):
+ """Update categoryLayout adding buttons for downloading dataSources.
+
+ Download buttons are organized in collapsible GroupBox with one GroupBox
+ per category.
+ """
+ iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons')
+ mainWindow = slicer.util.mainWindow()
+ if mainWindow:
+ iconSize = qt.QSize(int(mainWindow.width / 8), int(mainWindow.height / 6))
+ else:
+ # There is no main window in the automated tests
+ desktop = qt.QDesktopWidget()
+ mainScreenSize = desktop.availableGeometry(desktop.primaryScreen)
+ iconSize = qt.QSize(int(mainScreenSize.width() / 15), int(mainScreenSize.height() / 10))
+
+ categories = sorted(dataSources.keys())
+
+ # Ensure "builtIn" catergory is always first
+ if logic.builtInCategoryName in categories:
+ categories.remove(logic.builtInCategoryName)
+ categories.insert(0, logic.builtInCategoryName)
+
+ # Clear category layout
+ SampleDataWidget.removeCategories(categoryLayout)
+
+ # Populate category layout
+ for category in categories:
+ frame = ctk.ctkCollapsibleGroupBox(categoryLayout.parentWidget())
+ categoryLayout.addWidget(frame)
+ frame.title = category
+ frame.name = '%sCollapsibleGroupBox' % category
+ layout = ctk.ctkFlowLayout()
+ layout.preferredExpandingDirections = qt.Qt.Vertical
+ frame.setLayout(layout)
+ for source in dataSources[category]:
+ name = source.sampleDescription
+ if not name:
+ name = source.nodeNames[0]
+
+ b = qt.QToolButton()
+ b.setText(name)
+
+ # Set thumbnail
+ if source.thumbnailFileName:
+ # Thumbnail provided
+ thumbnailImage = source.thumbnailFileName
+ else:
+ # Look for thumbnail image with the name of any node name with .png extension
+ thumbnailImage = None
+ for nodeName in source.nodeNames:
+ if not nodeName:
+ continue
+ thumbnailImageAttempt = os.path.join(iconPath, nodeName + '.png')
+ if os.path.exists(thumbnailImageAttempt):
+ thumbnailImage = thumbnailImageAttempt
+ break
+ if thumbnailImage and os.path.exists(thumbnailImage):
+ b.setIcon(qt.QIcon(thumbnailImage))
+
+ b.setIconSize(iconSize)
+ b.setToolButtonStyle(qt.Qt.ToolButtonTextUnderIcon)
+ qSize = qt.QSizePolicy()
+ qSize.setHorizontalPolicy(qt.QSizePolicy.Expanding)
+ b.setSizePolicy(qSize)
+
+ b.name = '%sPushButton' % name
+ layout.addWidget(b)
+ if source.customDownloader:
+ b.connect('clicked()', lambda s=source: s.customDownloader(s))
+ else:
+ b.connect('clicked()', lambda s=source: logic.downloadFromSource(s))
+
+ def logMessage(self, message, logLevel=logging.DEBUG):
+ # Set text color based on log level
+ if logLevel >= logging.ERROR:
+ message = '' + message + ''
+ elif logLevel >= logging.WARNING:
+ message = '' + message + ''
+ # Show message in status bar
+ doc = qt.QTextDocument()
+ doc.setHtml(message)
+ slicer.util.showStatusMessage(doc.toPlainText(), 3000)
+ # Show message in log window at the bottom of the module widget
+ self.log.insertHtml(message)
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+ self.log.repaint()
+ logging.log(logLevel, message)
+ slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
+
+ def isCategoryVisible(self, category):
+ """Check the visibility of a SampleData category given its name.
+
+ Returns False if the category is not visible or if it does not exist,
+ otherwise returns True.
+ """
+ if not SampleDataLogic.sampleDataSourcesByCategory(category):
+ return False
+ return slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).isVisible()
+
+ def setCategoryVisible(self, category, visible):
+ """Update visibility of a SampleData category given its name.
+
+ The function is a no-op if the category does not exist.
+ """
+ if not SampleDataLogic.sampleDataSourcesByCategory(category):
+ return
+ slicer.util.findChild(self.parent, '%sCollapsibleGroupBox' % category).setVisible(visible)
#
@@ -407,770 +407,770 @@ def setCategoryVisible(self, category, visible):
#
class SampleDataLogic:
- """Manage the slicer.modules.sampleDataSources dictionary.
- The dictionary keys are categories of sample data sources.
- The BuiltIn category is managed here. Modules or extensions can
- register their own sample data by creating instances of the
- SampleDataSource class. These instances should be stored in a
- list that is assigned to a category following the model
- used in registerBuiltInSampleDataSources below.
-
- Checksums are expected to be formatted as a string of the form
- ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- """
-
- @staticmethod
- def registerCustomSampleDataSource(category='Custom',
- sampleName=None, uris=None, fileNames=None, nodeNames=None,
- customDownloader=None, thumbnailFileName=None,
- loadFileType='VolumeFile', loadFiles=None, loadFileProperties={},
- checksums=None):
- """Adds custom data sets to SampleData.
- :param category: Section title of data set in SampleData module GUI.
- :param sampleName: Displayed name of data set in SampleData module GUI.
- :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
- :param uris: Download URL(s).
- :param fileNames: File name(s) that will be loaded.
- :param nodeNames: Node name(s) in the scene.
- :param customDownloader: Custom function for downloading.
- :param loadFileType: file format name(s) ('VolumeFile' by default).
- :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
- :param loadFileProperties: custom properties passed to the IO plugin.
- :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- """
-
- try:
- slicer.modules.sampleDataSources
- except AttributeError:
- slicer.modules.sampleDataSources = {}
-
- if category not in slicer.modules.sampleDataSources:
- slicer.modules.sampleDataSources[category] = []
-
- dataSource = SampleDataSource(
- sampleName=sampleName,
- uris=uris,
- fileNames=fileNames,
- nodeNames=nodeNames,
- thumbnailFileName=thumbnailFileName,
- loadFileType=loadFileType,
- loadFiles=loadFiles,
- loadFileProperties=loadFileProperties,
- checksums=checksums,
- customDownloader=customDownloader,
- )
-
- if SampleDataLogic.isSampleDataSourceRegistered(category, dataSource):
- return
-
- slicer.modules.sampleDataSources[category].append(dataSource)
-
- @staticmethod
- def sampleDataSourcesByCategory(category=None):
- """Return the registered SampleDataSources for with the given category.
-
- If no category is specified, returns all registered SampleDataSources.
- """
- try:
- slicer.modules.sampleDataSources
- except AttributeError:
- slicer.modules.sampleDataSources = {}
-
- if category is None:
- return slicer.modules.sampleDataSources
- else:
- return slicer.modules.sampleDataSources.get(category, [])
-
- @staticmethod
- def isSampleDataSourceRegistered(category, sampleDataSource):
- """Returns True if the sampleDataSource is registered with the category.
+ """Manage the slicer.modules.sampleDataSources dictionary.
+ The dictionary keys are categories of sample data sources.
+ The BuiltIn category is managed here. Modules or extensions can
+ register their own sample data by creating instances of the
+ SampleDataSource class. These instances should be stored in a
+ list that is assigned to a category following the model
+ used in registerBuiltInSampleDataSources below.
+
+ Checksums are expected to be formatted as a string of the form
+ ``:``. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
"""
- try:
- slicer.modules.sampleDataSources
- except AttributeError:
- slicer.modules.sampleDataSources = {}
-
- if not isinstance(sampleDataSource, SampleDataSource):
- raise TypeError(f"unsupported sampleDataSource type '{type(sampleDataSource)}': '{str(SampleDataSource)}' is expected")
-
- return sampleDataSource in slicer.modules.sampleDataSources.get(category, [])
-
- def __init__(self, logMessage=None):
- if logMessage:
- self.logMessage = logMessage
- self.builtInCategoryName = 'BuiltIn'
- self.developmentCategoryName = 'Development'
- self.registerBuiltInSampleDataSources()
- self.registerDevelopmentSampleDataSources()
- if slicer.app.testingEnabled():
- self.registerTestingDataSources()
- self.downloadPercent = 0
-
- def registerBuiltInSampleDataSources(self):
- """Fills in the pre-define sample data sources"""
-
- # Arguments:
- # sampleName=None, sampleDescription=None,
- # uris=None,
- # fileNames=None, nodeNames=None,
- # checksums=None,
- # loadFiles=None, customDownloader=None, thumbnailFileName=None, loadFileType=None, loadFileProperties=None
- sourceArguments = (
- ('MRHead', None, TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- 'MR-head.nrrd', 'MRHead', 'SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93'),
- ('CTChest', None, TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e',
- 'CT-chest.nrrd', 'CTChest', 'SHA256:4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'),
- ('CTACardio', None, TESTING_DATA_URL + 'SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2',
- 'CTA-cardio.nrrd', 'CTACardio', 'SHA256:3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2'),
- ('DTIBrain', None, TESTING_DATA_URL + 'SHA256/5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a',
- 'DTI-Brain.nrrd', 'DTIBrain', 'SHA256:5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a'),
- ('MRBrainTumor1', None, TESTING_DATA_URL + 'SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95',
- 'RegLib_C01_1.nrrd', 'MRBrainTumor1', 'SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95'),
- ('MRBrainTumor2', None, TESTING_DATA_URL + 'SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97',
- 'RegLib_C01_2.nrrd', 'MRBrainTumor2', 'SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97'),
- ('BaselineVolume', None, TESTING_DATA_URL + 'SHA256/dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2',
- 'BaselineVolume.nrrd', 'BaselineVolume', 'SHA256:dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2'),
- ('DTIVolume', None,
- (TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
- TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe', ),
- ('DTIVolume.raw.gz', 'DTIVolume.nhdr'), (None, 'DTIVolume'),
- ('SHA256:d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
- 'SHA256:67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe')),
- ('DWIVolume', None,
- (TESTING_DATA_URL + 'SHA256/cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692',
- TESTING_DATA_URL + 'SHA256/7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed'),
- ('dwi.raw.gz', 'dwi.nhdr'), (None, 'dwi'),
- ('SHA256:cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692',
- 'SHA256:7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed')),
- ('CTAAbdomenPanoramix', 'CTA abdomen\n(Panoramix)', TESTING_DATA_URL + 'SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433',
- 'Panoramix-cropped.nrrd', 'Panoramix-cropped', 'SHA256:146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433'),
- ('CBCTDentalSurgery', None,
- (TESTING_DATA_URL + 'SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968',
- TESTING_DATA_URL + 'SHA256/4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd',),
- ('PreDentalSurgery.gipl.gz', 'PostDentalSurgery.gipl.gz'), ('PreDentalSurgery', 'PostDentalSurgery'),
- ('SHA256:7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968',
- 'SHA256:4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd')),
- ('MRUSProstate', 'MR-US Prostate',
- (TESTING_DATA_URL + 'SHA256/4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa',
- TESTING_DATA_URL + 'SHA256/34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2',),
- ('Case10-MR.nrrd', 'case10_US_resampled.nrrd'), ('MRProstate', 'USProstate'),
- ('SHA256:4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa',
- 'SHA256:34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2')),
- ('CTMRBrain', 'CT-MR Brain',
- (TESTING_DATA_URL + 'SHA256/6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1',
- TESTING_DATA_URL + 'SHA256/2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593',
- TESTING_DATA_URL + 'SHA256/fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7',),
- ('CT-brain.nrrd', 'MR-brain-T1.nrrd', 'MR-brain-T2.nrrd'),
- ('CTBrain', 'MRBrainT1', 'MRBrainT2'),
- ('SHA256:6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1',
- 'SHA256:2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593',
- 'SHA256:fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7')),
- ('CBCTMRHead', 'CBCT-MR Head',
- (TESTING_DATA_URL + 'SHA256/4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db',
- TESTING_DATA_URL + 'SHA256/b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842',),
- ('DZ-CBCT.nrrd', 'DZ-MR.nrrd'),
- ('DZ-CBCT', 'DZ-MR'),
- ('SHA256:4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db',
- 'SHA256:b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842')),
- ('CTLiver', None, TESTING_DATA_URL + 'SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e',
- 'CTLiver.nrrd', 'CTLiver', 'SHA256:e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e'),
- ('CTPCardioSeq', "CTP Cardio Sequence",
- 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2',
- 'CTP-cardio.seq.nrrd', 'CTPCardioSeq',
- 'SHA256:7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2',
- None, None, None, "SequenceFile"),
- ('CTCardioSeq', "CT Cardio Sequence",
- 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93',
- 'CT-cardio.seq.nrrd', 'CTCardioSeq',
- 'SHA256:d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93',
- None, None, None, "SequenceFile"),
- )
-
- if self.builtInCategoryName not in slicer.modules.sampleDataSources:
- slicer.modules.sampleDataSources[self.builtInCategoryName] = []
- for sourceArgument in sourceArguments:
- dataSource = SampleDataSource(*sourceArgument)
- if SampleDataLogic.isSampleDataSourceRegistered(self.builtInCategoryName, dataSource):
- continue
- slicer.modules.sampleDataSources[self.builtInCategoryName].append(dataSource)
-
- def registerDevelopmentSampleDataSources(self):
- """Fills in the sample data sources displayed only if developer mode is enabled."""
- iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons')
- self.registerCustomSampleDataSource(
- category=self.developmentCategoryName, sampleName='TinyPatient',
- uris=[TESTING_DATA_URL + 'SHA256/c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b',
- TESTING_DATA_URL + 'SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'],
- fileNames=['TinyPatient_CT.nrrd', 'TinyPatient_Structures.seg.nrrd'],
- nodeNames=['TinyPatient_CT', 'TinyPatient_Segments'],
- thumbnailFileName=os.path.join(iconPath, 'TinyPatient.png'),
- loadFileType=['VolumeFile', 'SegmentationFile'],
- checksums=['SHA256:c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', 'SHA256:3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470']
- )
-
- def registerTestingDataSources(self):
- """Register sample data sources used by SampleData self-test to test module functionalities."""
- self.registerCustomSampleDataSource(**SampleDataTest.CustomDownloaderDataSource)
-
- def downloadFileIntoCache(self, uri, name, checksum=None):
- """Given a uri and and a filename, download the data into
- a file of the given name in the scene's cache"""
- destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()
-
- if not os.access(destFolderPath, os.W_OK):
- try:
- os.makedirs(destFolderPath, exist_ok=True)
- except:
- self.logMessage('Failed to create cache folder %s' % destFolderPath, logging.ERROR)
- if not os.access(destFolderPath, os.W_OK):
- self.logMessage('Cache folder %s is not writable' % destFolderPath, logging.ERROR)
- return self.downloadFile(uri, destFolderPath, name, checksum)
-
- def downloadSourceIntoCache(self, source):
- """Download all files for the given source and return a
- list of file paths for the results"""
- filePaths = []
- for uri, fileName, checksum in zip(source.uris, source.fileNames, source.checksums):
- filePaths.append(self.downloadFileIntoCache(uri, fileName, checksum))
- return filePaths
-
- def downloadFromSource(self, source, maximumAttemptsCount=3):
- """Given an instance of SampleDataSource, downloads the associated data and
- load them into Slicer if it applies.
-
- The function always returns a list.
-
- Based on the fileType(s), nodeName(s) and loadFile(s) associated with
- the source, different values may be appended to the returned list:
-
- - if nodeName is specified, appends loaded nodes but if ``loadFile`` is False appends downloaded filepath
- - if fileType is ``SceneFile``, appends downloaded filepath
- - if fileType is ``ZipFile``, appends directory of extracted archive but if ``loadFile`` is False appends downloaded filepath
-
- If no ``nodeNames`` and no ``fileTypes`` are specified or if ``loadFiles`` are all False,
- returns the list of all downloaded filepaths.
- """
-
- # Input may contain urls without associated node names, which correspond to additional data files
- # (e.g., .raw file for a .nhdr header file). Therefore we collect nodes and file paths separately
- # and we only return file paths if no node names have been provided.
- resultNodes = []
- resultFilePaths = []
- for uri, fileName, nodeName, checksum, loadFile, loadFileType in zip(source.uris, source.fileNames, source.nodeNames, source.checksums, source.loadFiles, source.loadFileType):
+ @staticmethod
+ def registerCustomSampleDataSource(category='Custom',
+ sampleName=None, uris=None, fileNames=None, nodeNames=None,
+ customDownloader=None, thumbnailFileName=None,
+ loadFileType='VolumeFile', loadFiles=None, loadFileProperties={},
+ checksums=None):
+ """Adds custom data sets to SampleData.
+ :param category: Section title of data set in SampleData module GUI.
+ :param sampleName: Displayed name of data set in SampleData module GUI.
+ :param thumbnailFileName: Displayed thumbnail of data set in SampleData module GUI,
+ :param uris: Download URL(s).
+ :param fileNames: File name(s) that will be loaded.
+ :param nodeNames: Node name(s) in the scene.
+ :param customDownloader: Custom function for downloading.
+ :param loadFileType: file format name(s) ('VolumeFile' by default).
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
+ :param loadFileProperties: custom properties passed to the IO plugin.
+ :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ """
- current_source = SampleDataSource(uris=uri, fileNames=fileName, nodeNames=nodeName, checksums=checksum, loadFiles=loadFile, loadFileType=loadFileType, loadFileProperties=source.loadFileProperties)
-
- for attemptsCount in range(maximumAttemptsCount):
-
- # Download
try:
- filePath = self.downloadFileIntoCache(uri, fileName, checksum)
- except ValueError:
- self.logMessage('Download failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR)
- continue
- resultFilePaths.append(filePath)
-
- if loadFileType == 'ZipFile':
- if loadFile == False:
- resultNodes.append(filePath)
- break
- outputDir = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + "/" + os.path.splitext(os.path.basename(filePath))[0]
- qt.QDir().mkpath(outputDir)
- if slicer.util.extractArchive(filePath, outputDir):
- # Success
- resultNodes.append(outputDir)
- break
- elif loadFileType == 'SceneFile':
- if not loadFile:
- resultNodes.append(filePath)
- break
- if self.loadScene(filePath, source.loadFileProperties.copy()):
- # Success
- resultNodes.append(filePath)
- break
- elif nodeName:
- if loadFile == False:
- resultNodes.append(filePath)
- break
- loadedNode = self.loadNode(filePath, nodeName, loadFileType, source.loadFileProperties.copy())
- if loadedNode:
- # Success
- resultNodes.append(loadedNode)
- break
- else:
- # no need to load node
- break
-
- # Failed. Clean up downloaded file (it might have been a partial download)
- file = qt.QFile(filePath)
- if file.exists() and not file.remove():
- self.logMessage('Load failed (attempt %d of %d). Unable to delete and try again loading %s'
- % (attemptsCount + 1, maximumAttemptsCount, filePath), logging.ERROR)
- resultNodes.append(loadedNode)
- break
- self.logMessage('Load failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR)
-
- if resultNodes:
- return resultNodes
- else:
- return resultFilePaths
-
- def sourceForSampleName(self, sampleName):
- """For a given sample name this will search the available sources.
- Returns SampleDataSource instance."""
- for category in slicer.modules.sampleDataSources.keys():
- for source in slicer.modules.sampleDataSources[category]:
- if sampleName == source.sampleName:
- return source
- return None
-
- def categoryForSource(self, a_source):
- """For a given SampleDataSource return the associated category name.
- """
- for category in slicer.modules.sampleDataSources.keys():
- for source in slicer.modules.sampleDataSources[category]:
- if a_source == source:
- return category
- return None
-
- def downloadFromURL(self, uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None,
- customDownloader=None, loadFileTypes=None, loadFileProperties={}):
- """Download and optionally load data into the application.
-
- :param uris: Download URL(s).
- :param fileNames: File name(s) that will be downloaded (and loaded).
- :param nodeNames: Node name(s) in the scene.
- :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
- :param customDownloader: Custom function for downloading.
- :param loadFileTypes: file format name(s) ('VolumeFile' by default).
- :param loadFileProperties: custom properties passed to the IO plugin.
-
- If the given ``fileNames`` are not found in the application cache directory, they
- are downloaded using the associated URIs.
- See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
-
- If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
- guessed based on the corresponding filename extensions.
-
- If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
- by default. To ensure the file is loaded, ``loadFiles`` must be set.
-
- The ``loadFileProperties`` are common for all files. If different properties
- need to be associated with files of different types, downloadFromURL must
- be called for each.
- """
- return self.downloadFromSource(SampleDataSource(
- uris=uris, fileNames=fileNames, nodeNames=nodeNames, loadFiles=loadFiles,
- loadFileType=loadFileTypes, loadFileProperties=loadFileProperties, checksums=checksums
- ))
-
- def downloadSample(self, sampleName):
- """For a given sample name this will search the available sources
- and load it if it is available. Returns the first loaded node."""
- return self.downloadSamples(sampleName)[0]
-
- def downloadSamples(self, sampleName):
- """For a given sample name this will search the available sources
- and load it if it is available. Returns the loaded nodes."""
- source = self.sourceForSampleName(sampleName)
- nodes = []
- if source:
- nodes = self.downloadFromSource(source)
- return nodes
-
- def logMessage(self, message, logLevel=logging.DEBUG):
- logging.log(logLevel, message)
-
- """Utility methods for backwards compatibility"""
-
- def downloadMRHead(self):
- return self.downloadSample('MRHead')
-
- def downloadCTChest(self):
- return self.downloadSample('CTChest')
-
- def downloadCTACardio(self):
- return self.downloadSample('CTACardio')
-
- def downloadDTIBrain(self):
- return self.downloadSample('DTIBrain')
-
- def downloadMRBrainTumor1(self):
- return self.downloadSample('MRBrainTumor1')
-
- def downloadMRBrainTumor2(self):
- return self.downloadSample('MRBrainTumor2')
-
- def downloadWhiteMatterExplorationBaselineVolume(self):
- return self.downloadSample('BaselineVolume')
+ slicer.modules.sampleDataSources
+ except AttributeError:
+ slicer.modules.sampleDataSources = {}
+
+ if category not in slicer.modules.sampleDataSources:
+ slicer.modules.sampleDataSources[category] = []
+
+ dataSource = SampleDataSource(
+ sampleName=sampleName,
+ uris=uris,
+ fileNames=fileNames,
+ nodeNames=nodeNames,
+ thumbnailFileName=thumbnailFileName,
+ loadFileType=loadFileType,
+ loadFiles=loadFiles,
+ loadFileProperties=loadFileProperties,
+ checksums=checksums,
+ customDownloader=customDownloader,
+ )
- def downloadWhiteMatterExplorationDTIVolume(self):
- return self.downloadSample('DTIVolume')
+ if SampleDataLogic.isSampleDataSourceRegistered(category, dataSource):
+ return
- def downloadDiffusionMRIDWIVolume(self):
- return self.downloadSample('DWIVolume')
+ slicer.modules.sampleDataSources[category].append(dataSource)
- def downloadAbdominalCTVolume(self):
- return self.downloadSample('CTAAbdomenPanoramix')
+ @staticmethod
+ def sampleDataSourcesByCategory(category=None):
+ """Return the registered SampleDataSources for with the given category.
- def downloadDentalSurgery(self):
- # returns list since that's what earlier method did
- return self.downloadSamples('CBCTDentalSurgery')
+ If no category is specified, returns all registered SampleDataSources.
+ """
+ try:
+ slicer.modules.sampleDataSources
+ except AttributeError:
+ slicer.modules.sampleDataSources = {}
- def downloadMRUSPostate(self):
- # returns list since that's what earlier method did
- return self.downloadSamples('MRUSProstate')
+ if category is None:
+ return slicer.modules.sampleDataSources
+ else:
+ return slicer.modules.sampleDataSources.get(category, [])
- def humanFormatSize(self, size):
- """ from https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size"""
- for x in ['bytes', 'KB', 'MB', 'GB']:
- if size < 1024.0 and size > -1024.0:
- return f"{size:3.1f} {x}"
- size /= 1024.0
- return "{:3.1f} {}".format(size, 'TB')
+ @staticmethod
+ def isSampleDataSourceRegistered(category, sampleDataSource):
+ """Returns True if the sampleDataSource is registered with the category.
+ """
+ try:
+ slicer.modules.sampleDataSources
+ except AttributeError:
+ slicer.modules.sampleDataSources = {}
+
+ if not isinstance(sampleDataSource, SampleDataSource):
+ raise TypeError(f"unsupported sampleDataSource type '{type(sampleDataSource)}': '{str(SampleDataSource)}' is expected")
+
+ return sampleDataSource in slicer.modules.sampleDataSources.get(category, [])
+
+ def __init__(self, logMessage=None):
+ if logMessage:
+ self.logMessage = logMessage
+ self.builtInCategoryName = 'BuiltIn'
+ self.developmentCategoryName = 'Development'
+ self.registerBuiltInSampleDataSources()
+ self.registerDevelopmentSampleDataSources()
+ if slicer.app.testingEnabled():
+ self.registerTestingDataSources()
+ self.downloadPercent = 0
+
+ def registerBuiltInSampleDataSources(self):
+ """Fills in the pre-define sample data sources"""
+
+ # Arguments:
+ # sampleName=None, sampleDescription=None,
+ # uris=None,
+ # fileNames=None, nodeNames=None,
+ # checksums=None,
+ # loadFiles=None, customDownloader=None, thumbnailFileName=None, loadFileType=None, loadFileProperties=None
+ sourceArguments = (
+ ('MRHead', None, TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ 'MR-head.nrrd', 'MRHead', 'SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93'),
+ ('CTChest', None, TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e',
+ 'CT-chest.nrrd', 'CTChest', 'SHA256:4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'),
+ ('CTACardio', None, TESTING_DATA_URL + 'SHA256/3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2',
+ 'CTA-cardio.nrrd', 'CTACardio', 'SHA256:3b0d4eb1a7d8ebb0c5a89cc0504640f76a030b4e869e33ff34c564c3d3b88ad2'),
+ ('DTIBrain', None, TESTING_DATA_URL + 'SHA256/5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a',
+ 'DTI-Brain.nrrd', 'DTIBrain', 'SHA256:5c78d00c86ae8d968caa7a49b870ef8e1c04525b1abc53845751d8bce1f0b91a'),
+ ('MRBrainTumor1', None, TESTING_DATA_URL + 'SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95',
+ 'RegLib_C01_1.nrrd', 'MRBrainTumor1', 'SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95'),
+ ('MRBrainTumor2', None, TESTING_DATA_URL + 'SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97',
+ 'RegLib_C01_2.nrrd', 'MRBrainTumor2', 'SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97'),
+ ('BaselineVolume', None, TESTING_DATA_URL + 'SHA256/dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2',
+ 'BaselineVolume.nrrd', 'BaselineVolume', 'SHA256:dff28a7711d20b6e16d5416535f6010eb99fd0c8468aaa39be4e39da78e93ec2'),
+ ('DTIVolume', None,
+ (TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
+ TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe', ),
+ ('DTIVolume.raw.gz', 'DTIVolume.nhdr'), (None, 'DTIVolume'),
+ ('SHA256:d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
+ 'SHA256:67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe')),
+ ('DWIVolume', None,
+ (TESTING_DATA_URL + 'SHA256/cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692',
+ TESTING_DATA_URL + 'SHA256/7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed'),
+ ('dwi.raw.gz', 'dwi.nhdr'), (None, 'dwi'),
+ ('SHA256:cf03fd53583dc05120d3314d0a82bdf5946799b1f72f2a7f08963f3fd24ca692',
+ 'SHA256:7666d83bc205382e418444ea60ab7df6dba6a0bd684933df8809da6b476b0fed')),
+ ('CTAAbdomenPanoramix', 'CTA abdomen\n(Panoramix)', TESTING_DATA_URL + 'SHA256/146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433',
+ 'Panoramix-cropped.nrrd', 'Panoramix-cropped', 'SHA256:146af87511520c500a3706b7b2bfb545f40d5d04dd180be3a7a2c6940e447433'),
+ ('CBCTDentalSurgery', None,
+ (TESTING_DATA_URL + 'SHA256/7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968',
+ TESTING_DATA_URL + 'SHA256/4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd',),
+ ('PreDentalSurgery.gipl.gz', 'PostDentalSurgery.gipl.gz'), ('PreDentalSurgery', 'PostDentalSurgery'),
+ ('SHA256:7bfa16945629c319a439f414cfb7edddd2a97ba97753e12eede3b56a0eb09968',
+ 'SHA256:4cdc3dc35519bb57daeef4e5df89c00849750e778809e94971d3876f95cc7bbd')),
+ ('MRUSProstate', 'MR-US Prostate',
+ (TESTING_DATA_URL + 'SHA256/4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa',
+ TESTING_DATA_URL + 'SHA256/34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2',),
+ ('Case10-MR.nrrd', 'case10_US_resampled.nrrd'), ('MRProstate', 'USProstate'),
+ ('SHA256:4843cdc9ea5d7bcce61650d1492ce01035727c892019339dca726380496896aa',
+ 'SHA256:34decf58b1e6794069acbe947b460252262fe95b6858c5e320aeab03bc82ebb2')),
+ ('CTMRBrain', 'CT-MR Brain',
+ (TESTING_DATA_URL + 'SHA256/6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1',
+ TESTING_DATA_URL + 'SHA256/2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593',
+ TESTING_DATA_URL + 'SHA256/fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7',),
+ ('CT-brain.nrrd', 'MR-brain-T1.nrrd', 'MR-brain-T2.nrrd'),
+ ('CTBrain', 'MRBrainT1', 'MRBrainT2'),
+ ('SHA256:6a5b6caccb76576a863beb095e3bfb910c50ca78f4c9bf043aa42f976cfa53d1',
+ 'SHA256:2da3f655ed20356ee8cdf32aa0f8f9420385de4b6e407d28e67f9974d7ce1593',
+ 'SHA256:fa1fe5910a69182f2b03c0150d8151ac6c75df986449fb5a6c5ae67141e0f5e7')),
+ ('CBCTMRHead', 'CBCT-MR Head',
+ (TESTING_DATA_URL + 'SHA256/4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db',
+ TESTING_DATA_URL + 'SHA256/b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842',),
+ ('DZ-CBCT.nrrd', 'DZ-MR.nrrd'),
+ ('DZ-CBCT', 'DZ-MR'),
+ ('SHA256:4ce7aa75278b5a7b757ed0c8d7a6b3caccfc3e2973b020532456dbc8f3def7db',
+ 'SHA256:b5e9f8afac58d6eb0e0d63d059616c25a98e0beb80f3108410b15260a6817842')),
+ ('CTLiver', None, TESTING_DATA_URL + 'SHA256/e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e',
+ 'CTLiver.nrrd', 'CTLiver', 'SHA256:e16eae0ae6fefa858c5c11e58f0f1bb81834d81b7102e021571056324ef6f37e'),
+ ('CTPCardioSeq', "CTP Cardio Sequence",
+ 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2',
+ 'CTP-cardio.seq.nrrd', 'CTPCardioSeq',
+ 'SHA256:7fbb6ad0aed9c00820d66e143c2f037568025ed63db0a8db05ae7f26affeb1c2',
+ None, None, None, "SequenceFile"),
+ ('CTCardioSeq', "CT Cardio Sequence",
+ 'https://github.com/Slicer/SlicerDataStore/releases/download/SHA256/d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93',
+ 'CT-cardio.seq.nrrd', 'CTCardioSeq',
+ 'SHA256:d1a1119969acead6c39c7c3ec69223fa2957edc561bc5bf384a203e2284dbc93',
+ None, None, None, "SequenceFile"),
+ )
- def reportHook(self, blocksSoFar, blockSize, totalSize):
- # we clamp to 100% because the blockSize might be larger than the file itself
- percent = min(int((100. * blocksSoFar * blockSize) / totalSize), 100)
- if percent == 100 or (percent - self.downloadPercent >= 10):
- # we clamp to totalSize when blockSize is larger than totalSize
- humanSizeSoFar = self.humanFormatSize(min(blocksSoFar * blockSize, totalSize))
- humanSizeTotal = self.humanFormatSize(totalSize)
- self.logMessage('Downloaded %s (%d%% of %s)...' % (humanSizeSoFar, percent, humanSizeTotal))
- self.downloadPercent = percent
+ if self.builtInCategoryName not in slicer.modules.sampleDataSources:
+ slicer.modules.sampleDataSources[self.builtInCategoryName] = []
+ for sourceArgument in sourceArguments:
+ dataSource = SampleDataSource(*sourceArgument)
+ if SampleDataLogic.isSampleDataSourceRegistered(self.builtInCategoryName, dataSource):
+ continue
+ slicer.modules.sampleDataSources[self.builtInCategoryName].append(dataSource)
+
+ def registerDevelopmentSampleDataSources(self):
+ """Fills in the sample data sources displayed only if developer mode is enabled."""
+ iconPath = os.path.join(os.path.dirname(__file__).replace('\\', '/'), 'Resources', 'Icons')
+ self.registerCustomSampleDataSource(
+ category=self.developmentCategoryName, sampleName='TinyPatient',
+ uris=[TESTING_DATA_URL + 'SHA256/c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b',
+ TESTING_DATA_URL + 'SHA256/3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470'],
+ fileNames=['TinyPatient_CT.nrrd', 'TinyPatient_Structures.seg.nrrd'],
+ nodeNames=['TinyPatient_CT', 'TinyPatient_Segments'],
+ thumbnailFileName=os.path.join(iconPath, 'TinyPatient.png'),
+ loadFileType=['VolumeFile', 'SegmentationFile'],
+ checksums=['SHA256:c0743772587e2dd4c97d4e894f5486f7a9a202049c8575e032114c0a5c935c3b', 'SHA256:3243b62bde36b1db1cdbfe204785bd4bc1fbb772558d5f8cac964cda8385d470']
+ )
- def downloadFile(self, uri, destFolderPath, name, checksum=None):
- """
- :param uri: Download URL.
- :param destFolderPath: Folder to download the file into.
- :param name: File name that will be downloaded.
- :param checksum: Checksum formatted as ``:`` to verify the downloaded file. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
- """
- self.downloadPercent = 0
- filePath = destFolderPath + '/' + name
- (algo, digest) = extractAlgoAndDigest(checksum)
- if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
- import urllib.request, urllib.parse, urllib.error
- self.logMessage(f'Requesting download {name} from {uri} ...')
- try:
- urllib.request.urlretrieve(uri, filePath, self.reportHook)
- self.logMessage('Download finished')
- except OSError as e:
- self.logMessage('\tDownload failed: %s' % e, logging.ERROR)
- raise ValueError(f"Failed to download {uri} to {filePath}")
-
- if algo is not None:
- self.logMessage('Verifying checksum')
- current_digest = computeChecksum(algo, filePath)
- if current_digest != digest:
- self.logMessage(f'Checksum verification failed. Computed checksum {current_digest} different from expected checksum {digest}')
- qt.QFile(filePath).remove()
+ def registerTestingDataSources(self):
+ """Register sample data sources used by SampleData self-test to test module functionalities."""
+ self.registerCustomSampleDataSource(**SampleDataTest.CustomDownloaderDataSource)
+
+ def downloadFileIntoCache(self, uri, name, checksum=None):
+ """Given a uri and and a filename, download the data into
+ a file of the given name in the scene's cache"""
+ destFolderPath = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()
+
+ if not os.access(destFolderPath, os.W_OK):
+ try:
+ os.makedirs(destFolderPath, exist_ok=True)
+ except:
+ self.logMessage('Failed to create cache folder %s' % destFolderPath, logging.ERROR)
+ if not os.access(destFolderPath, os.W_OK):
+ self.logMessage('Cache folder %s is not writable' % destFolderPath, logging.ERROR)
+ return self.downloadFile(uri, destFolderPath, name, checksum)
+
+ def downloadSourceIntoCache(self, source):
+ """Download all files for the given source and return a
+ list of file paths for the results"""
+ filePaths = []
+ for uri, fileName, checksum in zip(source.uris, source.fileNames, source.checksums):
+ filePaths.append(self.downloadFileIntoCache(uri, fileName, checksum))
+ return filePaths
+
+ def downloadFromSource(self, source, maximumAttemptsCount=3):
+ """Given an instance of SampleDataSource, downloads the associated data and
+ load them into Slicer if it applies.
+
+ The function always returns a list.
+
+ Based on the fileType(s), nodeName(s) and loadFile(s) associated with
+ the source, different values may be appended to the returned list:
+
+ - if nodeName is specified, appends loaded nodes but if ``loadFile`` is False appends downloaded filepath
+ - if fileType is ``SceneFile``, appends downloaded filepath
+ - if fileType is ``ZipFile``, appends directory of extracted archive but if ``loadFile`` is False appends downloaded filepath
+
+ If no ``nodeNames`` and no ``fileTypes`` are specified or if ``loadFiles`` are all False,
+ returns the list of all downloaded filepaths.
+ """
+
+ # Input may contain urls without associated node names, which correspond to additional data files
+ # (e.g., .raw file for a .nhdr header file). Therefore we collect nodes and file paths separately
+ # and we only return file paths if no node names have been provided.
+ resultNodes = []
+ resultFilePaths = []
+
+ for uri, fileName, nodeName, checksum, loadFile, loadFileType in zip(source.uris, source.fileNames, source.nodeNames, source.checksums, source.loadFiles, source.loadFileType):
+
+ current_source = SampleDataSource(uris=uri, fileNames=fileName, nodeNames=nodeName, checksums=checksum, loadFiles=loadFile, loadFileType=loadFileType, loadFileProperties=source.loadFileProperties)
+
+ for attemptsCount in range(maximumAttemptsCount):
+
+ # Download
+ try:
+ filePath = self.downloadFileIntoCache(uri, fileName, checksum)
+ except ValueError:
+ self.logMessage('Download failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR)
+ continue
+ resultFilePaths.append(filePath)
+
+ if loadFileType == 'ZipFile':
+ if loadFile == False:
+ resultNodes.append(filePath)
+ break
+ outputDir = slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory() + "/" + os.path.splitext(os.path.basename(filePath))[0]
+ qt.QDir().mkpath(outputDir)
+ if slicer.util.extractArchive(filePath, outputDir):
+ # Success
+ resultNodes.append(outputDir)
+ break
+ elif loadFileType == 'SceneFile':
+ if not loadFile:
+ resultNodes.append(filePath)
+ break
+ if self.loadScene(filePath, source.loadFileProperties.copy()):
+ # Success
+ resultNodes.append(filePath)
+ break
+ elif nodeName:
+ if loadFile == False:
+ resultNodes.append(filePath)
+ break
+ loadedNode = self.loadNode(filePath, nodeName, loadFileType, source.loadFileProperties.copy())
+ if loadedNode:
+ # Success
+ resultNodes.append(loadedNode)
+ break
+ else:
+ # no need to load node
+ break
+
+ # Failed. Clean up downloaded file (it might have been a partial download)
+ file = qt.QFile(filePath)
+ if file.exists() and not file.remove():
+ self.logMessage('Load failed (attempt %d of %d). Unable to delete and try again loading %s'
+ % (attemptsCount + 1, maximumAttemptsCount, filePath), logging.ERROR)
+ resultNodes.append(loadedNode)
+ break
+ self.logMessage('Load failed (attempt %d of %d)...' % (attemptsCount + 1, maximumAttemptsCount), logging.ERROR)
+
+ if resultNodes:
+ return resultNodes
else:
- self.downloadPercent = 100
- self.logMessage('Checksum OK')
- else:
- if algo is not None:
- self.logMessage('Verifying checksum')
- current_digest = computeChecksum(algo, filePath)
- if current_digest != digest:
- self.logMessage('File already exists in cache but checksum is different - re-downloading it.')
- qt.QFile(filePath).remove()
- return self.downloadFile(uri, destFolderPath, name, checksum)
+ return resultFilePaths
+
+ def sourceForSampleName(self, sampleName):
+ """For a given sample name this will search the available sources.
+ Returns SampleDataSource instance."""
+ for category in slicer.modules.sampleDataSources.keys():
+ for source in slicer.modules.sampleDataSources[category]:
+ if sampleName == source.sampleName:
+ return source
+ return None
+
+ def categoryForSource(self, a_source):
+ """For a given SampleDataSource return the associated category name.
+ """
+ for category in slicer.modules.sampleDataSources.keys():
+ for source in slicer.modules.sampleDataSources[category]:
+ if a_source == source:
+ return category
+ return None
+
+ def downloadFromURL(self, uris=None, fileNames=None, nodeNames=None, checksums=None, loadFiles=None,
+ customDownloader=None, loadFileTypes=None, loadFileProperties={}):
+ """Download and optionally load data into the application.
+
+ :param uris: Download URL(s).
+ :param fileNames: File name(s) that will be downloaded (and loaded).
+ :param nodeNames: Node name(s) in the scene.
+ :param checksums: Checksum(s) formatted as ``:`` to verify the downloaded file(s). For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ :param loadFiles: Boolean indicating if file(s) should be loaded. By default, the function decides.
+ :param customDownloader: Custom function for downloading.
+ :param loadFileTypes: file format name(s) ('VolumeFile' by default).
+ :param loadFileProperties: custom properties passed to the IO plugin.
+
+ If the given ``fileNames`` are not found in the application cache directory, they
+ are downloaded using the associated URIs.
+ See ``slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory()``
+
+ If not explicitly provided or if set to ``None``, the ``loadFileTypes`` are
+ guessed based on the corresponding filename extensions.
+
+ If a given fileName has the ``.mrb`` or ``.mrml`` extension, it will **not** be loaded
+ by default. To ensure the file is loaded, ``loadFiles`` must be set.
+
+ The ``loadFileProperties`` are common for all files. If different properties
+ need to be associated with files of different types, downloadFromURL must
+ be called for each.
+ """
+ return self.downloadFromSource(SampleDataSource(
+ uris=uris, fileNames=fileNames, nodeNames=nodeNames, loadFiles=loadFiles,
+ loadFileType=loadFileTypes, loadFileProperties=loadFileProperties, checksums=checksums
+ ))
+
+ def downloadSample(self, sampleName):
+ """For a given sample name this will search the available sources
+ and load it if it is available. Returns the first loaded node."""
+ return self.downloadSamples(sampleName)[0]
+
+ def downloadSamples(self, sampleName):
+ """For a given sample name this will search the available sources
+ and load it if it is available. Returns the loaded nodes."""
+ source = self.sourceForSampleName(sampleName)
+ nodes = []
+ if source:
+ nodes = self.downloadFromSource(source)
+ return nodes
+
+ def logMessage(self, message, logLevel=logging.DEBUG):
+ logging.log(logLevel, message)
+
+ """Utility methods for backwards compatibility"""
+
+ def downloadMRHead(self):
+ return self.downloadSample('MRHead')
+
+ def downloadCTChest(self):
+ return self.downloadSample('CTChest')
+
+ def downloadCTACardio(self):
+ return self.downloadSample('CTACardio')
+
+ def downloadDTIBrain(self):
+ return self.downloadSample('DTIBrain')
+
+ def downloadMRBrainTumor1(self):
+ return self.downloadSample('MRBrainTumor1')
+
+ def downloadMRBrainTumor2(self):
+ return self.downloadSample('MRBrainTumor2')
+
+ def downloadWhiteMatterExplorationBaselineVolume(self):
+ return self.downloadSample('BaselineVolume')
+
+ def downloadWhiteMatterExplorationDTIVolume(self):
+ return self.downloadSample('DTIVolume')
+
+ def downloadDiffusionMRIDWIVolume(self):
+ return self.downloadSample('DWIVolume')
+
+ def downloadAbdominalCTVolume(self):
+ return self.downloadSample('CTAAbdomenPanoramix')
+
+ def downloadDentalSurgery(self):
+ # returns list since that's what earlier method did
+ return self.downloadSamples('CBCTDentalSurgery')
+
+ def downloadMRUSPostate(self):
+ # returns list since that's what earlier method did
+ return self.downloadSamples('MRUSProstate')
+
+ def humanFormatSize(self, size):
+ """ from https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size"""
+ for x in ['bytes', 'KB', 'MB', 'GB']:
+ if size < 1024.0 and size > -1024.0:
+ return f"{size:3.1f} {x}"
+ size /= 1024.0
+ return "{:3.1f} {}".format(size, 'TB')
+
+ def reportHook(self, blocksSoFar, blockSize, totalSize):
+ # we clamp to 100% because the blockSize might be larger than the file itself
+ percent = min(int((100. * blocksSoFar * blockSize) / totalSize), 100)
+ if percent == 100 or (percent - self.downloadPercent >= 10):
+ # we clamp to totalSize when blockSize is larger than totalSize
+ humanSizeSoFar = self.humanFormatSize(min(blocksSoFar * blockSize, totalSize))
+ humanSizeTotal = self.humanFormatSize(totalSize)
+ self.logMessage('Downloaded %s (%d%% of %s)...' % (humanSizeSoFar, percent, humanSizeTotal))
+ self.downloadPercent = percent
+
+ def downloadFile(self, uri, destFolderPath, name, checksum=None):
+ """
+ :param uri: Download URL.
+ :param destFolderPath: Folder to download the file into.
+ :param name: File name that will be downloaded.
+ :param checksum: Checksum formatted as ``:`` to verify the downloaded file. For example, ``SHA256:cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93``.
+ """
+ self.downloadPercent = 0
+ filePath = destFolderPath + '/' + name
+ (algo, digest) = extractAlgoAndDigest(checksum)
+ if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
+ import urllib.request, urllib.parse, urllib.error
+ self.logMessage(f'Requesting download {name} from {uri} ...')
+ try:
+ urllib.request.urlretrieve(uri, filePath, self.reportHook)
+ self.logMessage('Download finished')
+ except OSError as e:
+ self.logMessage('\tDownload failed: %s' % e, logging.ERROR)
+ raise ValueError(f"Failed to download {uri} to {filePath}")
+
+ if algo is not None:
+ self.logMessage('Verifying checksum')
+ current_digest = computeChecksum(algo, filePath)
+ if current_digest != digest:
+ self.logMessage(f'Checksum verification failed. Computed checksum {current_digest} different from expected checksum {digest}')
+ qt.QFile(filePath).remove()
+ else:
+ self.downloadPercent = 100
+ self.logMessage('Checksum OK')
else:
- self.downloadPercent = 100
- self.logMessage('File already exists and checksum is OK - reusing it.')
- else:
- self.downloadPercent = 100
- self.logMessage('File already exists in cache - reusing it.')
- return filePath
-
- def loadScene(self, uri, fileProperties={}):
- self.logMessage('Requesting load %s ...' % uri)
- fileProperties['fileName'] = uri
- success = slicer.app.coreIOManager().loadNodes('SceneFile', fileProperties)
- if not success:
- self.logMessage('\tLoad failed!', logging.ERROR)
- return False
- self.logMessage('Load finished')
- return True
-
- def loadNode(self, uri, name, fileType='VolumeFile', fileProperties={}):
- self.logMessage(f'Requesting load {name} from {uri} ...')
-
- fileProperties['fileName'] = uri
- fileProperties['name'] = name
- firstLoadedNode = None
- loadedNodes = vtk.vtkCollection()
- success = slicer.app.coreIOManager().loadNodes(fileType, fileProperties, loadedNodes)
-
- if not success or loadedNodes.GetNumberOfItems() < 1:
- self.logMessage('\tLoad failed!', logging.ERROR)
- return None
-
- self.logMessage('Load finished')
-
- # since nodes were read from a temp directory remove the storage nodes
- for i in range(loadedNodes.GetNumberOfItems()):
- loadedNode = loadedNodes.GetItemAsObject(i)
- if not loadedNode.IsA("vtkMRMLStorableNode"):
- continue
- storageNode = loadedNode.GetStorageNode()
- if not storageNode:
- continue
- slicer.mrmlScene.RemoveNode(storageNode)
- loadedNode.SetAndObserveStorageNodeID(None)
-
- return loadedNodes.GetItemAsObject(0)
+ if algo is not None:
+ self.logMessage('Verifying checksum')
+ current_digest = computeChecksum(algo, filePath)
+ if current_digest != digest:
+ self.logMessage('File already exists in cache but checksum is different - re-downloading it.')
+ qt.QFile(filePath).remove()
+ return self.downloadFile(uri, destFolderPath, name, checksum)
+ else:
+ self.downloadPercent = 100
+ self.logMessage('File already exists and checksum is OK - reusing it.')
+ else:
+ self.downloadPercent = 100
+ self.logMessage('File already exists in cache - reusing it.')
+ return filePath
+
+ def loadScene(self, uri, fileProperties={}):
+ self.logMessage('Requesting load %s ...' % uri)
+ fileProperties['fileName'] = uri
+ success = slicer.app.coreIOManager().loadNodes('SceneFile', fileProperties)
+ if not success:
+ self.logMessage('\tLoad failed!', logging.ERROR)
+ return False
+ self.logMessage('Load finished')
+ return True
+
+ def loadNode(self, uri, name, fileType='VolumeFile', fileProperties={}):
+ self.logMessage(f'Requesting load {name} from {uri} ...')
+
+ fileProperties['fileName'] = uri
+ fileProperties['name'] = name
+ firstLoadedNode = None
+ loadedNodes = vtk.vtkCollection()
+ success = slicer.app.coreIOManager().loadNodes(fileType, fileProperties, loadedNodes)
+
+ if not success or loadedNodes.GetNumberOfItems() < 1:
+ self.logMessage('\tLoad failed!', logging.ERROR)
+ return None
+
+ self.logMessage('Load finished')
+
+ # since nodes were read from a temp directory remove the storage nodes
+ for i in range(loadedNodes.GetNumberOfItems()):
+ loadedNode = loadedNodes.GetItemAsObject(i)
+ if not loadedNode.IsA("vtkMRMLStorableNode"):
+ continue
+ storageNode = loadedNode.GetStorageNode()
+ if not storageNode:
+ continue
+ slicer.mrmlScene.RemoveNode(storageNode)
+ loadedNode.SetAndObserveStorageNodeID(None)
+
+ return loadedNodes.GetItemAsObject(0)
class SampleDataTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- customDownloads = []
-
- def setUp(self):
- slicer.mrmlScene.Clear(0)
- SampleDataTest.customDownloads = []
-
- def runTest(self):
- for test in [
- self.test_downloadFromSource_downloadFiles,
- self.test_downloadFromSource_downloadZipFile,
- self.test_downloadFromSource_loadMRBFile,
- self.test_downloadFromSource_loadMRMLFile,
- self.test_downloadFromSource_downloadMRBFile,
- self.test_downloadFromSource_downloadMRMLFile,
- self.test_downloadFromSource_loadNode,
- self.test_downloadFromSource_loadNodeFromMultipleFiles,
- self.test_downloadFromSource_loadNodes,
- self.test_downloadFromSource_loadNodesWithLoadFileFalse,
- self.test_sampleDataSourcesByCategory,
- self.test_categoryVisibility,
- self.test_setCategoriesFromSampleDataSources,
- self.test_isSampleDataSourceRegistered,
- self.test_customDownloader,
- self.test_categoryForSource,
- ]:
- self.setUp()
- test()
-
- @staticmethod
- def path2uri(path):
- """Gets a URI from a local file path.
- Typically it prefixes the received path by file:// or file:///.
"""
- import urllib.parse, urllib.request, urllib.parse, urllib.error
- return urllib.parse.urljoin('file:', urllib.request.pathname2url(path))
-
- def test_downloadFromSource_downloadFiles(self):
- """Specifying URIs and fileNames without nodeNames is expected to download the files
- without loading into Slicer.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- logic = SampleDataLogic()
-
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- fileNames='MR-head.nrrd'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
-
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
- fileNames=['MR-head.nrrd', 'CT-chest.nrrd']))
- self.assertEqual(len(filePaths), 2)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertTrue(os.path.exists(filePaths[1]))
- self.assertTrue(os.path.isfile(filePaths[1]))
- self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_downloadZipFile(self):
- logic = SampleDataLogic()
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
- fileNames='TinyPatient_Seg.zip'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isdir(filePaths[0]))
- self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_loadMRBFile(self):
- logic = SampleDataLogic()
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8',
- loadFiles=True, fileNames='slicer4minute.mrb'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_loadMRMLFile(self):
- logic = SampleDataLogic()
- tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
- tempFile.open()
- tempFile.write(textwrap.dedent("""
+
+ customDownloads = []
+
+ def setUp(self):
+ slicer.mrmlScene.Clear(0)
+ SampleDataTest.customDownloads = []
+
+ def runTest(self):
+ for test in [
+ self.test_downloadFromSource_downloadFiles,
+ self.test_downloadFromSource_downloadZipFile,
+ self.test_downloadFromSource_loadMRBFile,
+ self.test_downloadFromSource_loadMRMLFile,
+ self.test_downloadFromSource_downloadMRBFile,
+ self.test_downloadFromSource_downloadMRMLFile,
+ self.test_downloadFromSource_loadNode,
+ self.test_downloadFromSource_loadNodeFromMultipleFiles,
+ self.test_downloadFromSource_loadNodes,
+ self.test_downloadFromSource_loadNodesWithLoadFileFalse,
+ self.test_sampleDataSourcesByCategory,
+ self.test_categoryVisibility,
+ self.test_setCategoriesFromSampleDataSources,
+ self.test_isSampleDataSourceRegistered,
+ self.test_customDownloader,
+ self.test_categoryForSource,
+ ]:
+ self.setUp()
+ test()
+
+ @staticmethod
+ def path2uri(path):
+ """Gets a URI from a local file path.
+ Typically it prefixes the received path by file:// or file:///.
+ """
+ import urllib.parse, urllib.request, urllib.parse, urllib.error
+ return urllib.parse.urljoin('file:', urllib.request.pathname2url(path))
+
+ def test_downloadFromSource_downloadFiles(self):
+ """Specifying URIs and fileNames without nodeNames is expected to download the files
+ without loading into Slicer.
+ """
+ logic = SampleDataLogic()
+
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ fileNames='MR-head.nrrd'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
+
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
+ fileNames=['MR-head.nrrd', 'CT-chest.nrrd']))
+ self.assertEqual(len(filePaths), 2)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertTrue(os.path.exists(filePaths[1]))
+ self.assertTrue(os.path.isfile(filePaths[1]))
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_downloadZipFile(self):
+ logic = SampleDataLogic()
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=TESTING_DATA_URL + 'SHA256/b902f635ef2059cd3b4ba854c000b388e4a9e817a651f28be05c22511a317ec7',
+ fileNames='TinyPatient_Seg.zip'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isdir(filePaths[0]))
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_loadMRBFile(self):
+ logic = SampleDataLogic()
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8',
+ loadFiles=True, fileNames='slicer4minute.mrb'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_loadMRMLFile(self):
+ logic = SampleDataLogic()
+ tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
+ tempFile.open()
+ tempFile.write(textwrap.dedent("""
""").strip())
- tempFile.close()
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=self.path2uri(tempFile.fileName()), loadFiles=True, fileNames='scene.mrml'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_downloadMRBFile(self):
- logic = SampleDataLogic()
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8',
- fileNames='slicer4minute.mrb'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_downloadMRMLFile(self):
- logic = SampleDataLogic()
- tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
- tempFile.open()
- tempFile.write(textwrap.dedent("""
+ tempFile.close()
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=self.path2uri(tempFile.fileName()), loadFiles=True, fileNames='scene.mrml'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertTrue(sceneMTime < slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_downloadMRBFile(self):
+ logic = SampleDataLogic()
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=TESTING_DATA_URL + 'SHA256/5a1c78c3347f77970b1a29e718bfa10e5376214692d55a7320af94b9d8d592b8',
+ fileNames='slicer4minute.mrb'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_downloadMRMLFile(self):
+ logic = SampleDataLogic()
+ tempFile = qt.QTemporaryFile(slicer.app.temporaryPath + "/SampleDataTest-loadSceneFile-XXXXXX.mrml")
+ tempFile.open()
+ tempFile.write(textwrap.dedent("""
""").strip())
- tempFile.close()
- sceneMTime = slicer.mrmlScene.GetMTime()
- filePaths = logic.downloadFromSource(SampleDataSource(
- uris=self.path2uri(tempFile.fileName()), fileNames='scene.mrml'))
- self.assertEqual(len(filePaths), 1)
- self.assertTrue(os.path.exists(filePaths[0]))
- self.assertTrue(os.path.isfile(filePaths[0]))
- self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
-
- def test_downloadFromSource_loadNode(self):
- logic = SampleDataLogic()
- nodes = logic.downloadFromSource(SampleDataSource(
- uris=TESTING_DATA_URL + 'MD5/39b01631b7b38232a220007230624c8e',
- fileNames='MR-head.nrrd', nodeNames='MRHead'))
- self.assertEqual(len(nodes), 1)
- self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
-
- def test_downloadFromSource_loadNodeFromMultipleFiles(self):
- logic = SampleDataLogic()
- nodes = logic.downloadFromSource(SampleDataSource(
- uris=[TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
- TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe'],
- fileNames=['DTIVolume.raw.gz', 'DTIVolume.nhdr'],
- nodeNames=[None, 'DTIVolume']))
- self.assertEqual(len(nodes), 1)
- self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("DTIVolume"))
-
- def test_downloadFromSource_loadNodesWithLoadFileFalse(self):
- logic = SampleDataLogic()
- nodes = logic.downloadFromSource(SampleDataSource(
- uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
- fileNames=['MR-head.nrrd', 'CT-chest.nrrd'],
- nodeNames=['MRHead', 'CTChest'],
- loadFiles=[False, True]))
- self.assertEqual(len(nodes), 2)
- self.assertTrue(os.path.exists(nodes[0]))
- self.assertTrue(os.path.isfile(nodes[0]))
- self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
-
- def test_downloadFromSource_loadNodes(self):
- logic = SampleDataLogic()
- nodes = logic.downloadFromSource(SampleDataSource(
- uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
- TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
- fileNames=['MR-head.nrrd', 'CT-chest.nrrd'],
- nodeNames=['MRHead', 'CTChest']))
- self.assertEqual(len(nodes), 2)
- self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
- self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
-
- def test_sampleDataSourcesByCategory(self):
- self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory()) > 0)
- self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('BuiltIn')) > 0)
- self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('Not_A_Registered_Category')) == 0)
-
- def test_categoryVisibility(self):
- slicer.util.selectModule("SampleData")
- widget = slicer.modules.SampleDataWidget
- widget.setCategoryVisible('BuiltIn', False)
- self.assertFalse(widget.isCategoryVisible('BuiltIn'))
- widget.setCategoryVisible('BuiltIn', True)
- self.assertTrue(widget.isCategoryVisible('BuiltIn'))
-
- def test_setCategoriesFromSampleDataSources(self):
- slicer.util.selectModule("SampleData")
- widget = slicer.modules.SampleDataWidget
- self.assertGreater(widget.categoryLayout.count(), 0)
-
- SampleDataWidget.removeCategories(widget.categoryLayout)
- self.assertEqual(widget.categoryLayout.count(), 0)
-
- SampleDataWidget.setCategoriesFromSampleDataSources(widget.categoryLayout, slicer.modules.sampleDataSources, widget.logic)
- self.assertGreater(widget.categoryLayout.count(), 0)
-
- def test_isSampleDataSourceRegistered(self):
- if not slicer.app.testingEnabled():
- return
- sourceArguments = {
- 'sampleName': 'isSampleDataSourceRegistered',
- 'uris': 'https://slicer.org',
- 'fileNames': 'volume.nrrd',
- 'loadFileType': 'VolumeFile',
+ tempFile.close()
+ sceneMTime = slicer.mrmlScene.GetMTime()
+ filePaths = logic.downloadFromSource(SampleDataSource(
+ uris=self.path2uri(tempFile.fileName()), fileNames='scene.mrml'))
+ self.assertEqual(len(filePaths), 1)
+ self.assertTrue(os.path.exists(filePaths[0]))
+ self.assertTrue(os.path.isfile(filePaths[0]))
+ self.assertEqual(sceneMTime, slicer.mrmlScene.GetMTime())
+
+ def test_downloadFromSource_loadNode(self):
+ logic = SampleDataLogic()
+ nodes = logic.downloadFromSource(SampleDataSource(
+ uris=TESTING_DATA_URL + 'MD5/39b01631b7b38232a220007230624c8e',
+ fileNames='MR-head.nrrd', nodeNames='MRHead'))
+ self.assertEqual(len(nodes), 1)
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
+
+ def test_downloadFromSource_loadNodeFromMultipleFiles(self):
+ logic = SampleDataLogic()
+ nodes = logic.downloadFromSource(SampleDataSource(
+ uris=[TESTING_DATA_URL + 'SHA256/d785837276758ddd9d21d76a3694e7fd866505a05bc305793517774c117cb38d',
+ TESTING_DATA_URL + 'SHA256/67564aa42c7e2eec5c3fd68afb5a910e9eab837b61da780933716a3b922e50fe'],
+ fileNames=['DTIVolume.raw.gz', 'DTIVolume.nhdr'],
+ nodeNames=[None, 'DTIVolume']))
+ self.assertEqual(len(nodes), 1)
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("DTIVolume"))
+
+ def test_downloadFromSource_loadNodesWithLoadFileFalse(self):
+ logic = SampleDataLogic()
+ nodes = logic.downloadFromSource(SampleDataSource(
+ uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
+ fileNames=['MR-head.nrrd', 'CT-chest.nrrd'],
+ nodeNames=['MRHead', 'CTChest'],
+ loadFiles=[False, True]))
+ self.assertEqual(len(nodes), 2)
+ self.assertTrue(os.path.exists(nodes[0]))
+ self.assertTrue(os.path.isfile(nodes[0]))
+ self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
+
+ def test_downloadFromSource_loadNodes(self):
+ logic = SampleDataLogic()
+ nodes = logic.downloadFromSource(SampleDataSource(
+ uris=[TESTING_DATA_URL + 'SHA256/cc211f0dfd9a05ca3841ce1141b292898b2dd2d3f08286affadf823a7e58df93',
+ TESTING_DATA_URL + 'SHA256/4507b664690840abb6cb9af2d919377ffc4ef75b167cb6fd0f747befdb12e38e'],
+ fileNames=['MR-head.nrrd', 'CT-chest.nrrd'],
+ nodeNames=['MRHead', 'CTChest']))
+ self.assertEqual(len(nodes), 2)
+ self.assertEqual(nodes[0], slicer.mrmlScene.GetFirstNodeByName("MRHead"))
+ self.assertEqual(nodes[1], slicer.mrmlScene.GetFirstNodeByName("CTChest"))
+
+ def test_sampleDataSourcesByCategory(self):
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory()) > 0)
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('BuiltIn')) > 0)
+ self.assertTrue(len(SampleDataLogic.sampleDataSourcesByCategory('Not_A_Registered_Category')) == 0)
+
+ def test_categoryVisibility(self):
+ slicer.util.selectModule("SampleData")
+ widget = slicer.modules.SampleDataWidget
+ widget.setCategoryVisible('BuiltIn', False)
+ self.assertFalse(widget.isCategoryVisible('BuiltIn'))
+ widget.setCategoryVisible('BuiltIn', True)
+ self.assertTrue(widget.isCategoryVisible('BuiltIn'))
+
+ def test_setCategoriesFromSampleDataSources(self):
+ slicer.util.selectModule("SampleData")
+ widget = slicer.modules.SampleDataWidget
+ self.assertGreater(widget.categoryLayout.count(), 0)
+
+ SampleDataWidget.removeCategories(widget.categoryLayout)
+ self.assertEqual(widget.categoryLayout.count(), 0)
+
+ SampleDataWidget.setCategoriesFromSampleDataSources(widget.categoryLayout, slicer.modules.sampleDataSources, widget.logic)
+ self.assertGreater(widget.categoryLayout.count(), 0)
+
+ def test_isSampleDataSourceRegistered(self):
+ if not slicer.app.testingEnabled():
+ return
+ sourceArguments = {
+ 'sampleName': 'isSampleDataSourceRegistered',
+ 'uris': 'https://slicer.org',
+ 'fileNames': 'volume.nrrd',
+ 'loadFileType': 'VolumeFile',
+ }
+ self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
+ SampleDataLogic.registerCustomSampleDataSource(**sourceArguments, category="Testing")
+ self.assertTrue(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
+ self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Other", SampleDataSource(**sourceArguments)))
+
+ class CustomDownloader:
+ def __call__(self, source):
+ SampleDataTest.customDownloads.append(source)
+
+ CustomDownloaderDataSource = {
+ 'category': "Testing",
+ 'sampleName': 'customDownloader',
+ 'uris': 'http://down.load/test',
+ 'fileNames': 'cust.om',
+ 'customDownloader': CustomDownloader()
}
- self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
- SampleDataLogic.registerCustomSampleDataSource(**sourceArguments, category="Testing")
- self.assertTrue(SampleDataLogic.isSampleDataSourceRegistered("Testing", SampleDataSource(**sourceArguments)))
- self.assertFalse(SampleDataLogic.isSampleDataSourceRegistered("Other", SampleDataSource(**sourceArguments)))
-
- class CustomDownloader:
- def __call__(self, source):
- SampleDataTest.customDownloads.append(source)
-
- CustomDownloaderDataSource = {
- 'category': "Testing",
- 'sampleName': 'customDownloader',
- 'uris': 'http://down.load/test',
- 'fileNames': 'cust.om',
- 'customDownloader': CustomDownloader()
- }
-
- def test_customDownloader(self):
- if not slicer.app.testingEnabled():
- return
- slicer.util.selectModule("SampleData")
- widget = slicer.modules.SampleDataWidget
- button = slicer.util.findChild(widget.parent, 'customDownloaderPushButton')
-
- self.assertEqual(self.customDownloads, [])
-
- button.click()
-
- self.assertEqual(len(self.customDownloads), 1)
- self.assertEqual(self.customDownloads[0].sampleName, 'customDownloader')
-
- def test_categoryForSource(self):
- logic = SampleDataLogic()
- source = slicer.modules.sampleDataSources[logic.builtInCategoryName][0]
- self.assertEqual(logic.categoryForSource(source), logic.builtInCategoryName)
+
+ def test_customDownloader(self):
+ if not slicer.app.testingEnabled():
+ return
+ slicer.util.selectModule("SampleData")
+ widget = slicer.modules.SampleDataWidget
+ button = slicer.util.findChild(widget.parent, 'customDownloaderPushButton')
+
+ self.assertEqual(self.customDownloads, [])
+
+ button.click()
+
+ self.assertEqual(len(self.customDownloads), 1)
+ self.assertEqual(self.customDownloads[0].sampleName, 'customDownloader')
+
+ def test_categoryForSource(self):
+ logic = SampleDataLogic()
+ source = slicer.modules.sampleDataSources[logic.builtInCategoryName][0]
+ self.assertEqual(logic.categoryForSource(source), logic.builtInCategoryName)
diff --git a/Modules/Scripted/ScreenCapture/ScreenCapture.py b/Modules/Scripted/ScreenCapture/ScreenCapture.py
index e20a4cb32a0..35b3afe674d 100644
--- a/Modules/Scripted/ScreenCapture/ScreenCapture.py
+++ b/Modules/Scripted/ScreenCapture/ScreenCapture.py
@@ -14,22 +14,22 @@
#
class ScreenCapture(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Screen Capture"
- self.parent.categories = ["Utilities"]
- self.parent.dependencies = []
- self.parent.contributors = ["Andras Lasso (PerkLab Queen's University)"]
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Screen Capture"
+ self.parent.categories = ["Utilities"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Andras Lasso (PerkLab Queen's University)"]
+ self.parent.helpText = """
This module captures image sequences and videos
from dynamic contents shown in 3D and slice viewers.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This work was was funded by Cancer Care Ontario
and the Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO)
"""
@@ -47,753 +47,753 @@ def __init__(self, parent):
class ScreenCaptureWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- self.logic = ScreenCaptureLogic()
- self.logic.logCallback = self.addLog
- self.viewNodeType = None
- self.animationMode = None
- self.createdOutputFile = None
-
- self.snapshotIndex = 0 # this counter is used for determining file names for single-image snapshots
- self.snapshotOutputDir = None
- self.snapshotFileNamePattern = None
-
- # Instantiate and connect widgets ...
-
- #
- # Input area
- #
- self.inputCollapsibleButton = ctk.ctkCollapsibleButton()
- self.inputCollapsibleButton.text = "Input"
- self.layout.addWidget(self.inputCollapsibleButton)
- inputFormLayout = qt.QFormLayout(self.inputCollapsibleButton)
-
- # Input view selector
- self.viewNodeSelector = slicer.qMRMLNodeComboBox()
- self.viewNodeSelector.nodeTypes = ["vtkMRMLSliceNode", "vtkMRMLViewNode"]
- self.viewNodeSelector.addEnabled = False
- self.viewNodeSelector.removeEnabled = False
- self.viewNodeSelector.noneEnabled = False
- self.viewNodeSelector.showHidden = False
- self.viewNodeSelector.showChildNodeTypes = False
- self.viewNodeSelector.setMRMLScene(slicer.mrmlScene)
- self.viewNodeSelector.setToolTip("This slice or 3D view will be updated during capture."
- "Only this view will be captured unless 'Capture of all views' option in output section is enabled.")
- inputFormLayout.addRow("Master view: ", self.viewNodeSelector)
-
- self.captureAllViewsCheckBox = qt.QCheckBox(" ")
- self.captureAllViewsCheckBox.checked = False
- self.captureAllViewsCheckBox.setToolTip("If checked, all views will be captured. If unchecked then only the selected view will be captured.")
- inputFormLayout.addRow("Capture all views:", self.captureAllViewsCheckBox)
-
- # Mode
- self.animationModeWidget = qt.QComboBox()
- self.animationModeWidget.setToolTip("Select the property that will be adjusted")
- inputFormLayout.addRow("Animation mode:", self.animationModeWidget)
-
- # Slice start offset position
- self.sliceStartOffsetSliderLabel = qt.QLabel("Start sweep offset:")
- self.sliceStartOffsetSliderWidget = ctk.ctkSliderWidget()
- self.sliceStartOffsetSliderWidget.singleStep = 30
- self.sliceStartOffsetSliderWidget.minimum = -100
- self.sliceStartOffsetSliderWidget.maximum = 100
- self.sliceStartOffsetSliderWidget.value = 0
- self.sliceStartOffsetSliderWidget.setToolTip("Start slice sweep offset.")
- inputFormLayout.addRow(self.sliceStartOffsetSliderLabel, self.sliceStartOffsetSliderWidget)
-
- # Slice end offset position
- self.sliceEndOffsetSliderLabel = qt.QLabel("End sweep offset:")
- self.sliceEndOffsetSliderWidget = ctk.ctkSliderWidget()
- self.sliceEndOffsetSliderWidget.singleStep = 5
- self.sliceEndOffsetSliderWidget.minimum = -100
- self.sliceEndOffsetSliderWidget.maximum = 100
- self.sliceEndOffsetSliderWidget.value = 0
- self.sliceEndOffsetSliderWidget.setToolTip("End slice sweep offset.")
- inputFormLayout.addRow(self.sliceEndOffsetSliderLabel, self.sliceEndOffsetSliderWidget)
-
- # 3D rotation range
- self.rotationSliderLabel = qt.QLabel("Rotation range:")
- self.rotationSliderWidget = ctk.ctkRangeWidget()
- self.rotationSliderWidget.singleStep = 5
- self.rotationSliderWidget.minimum = -180
- self.rotationSliderWidget.maximum = 180
- self.rotationSliderWidget.minimumValue = -180
- self.rotationSliderWidget.maximumValue = 180
- self.rotationSliderWidget.setToolTip("View rotation range, relative to current view orientation.")
- inputFormLayout.addRow(self.rotationSliderLabel, self.rotationSliderWidget)
-
- # 3D rotation axis
- self.rotationAxisLabel = qt.QLabel("Rotation axis:")
- self.rotationAxisWidget = ctk.ctkRangeWidget()
- self.rotationAxisWidget = qt.QComboBox()
- self.rotationAxisWidget.addItem("Yaw", AXIS_YAW)
- self.rotationAxisWidget.addItem("Pitch", AXIS_PITCH)
- inputFormLayout.addRow(self.rotationAxisLabel, self.rotationAxisWidget)
-
- # Sequence browser node selector
- self.sequenceBrowserNodeSelectorLabel = qt.QLabel("Sequence:")
- self.sequenceBrowserNodeSelectorWidget = slicer.qMRMLNodeComboBox()
- self.sequenceBrowserNodeSelectorWidget.nodeTypes = ["vtkMRMLSequenceBrowserNode"]
- self.sequenceBrowserNodeSelectorWidget.addEnabled = False
- self.sequenceBrowserNodeSelectorWidget.removeEnabled = False
- self.sequenceBrowserNodeSelectorWidget.noneEnabled = False
- self.sequenceBrowserNodeSelectorWidget.showHidden = False
- self.sequenceBrowserNodeSelectorWidget.setMRMLScene(slicer.mrmlScene)
- self.sequenceBrowserNodeSelectorWidget.setToolTip("Items defined by this sequence browser will be replayed.")
- inputFormLayout.addRow(self.sequenceBrowserNodeSelectorLabel, self.sequenceBrowserNodeSelectorWidget)
-
- # Sequence start index
- self.sequenceStartItemIndexLabel = qt.QLabel("Start index:")
- self.sequenceStartItemIndexWidget = ctk.ctkSliderWidget()
- self.sequenceStartItemIndexWidget.minimum = 0
- self.sequenceStartItemIndexWidget.decimals = 0
- self.sequenceStartItemIndexWidget.setToolTip("First item in the sequence to capture.")
- inputFormLayout.addRow(self.sequenceStartItemIndexLabel, self.sequenceStartItemIndexWidget)
-
- # Sequence end index
- self.sequenceEndItemIndexLabel = qt.QLabel("End index:")
- self.sequenceEndItemIndexWidget = ctk.ctkSliderWidget()
- self.sequenceEndItemIndexWidget.minimum = 0
- self.sequenceEndItemIndexWidget.decimals = 0
- self.sequenceEndItemIndexWidget.setToolTip("Last item in the sequence to capture.")
- inputFormLayout.addRow(self.sequenceEndItemIndexLabel, self.sequenceEndItemIndexWidget)
-
- #
- # Output area
- #
- self.outputCollapsibleButton = ctk.ctkCollapsibleButton()
- self.outputCollapsibleButton.text = "Output"
- self.layout.addWidget(self.outputCollapsibleButton)
- outputFormLayout = qt.QFormLayout(self.outputCollapsibleButton)
-
- self.outputTypeWidget = qt.QComboBox()
- self.outputTypeWidget.setToolTip("Select how captured images will be saved. Video mode requires setting of ffmpeg executable path in Advanced section.")
- self.outputTypeWidget.addItem("image series")
- self.outputTypeWidget.addItem("video")
- self.outputTypeWidget.addItem("lightbox image")
- outputFormLayout.addRow("Output type:", self.outputTypeWidget)
-
- # Number of steps value
- self.numberOfStepsSliderWidget = ctk.ctkSliderWidget()
- self.numberOfStepsSliderWidget.singleStep = 1
- self.numberOfStepsSliderWidget.pageStep = 10
- self.numberOfStepsSliderWidget.minimum = 1
- self.numberOfStepsSliderWidget.maximum = 600
- self.numberOfStepsSliderWidget.value = 31
- self.numberOfStepsSliderWidget.decimals = 0
- self.numberOfStepsSliderWidget.setToolTip("Number of images extracted between start and stop positions.")
-
- # Single step toggle button
- self.singleStepButton = qt.QToolButton()
- self.singleStepButton.setText("single")
- self.singleStepButton.setCheckable(True)
- self.singleStepButton.toolTip = "Capture a single image of current state only.\n" + \
- "New filename is generated for each captured image (no files are overwritten)."
-
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.singleStepButton)
- hbox.addWidget(self.numberOfStepsSliderWidget)
- outputFormLayout.addRow("Number of images:", hbox)
-
- # Output directory selector
- self.outputDirSelector = ctk.ctkPathLineEdit()
- self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
- self.outputDirSelector.settingKey = 'ScreenCaptureOutputDir'
- outputFormLayout.addRow("Output directory:", self.outputDirSelector)
- if not self.outputDirSelector.currentPath:
- defaultOutputPath = os.path.abspath(os.path.join(slicer.app.defaultScenePath, 'SlicerCapture'))
- self.outputDirSelector.setCurrentPath(defaultOutputPath)
-
- self.videoFileNameWidget = qt.QLineEdit()
- self.videoFileNameWidget.setToolTip("String that defines file name and type.")
- self.videoFileNameWidget.text = "SlicerCapture.avi"
- self.videoFileNameWidget.setEnabled(False)
-
- self.lightboxImageFileNameWidget = qt.QLineEdit()
- self.lightboxImageFileNameWidget.setToolTip("String that defines output lightbox file name and type.")
- self.lightboxImageFileNameWidget.text = "SlicerCaptureLightbox.png"
- self.lightboxImageFileNameWidget.setEnabled(False)
-
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.videoFileNameWidget)
- hbox.addWidget(self.lightboxImageFileNameWidget)
- outputFormLayout.addRow("Output file name:", hbox)
-
- self.videoFormatWidget = qt.QComboBox()
- self.videoFormatWidget.enabled = False
- self.videoFormatWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
- for videoFormatPreset in self.logic.videoFormatPresets:
- self.videoFormatWidget.addItem(videoFormatPreset["name"])
- outputFormLayout.addRow("Video format:", self.videoFormatWidget)
-
- self.videoLengthSliderWidget = ctk.ctkSliderWidget()
- self.videoLengthSliderWidget.singleStep = 0.1
- self.videoLengthSliderWidget.minimum = 0.1
- self.videoLengthSliderWidget.maximum = 30
- self.videoLengthSliderWidget.value = 5
- self.videoLengthSliderWidget.suffix = "s"
- self.videoLengthSliderWidget.decimals = 1
- self.videoLengthSliderWidget.setToolTip("Length of the exported video in seconds (without backward steps and repeating).")
- self.videoLengthSliderWidget.setEnabled(False)
- outputFormLayout.addRow("Video length:", self.videoLengthSliderWidget)
-
- self.videoFrameRateSliderWidget = ctk.ctkSliderWidget()
- self.videoFrameRateSliderWidget.singleStep = 0.1
- self.videoFrameRateSliderWidget.minimum = 0.1
- self.videoFrameRateSliderWidget.maximum = 60
- self.videoFrameRateSliderWidget.value = 5.0
- self.videoFrameRateSliderWidget.suffix = "fps"
- self.videoFrameRateSliderWidget.decimals = 3
- self.videoFrameRateSliderWidget.setToolTip("Frame rate in frames per second.")
- self.videoFrameRateSliderWidget.setEnabled(False)
- outputFormLayout.addRow("Video frame rate:", self.videoFrameRateSliderWidget)
-
- #
- # Advanced area
- #
- self.advancedCollapsibleButton = ctk.ctkCollapsibleButton()
- self.advancedCollapsibleButton.text = "Advanced"
- self.advancedCollapsibleButton.collapsed = True
- outputFormLayout.addRow(self.advancedCollapsibleButton)
- advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton)
-
- self.forwardBackwardCheckBox = qt.QCheckBox(" ")
- self.forwardBackwardCheckBox.checked = False
- self.forwardBackwardCheckBox.setToolTip("If checked, image series will be generated playing forward and then backward.")
- advancedFormLayout.addRow("Forward-backward:", self.forwardBackwardCheckBox)
-
- self.repeatSliderWidget = ctk.ctkSliderWidget()
- self.repeatSliderWidget.decimals = 0
- self.repeatSliderWidget.singleStep = 1
- self.repeatSliderWidget.minimum = 1
- self.repeatSliderWidget.maximum = 50
- self.repeatSliderWidget.value = 1
- self.repeatSliderWidget.setToolTip("Number of times image series are repeated. Useful for making short videos longer for playback in software"
- " that does not support looped playback.")
- advancedFormLayout.addRow("Repeat:", self.repeatSliderWidget)
-
- ffmpegPath = self.logic.getFfmpegPath()
- self.ffmpegPathSelector = ctk.ctkPathLineEdit()
- self.ffmpegPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
- self.ffmpegPathSelector.setCurrentPath(ffmpegPath)
- self.ffmpegPathSelector.nameFilters = [self.logic.getFfmpegExecutableFilename()]
- self.ffmpegPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
- self.ffmpegPathSelector.setToolTip("Set the path to ffmpeg executable. Download from: https://www.ffmpeg.org/")
- advancedFormLayout.addRow("ffmpeg executable:", self.ffmpegPathSelector)
-
- self.videoExportFfmpegWarning = qt.QLabel('Set valid ffmpeg executable path! ' +
- 'Help...')
- self.videoExportFfmpegWarning.connect('linkActivated(QString)', self.openURL)
- self.videoExportFfmpegWarning.setVisible(False)
- advancedFormLayout.addRow("", self.videoExportFfmpegWarning)
-
- self.extraVideoOptionsWidget = qt.QLineEdit()
- self.extraVideoOptionsWidget.setToolTip('Additional video conversion options passed to ffmpeg. Parameters -i (input files), -y'
- + '(overwrite without asking), -r (frame rate), -start_number are specified by the module and therefore'
- + 'should not be included in this list.')
- advancedFormLayout.addRow("Video extra options:", self.extraVideoOptionsWidget)
-
- self.fileNamePatternWidget = qt.QLineEdit()
- self.fileNamePatternWidget.setToolTip(
- "String that defines file name, type, and numbering scheme. Default: image%05d.png.")
- self.fileNamePatternWidget.text = "image_%05d.png"
- advancedFormLayout.addRow("Image file name pattern:", self.fileNamePatternWidget)
-
- self.lightboxColumnCountSliderWidget = ctk.ctkSliderWidget()
- self.lightboxColumnCountSliderWidget.decimals = 0
- self.lightboxColumnCountSliderWidget.singleStep = 1
- self.lightboxColumnCountSliderWidget.minimum = 1
- self.lightboxColumnCountSliderWidget.maximum = 20
- self.lightboxColumnCountSliderWidget.value = 6
- self.lightboxColumnCountSliderWidget.setToolTip("Number of columns in lightbox image")
- advancedFormLayout.addRow("Lightbox image columns:", self.lightboxColumnCountSliderWidget)
-
- self.maxFramesWidget = qt.QSpinBox()
- self.maxFramesWidget.setRange(1, 9999)
- self.maxFramesWidget.setValue(600)
- self.maxFramesWidget.setToolTip(
- "Maximum number of images to be captured (without backward steps and repeating).")
- advancedFormLayout.addRow("Maximum number of images:", self.maxFramesWidget)
-
- self.volumeNodeComboBox = slicer.qMRMLNodeComboBox()
- self.volumeNodeComboBox.nodeTypes = ["vtkMRMLVectorVolumeNode"]
- self.volumeNodeComboBox.baseName = "Screenshot"
- self.volumeNodeComboBox.renameEnabled = True
- self.volumeNodeComboBox.noneEnabled = True
- self.volumeNodeComboBox.setToolTip("Select a volume node to store the captured image in the scene instead of just writing immediately to disk. Requires output 'Number of images' to be set to 1.")
- self.volumeNodeComboBox.setMRMLScene(slicer.mrmlScene)
- advancedFormLayout.addRow("Output volume node:", self.volumeNodeComboBox)
-
- self.showViewControllersCheckBox = qt.QCheckBox(" ")
- self.showViewControllersCheckBox.checked = False
- self.showViewControllersCheckBox.setToolTip("If checked, images will be captured with view controllers visible.")
- advancedFormLayout.addRow("View controllers:", self.showViewControllersCheckBox)
-
- self.transparentBackgroundCheckBox = qt.QCheckBox(" ")
- self.transparentBackgroundCheckBox.checked = False
- self.transparentBackgroundCheckBox.setToolTip("If checked, images will be captured with transparent background.")
- advancedFormLayout.addRow("Transparent background:", self.transparentBackgroundCheckBox)
-
- watermarkEnabled = slicer.util.settingsValue('ScreenCapture/WatermarkEnabled', False, converter=slicer.util.toBool)
-
- self.watermarkEnabledCheckBox = qt.QCheckBox(" ")
- self.watermarkEnabledCheckBox.checked = watermarkEnabled
- self.watermarkEnabledCheckBox.setToolTip("If checked, selected watermark image will be added to all exported images.")
-
- self.watermarkPositionWidget = qt.QComboBox()
- self.watermarkPositionWidget.enabled = watermarkEnabled
- self.watermarkPositionWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
- self.watermarkPositionWidget.setToolTip("Add a watermark image to all exported images.")
- for watermarkPositionPreset in self.logic.watermarkPositionPresets:
- self.watermarkPositionWidget.addItem(watermarkPositionPreset["name"])
- self.watermarkPositionWidget.setCurrentText(
- slicer.util.settingsValue('ScreenCapture/WatermarkPosition', self.logic.watermarkPositionPresets[0]["name"]))
-
- self.watermarkSizeSliderWidget = qt.QSpinBox()
- self.watermarkSizeSliderWidget.enabled = watermarkEnabled
- self.watermarkSizeSliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
- self.watermarkSizeSliderWidget.singleStep = 10
- self.watermarkSizeSliderWidget.minimum = 10
- self.watermarkSizeSliderWidget.maximum = 1000
- self.watermarkSizeSliderWidget.value = 100
- self.watermarkSizeSliderWidget.suffix = "%"
- self.watermarkSizeSliderWidget.setToolTip("Size scaling applied to the watermark image. 100% is original size")
- try:
- self.watermarkSizeSliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkSize', 100))
- except:
- pass
-
- self.watermarkOpacitySliderWidget = qt.QSpinBox()
- self.watermarkOpacitySliderWidget.enabled = watermarkEnabled
- self.watermarkOpacitySliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
- self.watermarkOpacitySliderWidget.singleStep = 10
- self.watermarkOpacitySliderWidget.minimum = 0
- self.watermarkOpacitySliderWidget.maximum = 100
- self.watermarkOpacitySliderWidget.value = 30
- self.watermarkOpacitySliderWidget.suffix = "%"
- self.watermarkOpacitySliderWidget.setToolTip("Opacity of the watermark image. 100% is fully opaque.")
- try:
- self.watermarkOpacitySliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkOpacity', 30))
- except:
- pass
-
- self.watermarkPathSelector = ctk.ctkPathLineEdit()
- self.watermarkPathSelector.enabled = watermarkEnabled
- self.watermarkPathSelector.settingKey = 'ScreenCaptureWatermarkImagePath'
- self.watermarkPathSelector.nameFilters = ["*.png"]
- self.watermarkPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
- self.watermarkPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
- self.watermarkPathSelector.setToolTip("Watermark image file in png format")
-
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.watermarkEnabledCheckBox)
- hbox.addWidget(qt.QLabel("Position:"))
- hbox.addWidget(self.watermarkPositionWidget)
- hbox.addWidget(qt.QLabel("Size:"))
- hbox.addWidget(self.watermarkSizeSliderWidget)
- hbox.addWidget(qt.QLabel("Opacity:"))
- hbox.addWidget(self.watermarkOpacitySliderWidget)
- # hbox.addStretch()
- advancedFormLayout.addRow("Watermark image:", hbox)
-
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.watermarkPathSelector)
- advancedFormLayout.addRow("", hbox)
-
- # Capture button
- self.captureButtonLabelCapture = "Capture"
- self.captureButtonLabelCancel = "Cancel"
- self.captureButton = qt.QPushButton(self.captureButtonLabelCapture)
- self.captureButton.toolTip = "Capture slice sweep to image sequence."
- self.showCreatedOutputFileButton = qt.QPushButton()
- self.showCreatedOutputFileButton.setIcon(qt.QIcon(':Icons/Go.png'))
- self.showCreatedOutputFileButton.setMaximumWidth(60)
- self.showCreatedOutputFileButton.enabled = False
- self.showCreatedOutputFileButton.toolTip = "Show created output file."
- hbox = qt.QHBoxLayout()
- hbox.addWidget(self.captureButton)
- hbox.addWidget(self.showCreatedOutputFileButton)
- self.layout.addLayout(hbox)
-
- self.statusLabel = qt.QPlainTextEdit()
- self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
- self.statusLabel.setCenterOnScroll(True)
- self.layout.addWidget(self.statusLabel)
-
- #
- # Add vertical spacer
- # self.layout.addStretch(1)
-
- # connections
- self.captureButton.connect('clicked(bool)', self.onCaptureButton)
- self.showCreatedOutputFileButton.connect('clicked(bool)', self.onShowCreatedOutputFile)
- self.viewNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
- self.animationModeWidget.connect("currentIndexChanged(int)", self.updateViewOptions)
- self.sliceStartOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset)
- self.sliceEndOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset)
- self.sequenceBrowserNodeSelectorWidget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
- self.sequenceStartItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex)
- self.sequenceEndItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex)
- self.outputTypeWidget.connect('currentIndexChanged(int)', self.updateOutputType)
- self.videoFormatWidget.connect("currentIndexChanged(int)", self.updateVideoFormat)
- self.maxFramesWidget.connect('valueChanged(int)', self.maxFramesChanged)
- self.videoLengthSliderWidget.connect('valueChanged(double)', self.setVideoLength)
- self.videoFrameRateSliderWidget.connect('valueChanged(double)', self.setVideoFrameRate)
- self.singleStepButton.connect('toggled(bool)', self.setForceSingleStep)
- self.numberOfStepsSliderWidget.connect('valueChanged(double)', self.setNumberOfSteps)
- self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPositionWidget, 'setEnabled(bool)')
- self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkSizeSliderWidget, 'setEnabled(bool)')
- self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPathSelector, 'setEnabled(bool)')
-
- self.setVideoLength() # update frame rate based on video length
- self.updateOutputType()
- self.updateVideoFormat(0)
- self.updateViewOptions()
-
- def maxFramesChanged(self):
- self.numberOfStepsSliderWidget.maximum = self.maxFramesWidget.value
-
- def openURL(self, URL):
- qt.QDesktopServices().openUrl(qt.QUrl(URL))
- QDesktopServices
-
- def onShowCreatedOutputFile(self):
- if not self.createdOutputFile:
- return
- qt.QDesktopServices().openUrl(qt.QUrl("file:///" + self.createdOutputFile, qt.QUrl.TolerantMode))
-
- def updateOutputType(self, selectionIndex=0):
- isVideo = self.outputTypeWidget.currentText == "video"
- isLightbox = self.outputTypeWidget.currentText == "lightbox image"
- self.fileNamePatternWidget.enabled = not (isVideo or isLightbox)
- self.videoFileNameWidget.enabled = isVideo
- self.videoFormatWidget.enabled = isVideo
- self.videoLengthSliderWidget.enabled = isVideo
- self.videoFrameRateSliderWidget.enabled = isVideo
- self.videoFileNameWidget.setVisible(not isLightbox)
- self.videoFileNameWidget.enabled = isVideo
- self.lightboxImageFileNameWidget.setVisible(isLightbox)
- self.lightboxImageFileNameWidget.enabled = isLightbox
-
- def updateVideoFormat(self, selectionIndex):
- videoFormatPreset = self.logic.videoFormatPresets[selectionIndex]
-
- import os
- filenameExt = os.path.splitext(self.videoFileNameWidget.text)
- self.videoFileNameWidget.text = filenameExt[0] + "." + videoFormatPreset["fileExtension"]
-
- self.extraVideoOptionsWidget.text = videoFormatPreset["extraVideoOptions"]
-
- def currentViewNodeType(self):
- viewNode = self.viewNodeSelector.currentNode()
- if not viewNode:
- return None
- elif viewNode.IsA("vtkMRMLSliceNode"):
- return VIEW_SLICE
- elif viewNode.IsA("vtkMRMLViewNode"):
- return VIEW_3D
- else:
- return None
-
- def addLog(self, text):
- """Append text to log window
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.statusLabel.appendPlainText(text)
- self.statusLabel.ensureCursorVisible()
- slicer.app.processEvents() # force update
-
- def cleanup(self):
- pass
-
- def updateViewOptions(self):
-
- sequencesModuleAvailable = hasattr(slicer.modules, 'sequences')
-
- if self.viewNodeType != self.currentViewNodeType():
- self.viewNodeType = self.currentViewNodeType()
-
- self.animationModeWidget.clear()
- if self.viewNodeType == VIEW_SLICE:
- self.animationModeWidget.addItem("slice sweep")
- self.animationModeWidget.addItem("slice fade")
- if self.viewNodeType == VIEW_3D:
- self.animationModeWidget.addItem("3D rotation")
- if sequencesModuleAvailable:
- self.animationModeWidget.addItem("sequence")
-
- if self.animationMode != self.animationModeWidget.currentText:
- self.animationMode = self.animationModeWidget.currentText
-
- # slice sweep
- self.sliceStartOffsetSliderLabel.visible = (self.animationMode == "slice sweep")
- self.sliceStartOffsetSliderWidget.visible = (self.animationMode == "slice sweep")
- self.sliceEndOffsetSliderLabel.visible = (self.animationMode == "slice sweep")
- self.sliceEndOffsetSliderWidget.visible = (self.animationMode == "slice sweep")
- if self.animationMode == "slice sweep":
- offsetResolution = self.logic.getSliceOffsetResolution(self.viewNodeSelector.currentNode())
- sliceOffsetMin, sliceOffsetMax = self.logic.getSliceOffsetRange(self.viewNodeSelector.currentNode())
-
- wasBlocked = self.sliceStartOffsetSliderWidget.blockSignals(True)
- self.sliceStartOffsetSliderWidget.singleStep = offsetResolution
- self.sliceStartOffsetSliderWidget.minimum = sliceOffsetMin
- self.sliceStartOffsetSliderWidget.maximum = sliceOffsetMax
- self.sliceStartOffsetSliderWidget.value = sliceOffsetMin
- self.sliceStartOffsetSliderWidget.blockSignals(wasBlocked)
-
- wasBlocked = self.sliceEndOffsetSliderWidget.blockSignals(True)
- self.sliceEndOffsetSliderWidget.singleStep = offsetResolution
- self.sliceEndOffsetSliderWidget.minimum = sliceOffsetMin
- self.sliceEndOffsetSliderWidget.maximum = sliceOffsetMax
- self.sliceEndOffsetSliderWidget.value = sliceOffsetMax
- self.sliceEndOffsetSliderWidget.blockSignals(wasBlocked)
-
- # 3D rotation
- self.rotationSliderLabel.visible = (self.animationMode == "3D rotation")
- self.rotationSliderWidget.visible = (self.animationMode == "3D rotation")
- self.rotationAxisLabel.visible = (self.animationMode == "3D rotation")
- self.rotationAxisWidget.visible = (self.animationMode == "3D rotation")
-
- # Sequence
- self.sequenceBrowserNodeSelectorLabel.visible = (self.animationMode == "sequence")
- self.sequenceBrowserNodeSelectorWidget.visible = (self.animationMode == "sequence")
- self.sequenceStartItemIndexLabel.visible = (self.animationMode == "sequence")
- self.sequenceStartItemIndexWidget.visible = (self.animationMode == "sequence")
- self.sequenceEndItemIndexLabel.visible = (self.animationMode == "sequence")
- self.sequenceEndItemIndexWidget.visible = (self.animationMode == "sequence")
- if self.animationMode == "sequence":
- sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()
-
- sequenceItemCount = 0
- if sequenceBrowserNode and sequenceBrowserNode.GetMasterSequenceNode():
- sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
-
- if sequenceItemCount > 0:
- wasBlocked = self.sequenceStartItemIndexWidget.blockSignals(True)
- self.sequenceStartItemIndexWidget.maximum = sequenceItemCount - 1
- self.sequenceStartItemIndexWidget.value = 0
- self.sequenceStartItemIndexWidget.blockSignals(wasBlocked)
-
- wasBlocked = self.sequenceEndItemIndexWidget.blockSignals(True)
- self.sequenceEndItemIndexWidget.maximum = sequenceItemCount - 1
- self.sequenceEndItemIndexWidget.value = sequenceItemCount - 1
- self.sequenceEndItemIndexWidget.blockSignals(wasBlocked)
-
- self.sequenceStartItemIndexWidget.enabled = sequenceItemCount > 0
- self.sequenceEndItemIndexWidget.enabled = sequenceItemCount > 0
-
- numberOfSteps = int(self.numberOfStepsSliderWidget.value)
- forceSingleStep = self.singleStepButton.checked
- if forceSingleStep:
- numberOfSteps = 1
- self.numberOfStepsSliderWidget.setDisabled(forceSingleStep)
- self.forwardBackwardCheckBox.enabled = (numberOfSteps > 1)
- self.repeatSliderWidget.enabled = (numberOfSteps > 1)
- self.volumeNodeComboBox.setEnabled(numberOfSteps == 1)
-
- def setSliceOffset(self, offset):
- sliceLogic = self.logic.getSliceLogicFromSliceNode(self.viewNodeSelector.currentNode())
- sliceLogic.SetSliceOffset(offset)
-
- def setVideoLength(self, lengthSec=None):
- wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
- self.videoFrameRateSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoLengthSliderWidget.value
- self.videoFrameRateSliderWidget.blockSignals(wasBlocked)
-
- def setVideoFrameRate(self, frameRateFps):
- wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
- self.videoLengthSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoFrameRateSliderWidget.value
- self.videoFrameRateSliderWidget.blockSignals(wasBlocked)
-
- def setNumberOfSteps(self, steps):
- self.setVideoLength()
- self.updateViewOptions()
-
- def setForceSingleStep(self, force):
- self.updateViewOptions()
-
- def setSequenceItemIndex(self, index):
- sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()
- sequenceBrowserNode.SetSelectedItemNumber(int(index))
-
- def enableInputOutputWidgets(self, enable):
- self.inputCollapsibleButton.setEnabled(enable)
- self.outputCollapsibleButton.setEnabled(enable)
-
- def onCaptureButton(self):
-
- # Disable capture button to prevent multiple clicks
- self.captureButton.setEnabled(False)
- self.enableInputOutputWidgets(False)
- slicer.app.processEvents()
-
- if self.captureButton.text == self.captureButtonLabelCancel:
- self.logic.requestCancel()
- return
-
- self.logic.setFfmpegPath(self.ffmpegPathSelector.currentPath)
-
- qt.QSettings().setValue('ScreenCapture/WatermarkEnabled', bool(self.watermarkEnabledCheckBox.checked))
- qt.QSettings().setValue('ScreenCapture/WatermarkPosition', self.watermarkPositionWidget.currentText)
- qt.QSettings().setValue('ScreenCapture/WatermarkOpacity', self.watermarkOpacitySliderWidget.value)
- qt.QSettings().setValue('ScreenCapture/WatermarkSize', self.watermarkSizeSliderWidget.value)
-
- if self.watermarkEnabledCheckBox.checked:
- if self.watermarkPathSelector.currentPath:
- self.logic.setWatermarkImagePath(self.watermarkPathSelector.currentPath)
- self.watermarkPathSelector.addCurrentPathToHistory()
- else:
- self.logic.setWatermarkImagePath(self.resourcePath('SlicerWatermark.png'))
- self.logic.setWatermarkPosition(self.watermarkPositionWidget.currentIndex)
- self.logic.setWatermarkSizePercent(self.watermarkSizeSliderWidget.value)
- self.logic.setWatermarkOpacityPercent(self.watermarkOpacitySliderWidget.value)
- else:
- self.logic.setWatermarkPosition(-1)
-
- self.statusLabel.plainText = ''
-
- videoOutputRequested = (self.outputTypeWidget.currentText == "video")
- viewNode = self.viewNodeSelector.currentNode()
- numberOfSteps = int(self.numberOfStepsSliderWidget.value)
- if self.singleStepButton.checked:
- numberOfSteps = 1
- if numberOfSteps < 2:
- # If a single image is selected
- videoOutputRequested = False
- outputDir = self.outputDirSelector.currentPath
- self.outputDirSelector.addCurrentPathToHistory()
-
- self.videoExportFfmpegWarning.setVisible(False)
- if videoOutputRequested:
- if not self.logic.isFfmpegPathValid():
- # ffmpeg not found, try to automatically find it at common locations
- self.logic.findFfmpeg()
- if not self.logic.isFfmpegPathValid() and os.name == 'nt': # TODO: implement download for Linux/MacOS?
- # ffmpeg not found, offer downloading it
- if slicer.util.confirmOkCancelDisplay(
- 'Video encoder not detected on your system. '
- 'Download ffmpeg video encoder?',
- windowTitle='Download confirmation'):
- if not self.logic.ffmpegDownload():
- slicer.util.errorDisplay("ffmpeg download failed")
- if not self.logic.isFfmpegPathValid():
- # still not found, user has to specify path manually
- self.videoExportFfmpegWarning.setVisible(True)
- self.advancedCollapsibleButton.collapsed = False
- self.captureButton.setEnabled(True)
- self.enableInputOutputWidgets(True)
- return
- self.ffmpegPathSelector.currentPath = self.logic.getFfmpegPath()
-
- # Need to create a new random file pattern if video output is requested to make sure that new image files are not mixed up with
- # existing files in the output directory
- imageFileNamePattern = self.fileNamePatternWidget.text if (self.outputTypeWidget.currentText == "image series") else self.logic.getRandomFilePattern()
-
- self.captureButton.setEnabled(True)
- self.captureButton.text = self.captureButtonLabelCancel
- slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
- captureAllViews = self.captureAllViewsCheckBox.checked
- transparentBackground = self.transparentBackgroundCheckBox.checked
- showViewControllers = self.showViewControllersCheckBox.checked
- if captureAllViews:
- self.logic.showViewControllers(showViewControllers)
- elif showViewControllers:
- logging.warning("View controllers are only available to be shown when capturing all views.")
- try:
- if numberOfSteps < 2:
- if imageFileNamePattern != self.snapshotFileNamePattern or outputDir != self.snapshotOutputDir:
- self.snapshotIndex = 0
- if outputDir:
- [filename, self.snapshotIndex] = self.logic.getNextAvailableFileName(outputDir, imageFileNamePattern, self.snapshotIndex)
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ self.logic = ScreenCaptureLogic()
+ self.logic.logCallback = self.addLog
+ self.viewNodeType = None
+ self.animationMode = None
+ self.createdOutputFile = None
+
+ self.snapshotIndex = 0 # this counter is used for determining file names for single-image snapshots
+ self.snapshotOutputDir = None
+ self.snapshotFileNamePattern = None
+
+ # Instantiate and connect widgets ...
+
+ #
+ # Input area
+ #
+ self.inputCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.inputCollapsibleButton.text = "Input"
+ self.layout.addWidget(self.inputCollapsibleButton)
+ inputFormLayout = qt.QFormLayout(self.inputCollapsibleButton)
+
+ # Input view selector
+ self.viewNodeSelector = slicer.qMRMLNodeComboBox()
+ self.viewNodeSelector.nodeTypes = ["vtkMRMLSliceNode", "vtkMRMLViewNode"]
+ self.viewNodeSelector.addEnabled = False
+ self.viewNodeSelector.removeEnabled = False
+ self.viewNodeSelector.noneEnabled = False
+ self.viewNodeSelector.showHidden = False
+ self.viewNodeSelector.showChildNodeTypes = False
+ self.viewNodeSelector.setMRMLScene(slicer.mrmlScene)
+ self.viewNodeSelector.setToolTip("This slice or 3D view will be updated during capture."
+ "Only this view will be captured unless 'Capture of all views' option in output section is enabled.")
+ inputFormLayout.addRow("Master view: ", self.viewNodeSelector)
+
+ self.captureAllViewsCheckBox = qt.QCheckBox(" ")
+ self.captureAllViewsCheckBox.checked = False
+ self.captureAllViewsCheckBox.setToolTip("If checked, all views will be captured. If unchecked then only the selected view will be captured.")
+ inputFormLayout.addRow("Capture all views:", self.captureAllViewsCheckBox)
+
+ # Mode
+ self.animationModeWidget = qt.QComboBox()
+ self.animationModeWidget.setToolTip("Select the property that will be adjusted")
+ inputFormLayout.addRow("Animation mode:", self.animationModeWidget)
+
+ # Slice start offset position
+ self.sliceStartOffsetSliderLabel = qt.QLabel("Start sweep offset:")
+ self.sliceStartOffsetSliderWidget = ctk.ctkSliderWidget()
+ self.sliceStartOffsetSliderWidget.singleStep = 30
+ self.sliceStartOffsetSliderWidget.minimum = -100
+ self.sliceStartOffsetSliderWidget.maximum = 100
+ self.sliceStartOffsetSliderWidget.value = 0
+ self.sliceStartOffsetSliderWidget.setToolTip("Start slice sweep offset.")
+ inputFormLayout.addRow(self.sliceStartOffsetSliderLabel, self.sliceStartOffsetSliderWidget)
+
+ # Slice end offset position
+ self.sliceEndOffsetSliderLabel = qt.QLabel("End sweep offset:")
+ self.sliceEndOffsetSliderWidget = ctk.ctkSliderWidget()
+ self.sliceEndOffsetSliderWidget.singleStep = 5
+ self.sliceEndOffsetSliderWidget.minimum = -100
+ self.sliceEndOffsetSliderWidget.maximum = 100
+ self.sliceEndOffsetSliderWidget.value = 0
+ self.sliceEndOffsetSliderWidget.setToolTip("End slice sweep offset.")
+ inputFormLayout.addRow(self.sliceEndOffsetSliderLabel, self.sliceEndOffsetSliderWidget)
+
+ # 3D rotation range
+ self.rotationSliderLabel = qt.QLabel("Rotation range:")
+ self.rotationSliderWidget = ctk.ctkRangeWidget()
+ self.rotationSliderWidget.singleStep = 5
+ self.rotationSliderWidget.minimum = -180
+ self.rotationSliderWidget.maximum = 180
+ self.rotationSliderWidget.minimumValue = -180
+ self.rotationSliderWidget.maximumValue = 180
+ self.rotationSliderWidget.setToolTip("View rotation range, relative to current view orientation.")
+ inputFormLayout.addRow(self.rotationSliderLabel, self.rotationSliderWidget)
+
+ # 3D rotation axis
+ self.rotationAxisLabel = qt.QLabel("Rotation axis:")
+ self.rotationAxisWidget = ctk.ctkRangeWidget()
+ self.rotationAxisWidget = qt.QComboBox()
+ self.rotationAxisWidget.addItem("Yaw", AXIS_YAW)
+ self.rotationAxisWidget.addItem("Pitch", AXIS_PITCH)
+ inputFormLayout.addRow(self.rotationAxisLabel, self.rotationAxisWidget)
+
+ # Sequence browser node selector
+ self.sequenceBrowserNodeSelectorLabel = qt.QLabel("Sequence:")
+ self.sequenceBrowserNodeSelectorWidget = slicer.qMRMLNodeComboBox()
+ self.sequenceBrowserNodeSelectorWidget.nodeTypes = ["vtkMRMLSequenceBrowserNode"]
+ self.sequenceBrowserNodeSelectorWidget.addEnabled = False
+ self.sequenceBrowserNodeSelectorWidget.removeEnabled = False
+ self.sequenceBrowserNodeSelectorWidget.noneEnabled = False
+ self.sequenceBrowserNodeSelectorWidget.showHidden = False
+ self.sequenceBrowserNodeSelectorWidget.setMRMLScene(slicer.mrmlScene)
+ self.sequenceBrowserNodeSelectorWidget.setToolTip("Items defined by this sequence browser will be replayed.")
+ inputFormLayout.addRow(self.sequenceBrowserNodeSelectorLabel, self.sequenceBrowserNodeSelectorWidget)
+
+ # Sequence start index
+ self.sequenceStartItemIndexLabel = qt.QLabel("Start index:")
+ self.sequenceStartItemIndexWidget = ctk.ctkSliderWidget()
+ self.sequenceStartItemIndexWidget.minimum = 0
+ self.sequenceStartItemIndexWidget.decimals = 0
+ self.sequenceStartItemIndexWidget.setToolTip("First item in the sequence to capture.")
+ inputFormLayout.addRow(self.sequenceStartItemIndexLabel, self.sequenceStartItemIndexWidget)
+
+ # Sequence end index
+ self.sequenceEndItemIndexLabel = qt.QLabel("End index:")
+ self.sequenceEndItemIndexWidget = ctk.ctkSliderWidget()
+ self.sequenceEndItemIndexWidget.minimum = 0
+ self.sequenceEndItemIndexWidget.decimals = 0
+ self.sequenceEndItemIndexWidget.setToolTip("Last item in the sequence to capture.")
+ inputFormLayout.addRow(self.sequenceEndItemIndexLabel, self.sequenceEndItemIndexWidget)
+
+ #
+ # Output area
+ #
+ self.outputCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.outputCollapsibleButton.text = "Output"
+ self.layout.addWidget(self.outputCollapsibleButton)
+ outputFormLayout = qt.QFormLayout(self.outputCollapsibleButton)
+
+ self.outputTypeWidget = qt.QComboBox()
+ self.outputTypeWidget.setToolTip("Select how captured images will be saved. Video mode requires setting of ffmpeg executable path in Advanced section.")
+ self.outputTypeWidget.addItem("image series")
+ self.outputTypeWidget.addItem("video")
+ self.outputTypeWidget.addItem("lightbox image")
+ outputFormLayout.addRow("Output type:", self.outputTypeWidget)
+
+ # Number of steps value
+ self.numberOfStepsSliderWidget = ctk.ctkSliderWidget()
+ self.numberOfStepsSliderWidget.singleStep = 1
+ self.numberOfStepsSliderWidget.pageStep = 10
+ self.numberOfStepsSliderWidget.minimum = 1
+ self.numberOfStepsSliderWidget.maximum = 600
+ self.numberOfStepsSliderWidget.value = 31
+ self.numberOfStepsSliderWidget.decimals = 0
+ self.numberOfStepsSliderWidget.setToolTip("Number of images extracted between start and stop positions.")
+
+ # Single step toggle button
+ self.singleStepButton = qt.QToolButton()
+ self.singleStepButton.setText("single")
+ self.singleStepButton.setCheckable(True)
+ self.singleStepButton.toolTip = "Capture a single image of current state only.\n" + \
+ "New filename is generated for each captured image (no files are overwritten)."
+
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.singleStepButton)
+ hbox.addWidget(self.numberOfStepsSliderWidget)
+ outputFormLayout.addRow("Number of images:", hbox)
+
+ # Output directory selector
+ self.outputDirSelector = ctk.ctkPathLineEdit()
+ self.outputDirSelector.filters = ctk.ctkPathLineEdit.Dirs
+ self.outputDirSelector.settingKey = 'ScreenCaptureOutputDir'
+ outputFormLayout.addRow("Output directory:", self.outputDirSelector)
+ if not self.outputDirSelector.currentPath:
+ defaultOutputPath = os.path.abspath(os.path.join(slicer.app.defaultScenePath, 'SlicerCapture'))
+ self.outputDirSelector.setCurrentPath(defaultOutputPath)
+
+ self.videoFileNameWidget = qt.QLineEdit()
+ self.videoFileNameWidget.setToolTip("String that defines file name and type.")
+ self.videoFileNameWidget.text = "SlicerCapture.avi"
+ self.videoFileNameWidget.setEnabled(False)
+
+ self.lightboxImageFileNameWidget = qt.QLineEdit()
+ self.lightboxImageFileNameWidget.setToolTip("String that defines output lightbox file name and type.")
+ self.lightboxImageFileNameWidget.text = "SlicerCaptureLightbox.png"
+ self.lightboxImageFileNameWidget.setEnabled(False)
+
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.videoFileNameWidget)
+ hbox.addWidget(self.lightboxImageFileNameWidget)
+ outputFormLayout.addRow("Output file name:", hbox)
+
+ self.videoFormatWidget = qt.QComboBox()
+ self.videoFormatWidget.enabled = False
+ self.videoFormatWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
+ for videoFormatPreset in self.logic.videoFormatPresets:
+ self.videoFormatWidget.addItem(videoFormatPreset["name"])
+ outputFormLayout.addRow("Video format:", self.videoFormatWidget)
+
+ self.videoLengthSliderWidget = ctk.ctkSliderWidget()
+ self.videoLengthSliderWidget.singleStep = 0.1
+ self.videoLengthSliderWidget.minimum = 0.1
+ self.videoLengthSliderWidget.maximum = 30
+ self.videoLengthSliderWidget.value = 5
+ self.videoLengthSliderWidget.suffix = "s"
+ self.videoLengthSliderWidget.decimals = 1
+ self.videoLengthSliderWidget.setToolTip("Length of the exported video in seconds (without backward steps and repeating).")
+ self.videoLengthSliderWidget.setEnabled(False)
+ outputFormLayout.addRow("Video length:", self.videoLengthSliderWidget)
+
+ self.videoFrameRateSliderWidget = ctk.ctkSliderWidget()
+ self.videoFrameRateSliderWidget.singleStep = 0.1
+ self.videoFrameRateSliderWidget.minimum = 0.1
+ self.videoFrameRateSliderWidget.maximum = 60
+ self.videoFrameRateSliderWidget.value = 5.0
+ self.videoFrameRateSliderWidget.suffix = "fps"
+ self.videoFrameRateSliderWidget.decimals = 3
+ self.videoFrameRateSliderWidget.setToolTip("Frame rate in frames per second.")
+ self.videoFrameRateSliderWidget.setEnabled(False)
+ outputFormLayout.addRow("Video frame rate:", self.videoFrameRateSliderWidget)
+
+ #
+ # Advanced area
+ #
+ self.advancedCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.advancedCollapsibleButton.text = "Advanced"
+ self.advancedCollapsibleButton.collapsed = True
+ outputFormLayout.addRow(self.advancedCollapsibleButton)
+ advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton)
+
+ self.forwardBackwardCheckBox = qt.QCheckBox(" ")
+ self.forwardBackwardCheckBox.checked = False
+ self.forwardBackwardCheckBox.setToolTip("If checked, image series will be generated playing forward and then backward.")
+ advancedFormLayout.addRow("Forward-backward:", self.forwardBackwardCheckBox)
+
+ self.repeatSliderWidget = ctk.ctkSliderWidget()
+ self.repeatSliderWidget.decimals = 0
+ self.repeatSliderWidget.singleStep = 1
+ self.repeatSliderWidget.minimum = 1
+ self.repeatSliderWidget.maximum = 50
+ self.repeatSliderWidget.value = 1
+ self.repeatSliderWidget.setToolTip("Number of times image series are repeated. Useful for making short videos longer for playback in software"
+ " that does not support looped playback.")
+ advancedFormLayout.addRow("Repeat:", self.repeatSliderWidget)
+
+ ffmpegPath = self.logic.getFfmpegPath()
+ self.ffmpegPathSelector = ctk.ctkPathLineEdit()
+ self.ffmpegPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
+ self.ffmpegPathSelector.setCurrentPath(ffmpegPath)
+ self.ffmpegPathSelector.nameFilters = [self.logic.getFfmpegExecutableFilename()]
+ self.ffmpegPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
+ self.ffmpegPathSelector.setToolTip("Set the path to ffmpeg executable. Download from: https://www.ffmpeg.org/")
+ advancedFormLayout.addRow("ffmpeg executable:", self.ffmpegPathSelector)
+
+ self.videoExportFfmpegWarning = qt.QLabel('Set valid ffmpeg executable path! ' +
+ 'Help...')
+ self.videoExportFfmpegWarning.connect('linkActivated(QString)', self.openURL)
+ self.videoExportFfmpegWarning.setVisible(False)
+ advancedFormLayout.addRow("", self.videoExportFfmpegWarning)
+
+ self.extraVideoOptionsWidget = qt.QLineEdit()
+ self.extraVideoOptionsWidget.setToolTip('Additional video conversion options passed to ffmpeg. Parameters -i (input files), -y'
+ + '(overwrite without asking), -r (frame rate), -start_number are specified by the module and therefore'
+ + 'should not be included in this list.')
+ advancedFormLayout.addRow("Video extra options:", self.extraVideoOptionsWidget)
+
+ self.fileNamePatternWidget = qt.QLineEdit()
+ self.fileNamePatternWidget.setToolTip(
+ "String that defines file name, type, and numbering scheme. Default: image%05d.png.")
+ self.fileNamePatternWidget.text = "image_%05d.png"
+ advancedFormLayout.addRow("Image file name pattern:", self.fileNamePatternWidget)
+
+ self.lightboxColumnCountSliderWidget = ctk.ctkSliderWidget()
+ self.lightboxColumnCountSliderWidget.decimals = 0
+ self.lightboxColumnCountSliderWidget.singleStep = 1
+ self.lightboxColumnCountSliderWidget.minimum = 1
+ self.lightboxColumnCountSliderWidget.maximum = 20
+ self.lightboxColumnCountSliderWidget.value = 6
+ self.lightboxColumnCountSliderWidget.setToolTip("Number of columns in lightbox image")
+ advancedFormLayout.addRow("Lightbox image columns:", self.lightboxColumnCountSliderWidget)
+
+ self.maxFramesWidget = qt.QSpinBox()
+ self.maxFramesWidget.setRange(1, 9999)
+ self.maxFramesWidget.setValue(600)
+ self.maxFramesWidget.setToolTip(
+ "Maximum number of images to be captured (without backward steps and repeating).")
+ advancedFormLayout.addRow("Maximum number of images:", self.maxFramesWidget)
+
+ self.volumeNodeComboBox = slicer.qMRMLNodeComboBox()
+ self.volumeNodeComboBox.nodeTypes = ["vtkMRMLVectorVolumeNode"]
+ self.volumeNodeComboBox.baseName = "Screenshot"
+ self.volumeNodeComboBox.renameEnabled = True
+ self.volumeNodeComboBox.noneEnabled = True
+ self.volumeNodeComboBox.setToolTip("Select a volume node to store the captured image in the scene instead of just writing immediately to disk. Requires output 'Number of images' to be set to 1.")
+ self.volumeNodeComboBox.setMRMLScene(slicer.mrmlScene)
+ advancedFormLayout.addRow("Output volume node:", self.volumeNodeComboBox)
+
+ self.showViewControllersCheckBox = qt.QCheckBox(" ")
+ self.showViewControllersCheckBox.checked = False
+ self.showViewControllersCheckBox.setToolTip("If checked, images will be captured with view controllers visible.")
+ advancedFormLayout.addRow("View controllers:", self.showViewControllersCheckBox)
+
+ self.transparentBackgroundCheckBox = qt.QCheckBox(" ")
+ self.transparentBackgroundCheckBox.checked = False
+ self.transparentBackgroundCheckBox.setToolTip("If checked, images will be captured with transparent background.")
+ advancedFormLayout.addRow("Transparent background:", self.transparentBackgroundCheckBox)
+
+ watermarkEnabled = slicer.util.settingsValue('ScreenCapture/WatermarkEnabled', False, converter=slicer.util.toBool)
+
+ self.watermarkEnabledCheckBox = qt.QCheckBox(" ")
+ self.watermarkEnabledCheckBox.checked = watermarkEnabled
+ self.watermarkEnabledCheckBox.setToolTip("If checked, selected watermark image will be added to all exported images.")
+
+ self.watermarkPositionWidget = qt.QComboBox()
+ self.watermarkPositionWidget.enabled = watermarkEnabled
+ self.watermarkPositionWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
+ self.watermarkPositionWidget.setToolTip("Add a watermark image to all exported images.")
+ for watermarkPositionPreset in self.logic.watermarkPositionPresets:
+ self.watermarkPositionWidget.addItem(watermarkPositionPreset["name"])
+ self.watermarkPositionWidget.setCurrentText(
+ slicer.util.settingsValue('ScreenCapture/WatermarkPosition', self.logic.watermarkPositionPresets[0]["name"]))
+
+ self.watermarkSizeSliderWidget = qt.QSpinBox()
+ self.watermarkSizeSliderWidget.enabled = watermarkEnabled
+ self.watermarkSizeSliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
+ self.watermarkSizeSliderWidget.singleStep = 10
+ self.watermarkSizeSliderWidget.minimum = 10
+ self.watermarkSizeSliderWidget.maximum = 1000
+ self.watermarkSizeSliderWidget.value = 100
+ self.watermarkSizeSliderWidget.suffix = "%"
+ self.watermarkSizeSliderWidget.setToolTip("Size scaling applied to the watermark image. 100% is original size")
+ try:
+ self.watermarkSizeSliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkSize', 100))
+ except:
+ pass
+
+ self.watermarkOpacitySliderWidget = qt.QSpinBox()
+ self.watermarkOpacitySliderWidget.enabled = watermarkEnabled
+ self.watermarkOpacitySliderWidget.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
+ self.watermarkOpacitySliderWidget.singleStep = 10
+ self.watermarkOpacitySliderWidget.minimum = 0
+ self.watermarkOpacitySliderWidget.maximum = 100
+ self.watermarkOpacitySliderWidget.value = 30
+ self.watermarkOpacitySliderWidget.suffix = "%"
+ self.watermarkOpacitySliderWidget.setToolTip("Opacity of the watermark image. 100% is fully opaque.")
+ try:
+ self.watermarkOpacitySliderWidget.value = int(slicer.util.settingsValue('ScreenCapture/WatermarkOpacity', 30))
+ except:
+ pass
+
+ self.watermarkPathSelector = ctk.ctkPathLineEdit()
+ self.watermarkPathSelector.enabled = watermarkEnabled
+ self.watermarkPathSelector.settingKey = 'ScreenCaptureWatermarkImagePath'
+ self.watermarkPathSelector.nameFilters = ["*.png"]
+ self.watermarkPathSelector.sizeAdjustPolicy = ctk.ctkPathLineEdit.AdjustToMinimumContentsLength
+ self.watermarkPathSelector.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Preferred)
+ self.watermarkPathSelector.setToolTip("Watermark image file in png format")
+
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.watermarkEnabledCheckBox)
+ hbox.addWidget(qt.QLabel("Position:"))
+ hbox.addWidget(self.watermarkPositionWidget)
+ hbox.addWidget(qt.QLabel("Size:"))
+ hbox.addWidget(self.watermarkSizeSliderWidget)
+ hbox.addWidget(qt.QLabel("Opacity:"))
+ hbox.addWidget(self.watermarkOpacitySliderWidget)
+ # hbox.addStretch()
+ advancedFormLayout.addRow("Watermark image:", hbox)
+
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.watermarkPathSelector)
+ advancedFormLayout.addRow("", hbox)
+
+ # Capture button
+ self.captureButtonLabelCapture = "Capture"
+ self.captureButtonLabelCancel = "Cancel"
+ self.captureButton = qt.QPushButton(self.captureButtonLabelCapture)
+ self.captureButton.toolTip = "Capture slice sweep to image sequence."
+ self.showCreatedOutputFileButton = qt.QPushButton()
+ self.showCreatedOutputFileButton.setIcon(qt.QIcon(':Icons/Go.png'))
+ self.showCreatedOutputFileButton.setMaximumWidth(60)
+ self.showCreatedOutputFileButton.enabled = False
+ self.showCreatedOutputFileButton.toolTip = "Show created output file."
+ hbox = qt.QHBoxLayout()
+ hbox.addWidget(self.captureButton)
+ hbox.addWidget(self.showCreatedOutputFileButton)
+ self.layout.addLayout(hbox)
+
+ self.statusLabel = qt.QPlainTextEdit()
+ self.statusLabel.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
+ self.statusLabel.setCenterOnScroll(True)
+ self.layout.addWidget(self.statusLabel)
+
+ #
+ # Add vertical spacer
+ # self.layout.addStretch(1)
+
+ # connections
+ self.captureButton.connect('clicked(bool)', self.onCaptureButton)
+ self.showCreatedOutputFileButton.connect('clicked(bool)', self.onShowCreatedOutputFile)
+ self.viewNodeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
+ self.animationModeWidget.connect("currentIndexChanged(int)", self.updateViewOptions)
+ self.sliceStartOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset)
+ self.sliceEndOffsetSliderWidget.connect('valueChanged(double)', self.setSliceOffset)
+ self.sequenceBrowserNodeSelectorWidget.connect("currentNodeChanged(vtkMRMLNode*)", self.updateViewOptions)
+ self.sequenceStartItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex)
+ self.sequenceEndItemIndexWidget.connect('valueChanged(double)', self.setSequenceItemIndex)
+ self.outputTypeWidget.connect('currentIndexChanged(int)', self.updateOutputType)
+ self.videoFormatWidget.connect("currentIndexChanged(int)", self.updateVideoFormat)
+ self.maxFramesWidget.connect('valueChanged(int)', self.maxFramesChanged)
+ self.videoLengthSliderWidget.connect('valueChanged(double)', self.setVideoLength)
+ self.videoFrameRateSliderWidget.connect('valueChanged(double)', self.setVideoFrameRate)
+ self.singleStepButton.connect('toggled(bool)', self.setForceSingleStep)
+ self.numberOfStepsSliderWidget.connect('valueChanged(double)', self.setNumberOfSteps)
+ self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPositionWidget, 'setEnabled(bool)')
+ self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkSizeSliderWidget, 'setEnabled(bool)')
+ self.watermarkEnabledCheckBox.connect('toggled(bool)', self.watermarkPathSelector, 'setEnabled(bool)')
+
+ self.setVideoLength() # update frame rate based on video length
+ self.updateOutputType()
+ self.updateVideoFormat(0)
+ self.updateViewOptions()
+
+ def maxFramesChanged(self):
+ self.numberOfStepsSliderWidget.maximum = self.maxFramesWidget.value
+
+ def openURL(self, URL):
+ qt.QDesktopServices().openUrl(qt.QUrl(URL))
+ QDesktopServices
+
+ def onShowCreatedOutputFile(self):
+ if not self.createdOutputFile:
+ return
+ qt.QDesktopServices().openUrl(qt.QUrl("file:///" + self.createdOutputFile, qt.QUrl.TolerantMode))
+
+ def updateOutputType(self, selectionIndex=0):
+ isVideo = self.outputTypeWidget.currentText == "video"
+ isLightbox = self.outputTypeWidget.currentText == "lightbox image"
+ self.fileNamePatternWidget.enabled = not (isVideo or isLightbox)
+ self.videoFileNameWidget.enabled = isVideo
+ self.videoFormatWidget.enabled = isVideo
+ self.videoLengthSliderWidget.enabled = isVideo
+ self.videoFrameRateSliderWidget.enabled = isVideo
+ self.videoFileNameWidget.setVisible(not isLightbox)
+ self.videoFileNameWidget.enabled = isVideo
+ self.lightboxImageFileNameWidget.setVisible(isLightbox)
+ self.lightboxImageFileNameWidget.enabled = isLightbox
+
+ def updateVideoFormat(self, selectionIndex):
+ videoFormatPreset = self.logic.videoFormatPresets[selectionIndex]
+
+ import os
+ filenameExt = os.path.splitext(self.videoFileNameWidget.text)
+ self.videoFileNameWidget.text = filenameExt[0] + "." + videoFormatPreset["fileExtension"]
+
+ self.extraVideoOptionsWidget.text = videoFormatPreset["extraVideoOptions"]
+
+ def currentViewNodeType(self):
+ viewNode = self.viewNodeSelector.currentNode()
+ if not viewNode:
+ return None
+ elif viewNode.IsA("vtkMRMLSliceNode"):
+ return VIEW_SLICE
+ elif viewNode.IsA("vtkMRMLViewNode"):
+ return VIEW_3D
else:
- filename = None
- view = None if captureAllViews else self.logic.viewFromNode(viewNode)
- volumeNode = None if numberOfSteps > 1 else self.volumeNodeComboBox.currentNode()
- self.logic.captureImageFromView(view, filename, transparentBackground, volumeNode=volumeNode)
- if filename:
- self.logic.addLog("Write " + filename)
- if volumeNode:
- self.logic.addLog(f"Write to volume node '{volumeNode.GetName()}'")
- elif self.animationModeWidget.currentText == "slice sweep":
- self.logic.captureSliceSweep(viewNode, self.sliceStartOffsetSliderWidget.value,
- self.sliceEndOffsetSliderWidget.value, numberOfSteps, outputDir, imageFileNamePattern,
- captureAllViews=captureAllViews, transparentBackground=transparentBackground)
- elif self.animationModeWidget.currentText == "slice fade":
- self.logic.captureSliceFade(viewNode, numberOfSteps, outputDir, imageFileNamePattern,
- captureAllViews=captureAllViews, transparentBackground=transparentBackground)
- elif self.animationModeWidget.currentText == "3D rotation":
- self.logic.capture3dViewRotation(viewNode, self.rotationSliderWidget.minimumValue,
- self.rotationSliderWidget.maximumValue, numberOfSteps,
- self.rotationAxisWidget.itemData(self.rotationAxisWidget.currentIndex),
- outputDir, imageFileNamePattern,
- captureAllViews=captureAllViews, transparentBackground=transparentBackground)
- elif self.animationModeWidget.currentText == "sequence":
- self.logic.captureSequence(viewNode, self.sequenceBrowserNodeSelectorWidget.currentNode(),
- self.sequenceStartItemIndexWidget.value, self.sequenceEndItemIndexWidget.value,
- numberOfSteps, outputDir, imageFileNamePattern,
- captureAllViews=captureAllViews, transparentBackground=transparentBackground)
- else:
- raise ValueError('Unsupported view node type.')
-
- import shutil
-
- fps = self.videoFrameRateSliderWidget.value
-
- if numberOfSteps > 1:
- forwardBackward = self.forwardBackwardCheckBox.checked
- numberOfRepeats = int(self.repeatSliderWidget.value)
- filePathPattern = os.path.join(outputDir, imageFileNamePattern)
- fileIndex = numberOfSteps
- for repeatIndex in range(numberOfRepeats):
- if forwardBackward:
- for step in reversed(range(1, numberOfSteps - 1)):
- sourceFilename = filePathPattern % step
- destinationFilename = filePathPattern % fileIndex
- self.logic.addLog("Copy to " + destinationFilename)
- shutil.copyfile(sourceFilename, destinationFilename)
- fileIndex += 1
- if repeatIndex < numberOfRepeats - 1:
- for step in range(numberOfSteps):
- sourceFilename = filePathPattern % step
- destinationFilename = filePathPattern % fileIndex
- self.logic.addLog("Copy to " + destinationFilename)
- shutil.copyfile(sourceFilename, destinationFilename)
- fileIndex += 1
- if forwardBackward and (numberOfSteps > 2):
- numberOfSteps += numberOfSteps - 2
- numberOfSteps *= numberOfRepeats
-
- try:
+ return None
+
+ def addLog(self, text):
+ """Append text to log window
+ """
+ self.statusLabel.appendPlainText(text)
+ self.statusLabel.ensureCursorVisible()
+ slicer.app.processEvents() # force update
+
+ def cleanup(self):
+ pass
+
+ def updateViewOptions(self):
+
+ sequencesModuleAvailable = hasattr(slicer.modules, 'sequences')
+
+ if self.viewNodeType != self.currentViewNodeType():
+ self.viewNodeType = self.currentViewNodeType()
+
+ self.animationModeWidget.clear()
+ if self.viewNodeType == VIEW_SLICE:
+ self.animationModeWidget.addItem("slice sweep")
+ self.animationModeWidget.addItem("slice fade")
+ if self.viewNodeType == VIEW_3D:
+ self.animationModeWidget.addItem("3D rotation")
+ if sequencesModuleAvailable:
+ self.animationModeWidget.addItem("sequence")
+
+ if self.animationMode != self.animationModeWidget.currentText:
+ self.animationMode = self.animationModeWidget.currentText
+
+ # slice sweep
+ self.sliceStartOffsetSliderLabel.visible = (self.animationMode == "slice sweep")
+ self.sliceStartOffsetSliderWidget.visible = (self.animationMode == "slice sweep")
+ self.sliceEndOffsetSliderLabel.visible = (self.animationMode == "slice sweep")
+ self.sliceEndOffsetSliderWidget.visible = (self.animationMode == "slice sweep")
+ if self.animationMode == "slice sweep":
+ offsetResolution = self.logic.getSliceOffsetResolution(self.viewNodeSelector.currentNode())
+ sliceOffsetMin, sliceOffsetMax = self.logic.getSliceOffsetRange(self.viewNodeSelector.currentNode())
+
+ wasBlocked = self.sliceStartOffsetSliderWidget.blockSignals(True)
+ self.sliceStartOffsetSliderWidget.singleStep = offsetResolution
+ self.sliceStartOffsetSliderWidget.minimum = sliceOffsetMin
+ self.sliceStartOffsetSliderWidget.maximum = sliceOffsetMax
+ self.sliceStartOffsetSliderWidget.value = sliceOffsetMin
+ self.sliceStartOffsetSliderWidget.blockSignals(wasBlocked)
+
+ wasBlocked = self.sliceEndOffsetSliderWidget.blockSignals(True)
+ self.sliceEndOffsetSliderWidget.singleStep = offsetResolution
+ self.sliceEndOffsetSliderWidget.minimum = sliceOffsetMin
+ self.sliceEndOffsetSliderWidget.maximum = sliceOffsetMax
+ self.sliceEndOffsetSliderWidget.value = sliceOffsetMax
+ self.sliceEndOffsetSliderWidget.blockSignals(wasBlocked)
+
+ # 3D rotation
+ self.rotationSliderLabel.visible = (self.animationMode == "3D rotation")
+ self.rotationSliderWidget.visible = (self.animationMode == "3D rotation")
+ self.rotationAxisLabel.visible = (self.animationMode == "3D rotation")
+ self.rotationAxisWidget.visible = (self.animationMode == "3D rotation")
+
+ # Sequence
+ self.sequenceBrowserNodeSelectorLabel.visible = (self.animationMode == "sequence")
+ self.sequenceBrowserNodeSelectorWidget.visible = (self.animationMode == "sequence")
+ self.sequenceStartItemIndexLabel.visible = (self.animationMode == "sequence")
+ self.sequenceStartItemIndexWidget.visible = (self.animationMode == "sequence")
+ self.sequenceEndItemIndexLabel.visible = (self.animationMode == "sequence")
+ self.sequenceEndItemIndexWidget.visible = (self.animationMode == "sequence")
+ if self.animationMode == "sequence":
+ sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()
+
+ sequenceItemCount = 0
+ if sequenceBrowserNode and sequenceBrowserNode.GetMasterSequenceNode():
+ sequenceItemCount = sequenceBrowserNode.GetMasterSequenceNode().GetNumberOfDataNodes()
+
+ if sequenceItemCount > 0:
+ wasBlocked = self.sequenceStartItemIndexWidget.blockSignals(True)
+ self.sequenceStartItemIndexWidget.maximum = sequenceItemCount - 1
+ self.sequenceStartItemIndexWidget.value = 0
+ self.sequenceStartItemIndexWidget.blockSignals(wasBlocked)
+
+ wasBlocked = self.sequenceEndItemIndexWidget.blockSignals(True)
+ self.sequenceEndItemIndexWidget.maximum = sequenceItemCount - 1
+ self.sequenceEndItemIndexWidget.value = sequenceItemCount - 1
+ self.sequenceEndItemIndexWidget.blockSignals(wasBlocked)
+
+ self.sequenceStartItemIndexWidget.enabled = sequenceItemCount > 0
+ self.sequenceEndItemIndexWidget.enabled = sequenceItemCount > 0
+
+ numberOfSteps = int(self.numberOfStepsSliderWidget.value)
+ forceSingleStep = self.singleStepButton.checked
+ if forceSingleStep:
+ numberOfSteps = 1
+ self.numberOfStepsSliderWidget.setDisabled(forceSingleStep)
+ self.forwardBackwardCheckBox.enabled = (numberOfSteps > 1)
+ self.repeatSliderWidget.enabled = (numberOfSteps > 1)
+ self.volumeNodeComboBox.setEnabled(numberOfSteps == 1)
+
+ def setSliceOffset(self, offset):
+ sliceLogic = self.logic.getSliceLogicFromSliceNode(self.viewNodeSelector.currentNode())
+ sliceLogic.SetSliceOffset(offset)
+
+ def setVideoLength(self, lengthSec=None):
+ wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
+ self.videoFrameRateSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoLengthSliderWidget.value
+ self.videoFrameRateSliderWidget.blockSignals(wasBlocked)
+
+ def setVideoFrameRate(self, frameRateFps):
+ wasBlocked = self.videoFrameRateSliderWidget.blockSignals(True)
+ self.videoLengthSliderWidget.value = self.numberOfStepsSliderWidget.value / self.videoFrameRateSliderWidget.value
+ self.videoFrameRateSliderWidget.blockSignals(wasBlocked)
+
+ def setNumberOfSteps(self, steps):
+ self.setVideoLength()
+ self.updateViewOptions()
+
+ def setForceSingleStep(self, force):
+ self.updateViewOptions()
+
+ def setSequenceItemIndex(self, index):
+ sequenceBrowserNode = self.sequenceBrowserNodeSelectorWidget.currentNode()
+ sequenceBrowserNode.SetSelectedItemNumber(int(index))
+
+ def enableInputOutputWidgets(self, enable):
+ self.inputCollapsibleButton.setEnabled(enable)
+ self.outputCollapsibleButton.setEnabled(enable)
+
+ def onCaptureButton(self):
+
+ # Disable capture button to prevent multiple clicks
+ self.captureButton.setEnabled(False)
+ self.enableInputOutputWidgets(False)
+ slicer.app.processEvents()
+
+ if self.captureButton.text == self.captureButtonLabelCancel:
+ self.logic.requestCancel()
+ return
+
+ self.logic.setFfmpegPath(self.ffmpegPathSelector.currentPath)
+
+ qt.QSettings().setValue('ScreenCapture/WatermarkEnabled', bool(self.watermarkEnabledCheckBox.checked))
+ qt.QSettings().setValue('ScreenCapture/WatermarkPosition', self.watermarkPositionWidget.currentText)
+ qt.QSettings().setValue('ScreenCapture/WatermarkOpacity', self.watermarkOpacitySliderWidget.value)
+ qt.QSettings().setValue('ScreenCapture/WatermarkSize', self.watermarkSizeSliderWidget.value)
+
+ if self.watermarkEnabledCheckBox.checked:
+ if self.watermarkPathSelector.currentPath:
+ self.logic.setWatermarkImagePath(self.watermarkPathSelector.currentPath)
+ self.watermarkPathSelector.addCurrentPathToHistory()
+ else:
+ self.logic.setWatermarkImagePath(self.resourcePath('SlicerWatermark.png'))
+ self.logic.setWatermarkPosition(self.watermarkPositionWidget.currentIndex)
+ self.logic.setWatermarkSizePercent(self.watermarkSizeSliderWidget.value)
+ self.logic.setWatermarkOpacityPercent(self.watermarkOpacitySliderWidget.value)
+ else:
+ self.logic.setWatermarkPosition(-1)
+
+ self.statusLabel.plainText = ''
+
+ videoOutputRequested = (self.outputTypeWidget.currentText == "video")
+ viewNode = self.viewNodeSelector.currentNode()
+ numberOfSteps = int(self.numberOfStepsSliderWidget.value)
+ if self.singleStepButton.checked:
+ numberOfSteps = 1
+ if numberOfSteps < 2:
+ # If a single image is selected
+ videoOutputRequested = False
+ outputDir = self.outputDirSelector.currentPath
+ self.outputDirSelector.addCurrentPathToHistory()
+
+ self.videoExportFfmpegWarning.setVisible(False)
if videoOutputRequested:
- self.logic.createVideo(fps, self.extraVideoOptionsWidget.text,
- outputDir, imageFileNamePattern, self.videoFileNameWidget.text)
- elif (self.outputTypeWidget.currentText == "lightbox image"):
- self.logic.createLightboxImage(int(self.lightboxColumnCountSliderWidget.value),
- outputDir, imageFileNamePattern, numberOfSteps, self.lightboxImageFileNameWidget.text)
- finally:
- if not self.outputTypeWidget.currentText == "image series":
- self.logic.deleteTemporaryFiles(outputDir, imageFileNamePattern, numberOfSteps)
-
- self.addLog("Done.")
- self.createdOutputFile = os.path.join(outputDir, self.videoFileNameWidget.text) if videoOutputRequested else outputDir
- self.showCreatedOutputFileButton.enabled = True
- except Exception as e:
- self.addLog(f"Error: {str(e)}")
- import traceback
- traceback.print_exc()
- self.showCreatedOutputFileButton.enabled = False
- self.createdOutputFile = None
- if captureAllViews:
- self.logic.showViewControllers(True)
- slicer.app.restoreOverrideCursor()
- self.captureButton.text = self.captureButtonLabelCapture
- self.captureButton.setEnabled(True)
- self.enableInputOutputWidgets(True)
+ if not self.logic.isFfmpegPathValid():
+ # ffmpeg not found, try to automatically find it at common locations
+ self.logic.findFfmpeg()
+ if not self.logic.isFfmpegPathValid() and os.name == 'nt': # TODO: implement download for Linux/MacOS?
+ # ffmpeg not found, offer downloading it
+ if slicer.util.confirmOkCancelDisplay(
+ 'Video encoder not detected on your system. '
+ 'Download ffmpeg video encoder?',
+ windowTitle='Download confirmation'):
+ if not self.logic.ffmpegDownload():
+ slicer.util.errorDisplay("ffmpeg download failed")
+ if not self.logic.isFfmpegPathValid():
+ # still not found, user has to specify path manually
+ self.videoExportFfmpegWarning.setVisible(True)
+ self.advancedCollapsibleButton.collapsed = False
+ self.captureButton.setEnabled(True)
+ self.enableInputOutputWidgets(True)
+ return
+ self.ffmpegPathSelector.currentPath = self.logic.getFfmpegPath()
+
+ # Need to create a new random file pattern if video output is requested to make sure that new image files are not mixed up with
+ # existing files in the output directory
+ imageFileNamePattern = self.fileNamePatternWidget.text if (self.outputTypeWidget.currentText == "image series") else self.logic.getRandomFilePattern()
+
+ self.captureButton.setEnabled(True)
+ self.captureButton.text = self.captureButtonLabelCancel
+ slicer.app.setOverrideCursor(qt.Qt.WaitCursor)
+ captureAllViews = self.captureAllViewsCheckBox.checked
+ transparentBackground = self.transparentBackgroundCheckBox.checked
+ showViewControllers = self.showViewControllersCheckBox.checked
+ if captureAllViews:
+ self.logic.showViewControllers(showViewControllers)
+ elif showViewControllers:
+ logging.warning("View controllers are only available to be shown when capturing all views.")
+ try:
+ if numberOfSteps < 2:
+ if imageFileNamePattern != self.snapshotFileNamePattern or outputDir != self.snapshotOutputDir:
+ self.snapshotIndex = 0
+ if outputDir:
+ [filename, self.snapshotIndex] = self.logic.getNextAvailableFileName(outputDir, imageFileNamePattern, self.snapshotIndex)
+ else:
+ filename = None
+ view = None if captureAllViews else self.logic.viewFromNode(viewNode)
+ volumeNode = None if numberOfSteps > 1 else self.volumeNodeComboBox.currentNode()
+ self.logic.captureImageFromView(view, filename, transparentBackground, volumeNode=volumeNode)
+ if filename:
+ self.logic.addLog("Write " + filename)
+ if volumeNode:
+ self.logic.addLog(f"Write to volume node '{volumeNode.GetName()}'")
+ elif self.animationModeWidget.currentText == "slice sweep":
+ self.logic.captureSliceSweep(viewNode, self.sliceStartOffsetSliderWidget.value,
+ self.sliceEndOffsetSliderWidget.value, numberOfSteps, outputDir, imageFileNamePattern,
+ captureAllViews=captureAllViews, transparentBackground=transparentBackground)
+ elif self.animationModeWidget.currentText == "slice fade":
+ self.logic.captureSliceFade(viewNode, numberOfSteps, outputDir, imageFileNamePattern,
+ captureAllViews=captureAllViews, transparentBackground=transparentBackground)
+ elif self.animationModeWidget.currentText == "3D rotation":
+ self.logic.capture3dViewRotation(viewNode, self.rotationSliderWidget.minimumValue,
+ self.rotationSliderWidget.maximumValue, numberOfSteps,
+ self.rotationAxisWidget.itemData(self.rotationAxisWidget.currentIndex),
+ outputDir, imageFileNamePattern,
+ captureAllViews=captureAllViews, transparentBackground=transparentBackground)
+ elif self.animationModeWidget.currentText == "sequence":
+ self.logic.captureSequence(viewNode, self.sequenceBrowserNodeSelectorWidget.currentNode(),
+ self.sequenceStartItemIndexWidget.value, self.sequenceEndItemIndexWidget.value,
+ numberOfSteps, outputDir, imageFileNamePattern,
+ captureAllViews=captureAllViews, transparentBackground=transparentBackground)
+ else:
+ raise ValueError('Unsupported view node type.')
+
+ import shutil
+
+ fps = self.videoFrameRateSliderWidget.value
+
+ if numberOfSteps > 1:
+ forwardBackward = self.forwardBackwardCheckBox.checked
+ numberOfRepeats = int(self.repeatSliderWidget.value)
+ filePathPattern = os.path.join(outputDir, imageFileNamePattern)
+ fileIndex = numberOfSteps
+ for repeatIndex in range(numberOfRepeats):
+ if forwardBackward:
+ for step in reversed(range(1, numberOfSteps - 1)):
+ sourceFilename = filePathPattern % step
+ destinationFilename = filePathPattern % fileIndex
+ self.logic.addLog("Copy to " + destinationFilename)
+ shutil.copyfile(sourceFilename, destinationFilename)
+ fileIndex += 1
+ if repeatIndex < numberOfRepeats - 1:
+ for step in range(numberOfSteps):
+ sourceFilename = filePathPattern % step
+ destinationFilename = filePathPattern % fileIndex
+ self.logic.addLog("Copy to " + destinationFilename)
+ shutil.copyfile(sourceFilename, destinationFilename)
+ fileIndex += 1
+ if forwardBackward and (numberOfSteps > 2):
+ numberOfSteps += numberOfSteps - 2
+ numberOfSteps *= numberOfRepeats
+
+ try:
+ if videoOutputRequested:
+ self.logic.createVideo(fps, self.extraVideoOptionsWidget.text,
+ outputDir, imageFileNamePattern, self.videoFileNameWidget.text)
+ elif (self.outputTypeWidget.currentText == "lightbox image"):
+ self.logic.createLightboxImage(int(self.lightboxColumnCountSliderWidget.value),
+ outputDir, imageFileNamePattern, numberOfSteps, self.lightboxImageFileNameWidget.text)
+ finally:
+ if not self.outputTypeWidget.currentText == "image series":
+ self.logic.deleteTemporaryFiles(outputDir, imageFileNamePattern, numberOfSteps)
+
+ self.addLog("Done.")
+ self.createdOutputFile = os.path.join(outputDir, self.videoFileNameWidget.text) if videoOutputRequested else outputDir
+ self.showCreatedOutputFileButton.enabled = True
+ except Exception as e:
+ self.addLog(f"Error: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ self.showCreatedOutputFileButton.enabled = False
+ self.createdOutputFile = None
+ if captureAllViews:
+ self.logic.showViewControllers(True)
+ slicer.app.restoreOverrideCursor()
+ self.captureButton.text = self.captureButtonLabelCapture
+ self.captureButton.setEnabled(True)
+ self.enableInputOutputWidgets(True)
#
@@ -801,758 +801,758 @@ def onCaptureButton(self):
#
class ScreenCaptureLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self):
- self.logCallback = None
- self.cancelRequested = False
-
- self.videoFormatPresets = [
- {"name": "H.264", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -pix_fmt yuv420p"},
- {"name": "H.264 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -crf 18 -pix_fmt yuv420p"},
- {"name": "MPEG-4", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 5"},
- {"name": "MPEG-4 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 3"},
- {"name": "Animated GIF", "fileExtension": "gif", "extraVideoOptions": "-filter_complex palettegen,[v]paletteuse"},
- {"name": "Animated GIF (grayscale)", "fileExtension": "gif", "extraVideoOptions": "-vf format=gray"}]
-
- self.watermarkPositionPresets = [
- {"name": "bottom-left", "position": lambda capturedImageSize, watermarkSize, spacing: [-2, -2]},
- {"name": "bottom-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
- -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -2]},
- {"name": "top-left", "position": lambda capturedImageSize, watermarkSize, spacing: [
- -2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]},
- {"name": "top-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
- -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]}]
-
- self.watermarkPosition = -1
- self.watermarkSizePercent = 100
- self.watermarkOpacityPercent = 100
- self.watermarkImagePath = None
-
- def requestCancel(self):
- logging.info("User requested cancelling of capture")
- self.cancelRequested = True
-
- def addLog(self, text):
- logging.info(text)
- if self.logCallback:
- self.logCallback(text)
-
- def showViewControllers(self, show):
- slicer.util.setViewControllersVisible(show)
-
- def getRandomFilePattern(self):
- import string
- import random
- numberOfRandomChars = 5
- randomString = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(numberOfRandomChars))
- filePathPattern = "tmp-" + randomString + "-%05d.png"
- return filePathPattern
-
- def isFfmpegPathValid(self):
- import os
- ffmpegPath = self.getFfmpegPath()
- return os.path.isfile(ffmpegPath)
-
- def getDownloadedFfmpegDirectory(self):
- return os.path.dirname(slicer.app.slicerUserSettingsFilePath) + '/ffmpeg'
-
- def getFfmpegExecutableFilename(self):
- if os.name == 'nt':
- return 'ffmpeg.exe'
- else:
- return 'ffmpeg'
-
- def findFfmpeg(self):
- # Try to find the executable at specific paths
- commonFfmpegPaths = [
- '/usr/local/bin/ffmpeg',
- '/usr/bin/ffmpeg'
- ]
- for ffmpegPath in commonFfmpegPaths:
- if os.path.isfile(ffmpegPath):
- # found one
- self.setFfmpegPath(ffmpegPath)
- return True
- # Search for the executable in directories
- commonFfmpegDirs = [
- self.getDownloadedFfmpegDirectory()
- ]
- for ffmpegDir in commonFfmpegDirs:
- if self.findFfmpegInDirectory(ffmpegDir):
- # found it
- return True
- # Not found
- return False
-
- def findFfmpegInDirectory(self, ffmpegDir):
- ffmpegExecutableFilename = self.getFfmpegExecutableFilename()
- for dirpath, dirnames, files in os.walk(ffmpegDir):
- for name in files:
- if name == ffmpegExecutableFilename:
- ffmpegExecutablePath = (dirpath + '/' + name).replace('\\', '/')
- self.setFfmpegPath(ffmpegExecutablePath)
- return True
- return False
-
- def unzipFfmpeg(self, filePath, ffmpegTargetDirectory):
- if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
- logging.info('ffmpeg package is not found at ' + filePath)
- return False
-
- logging.info('Unzipping ffmpeg package ' + filePath)
- qt.QDir().mkpath(ffmpegTargetDirectory)
- slicer.app.applicationLogic().Unzip(filePath, ffmpegTargetDirectory)
- success = self.findFfmpegInDirectory(ffmpegTargetDirectory)
- return success
-
- def ffmpegDownload(self):
- ffmpegTargetDirectory = self.getDownloadedFfmpegDirectory()
- # The number in the filePath can be incremented each time a significantly different ffmpeg version
- # is to be introduced (it prevents reusing a previously downloaded package).
- filePath = slicer.app.temporaryPath + '/ffmpeg-package-slicer-01.zip'
- success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory)
- if success:
- # there was a valid downloaded package already
- return True
-
- # List of mirror sites to attempt download ffmpeg pre-built binaries from
- urls = []
- if os.name == 'nt':
- urls.append('https://github.com/Slicer/SlicerBinaryDependencies/releases/download/ffmpeg/ffmpeg-2021-05-16-win64.zip')
- else:
- # TODO: implement downloading for Linux/MacOS?
- pass
-
- success = False
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- for url in urls:
-
- success = True
- try:
- logging.info('Requesting download ffmpeg from %s...' % url)
- import urllib.request, urllib.error, urllib.parse
- req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
- data = urllib.request.urlopen(req).read()
- with open(filePath, "wb") as f:
- f.write(data)
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+ def __init__(self):
+ self.logCallback = None
+ self.cancelRequested = False
+
+ self.videoFormatPresets = [
+ {"name": "H.264", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -pix_fmt yuv420p"},
+ {"name": "H.264 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec libx264 -preset slower -crf 18 -pix_fmt yuv420p"},
+ {"name": "MPEG-4", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 5"},
+ {"name": "MPEG-4 (high-quality)", "fileExtension": "mp4", "extraVideoOptions": "-codec mpeg4 -qscale 3"},
+ {"name": "Animated GIF", "fileExtension": "gif", "extraVideoOptions": "-filter_complex palettegen,[v]paletteuse"},
+ {"name": "Animated GIF (grayscale)", "fileExtension": "gif", "extraVideoOptions": "-vf format=gray"}]
+
+ self.watermarkPositionPresets = [
+ {"name": "bottom-left", "position": lambda capturedImageSize, watermarkSize, spacing: [-2, -2]},
+ {"name": "bottom-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
+ -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -2]},
+ {"name": "top-left", "position": lambda capturedImageSize, watermarkSize, spacing: [
+ -2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]},
+ {"name": "top-right", "position": lambda capturedImageSize, watermarkSize, spacing: [
+ -capturedImageSize[0] * spacing + watermarkSize[0] + 2, -capturedImageSize[1] * spacing + watermarkSize[1] + 2]}]
+
+ self.watermarkPosition = -1
+ self.watermarkSizePercent = 100
+ self.watermarkOpacityPercent = 100
+ self.watermarkImagePath = None
+
+ def requestCancel(self):
+ logging.info("User requested cancelling of capture")
+ self.cancelRequested = True
+
+ def addLog(self, text):
+ logging.info(text)
+ if self.logCallback:
+ self.logCallback(text)
+
+ def showViewControllers(self, show):
+ slicer.util.setViewControllersVisible(show)
+
+ def getRandomFilePattern(self):
+ import string
+ import random
+ numberOfRandomChars = 5
+ randomString = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(numberOfRandomChars))
+ filePathPattern = "tmp-" + randomString + "-%05d.png"
+ return filePathPattern
+
+ def isFfmpegPathValid(self):
+ import os
+ ffmpegPath = self.getFfmpegPath()
+ return os.path.isfile(ffmpegPath)
+
+ def getDownloadedFfmpegDirectory(self):
+ return os.path.dirname(slicer.app.slicerUserSettingsFilePath) + '/ffmpeg'
+
+ def getFfmpegExecutableFilename(self):
+ if os.name == 'nt':
+ return 'ffmpeg.exe'
+ else:
+ return 'ffmpeg'
+
+ def findFfmpeg(self):
+ # Try to find the executable at specific paths
+ commonFfmpegPaths = [
+ '/usr/local/bin/ffmpeg',
+ '/usr/bin/ffmpeg'
+ ]
+ for ffmpegPath in commonFfmpegPaths:
+ if os.path.isfile(ffmpegPath):
+ # found one
+ self.setFfmpegPath(ffmpegPath)
+ return True
+ # Search for the executable in directories
+ commonFfmpegDirs = [
+ self.getDownloadedFfmpegDirectory()
+ ]
+ for ffmpegDir in commonFfmpegDirs:
+ if self.findFfmpegInDirectory(ffmpegDir):
+ # found it
+ return True
+ # Not found
+ return False
+
+ def findFfmpegInDirectory(self, ffmpegDir):
+ ffmpegExecutableFilename = self.getFfmpegExecutableFilename()
+ for dirpath, dirnames, files in os.walk(ffmpegDir):
+ for name in files:
+ if name == ffmpegExecutableFilename:
+ ffmpegExecutablePath = (dirpath + '/' + name).replace('\\', '/')
+ self.setFfmpegPath(ffmpegExecutablePath)
+ return True
+ return False
+
+ def unzipFfmpeg(self, filePath, ffmpegTargetDirectory):
+ if not os.path.exists(filePath) or os.stat(filePath).st_size == 0:
+ logging.info('ffmpeg package is not found at ' + filePath)
+ return False
+
+ logging.info('Unzipping ffmpeg package ' + filePath)
+ qt.QDir().mkpath(ffmpegTargetDirectory)
+ slicer.app.applicationLogic().Unzip(filePath, ffmpegTargetDirectory)
+ success = self.findFfmpegInDirectory(ffmpegTargetDirectory)
+ return success
+
+ def ffmpegDownload(self):
+ ffmpegTargetDirectory = self.getDownloadedFfmpegDirectory()
+ # The number in the filePath can be incremented each time a significantly different ffmpeg version
+ # is to be introduced (it prevents reusing a previously downloaded package).
+ filePath = slicer.app.temporaryPath + '/ffmpeg-package-slicer-01.zip'
success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory)
- except:
- success = False
-
- if success:
- break
-
- qt.QApplication.restoreOverrideCursor()
- return success
-
- def getFfmpegPath(self):
- settings = qt.QSettings()
- if settings.contains('General/ffmpegPath'):
- return slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath'))
- return ''
-
- def setFfmpegPath(self, ffmpegPath):
- # don't save it if already saved
- settings = qt.QSettings()
- if settings.contains('General/ffmpegPath'):
- if ffmpegPath == slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')):
- return
- settings.setValue('General/ffmpegPath', slicer.app.toSlicerHomeRelativePath(ffmpegPath))
-
- def setWatermarkPosition(self, watermarkPosition):
- self.watermarkPosition = watermarkPosition
-
- def setWatermarkSizePercent(self, watermarkSizePercent):
- self.watermarkSizePercent = watermarkSizePercent
-
- def setWatermarkOpacityPercent(self, watermarkOpacityPercent):
- self.watermarkOpacityPercent = watermarkOpacityPercent
-
- def setWatermarkImagePath(self, watermarkImagePath):
- self.watermarkImagePath = watermarkImagePath
+ if success:
+ # there was a valid downloaded package already
+ return True
+
+ # List of mirror sites to attempt download ffmpeg pre-built binaries from
+ urls = []
+ if os.name == 'nt':
+ urls.append('https://github.com/Slicer/SlicerBinaryDependencies/releases/download/ffmpeg/ffmpeg-2021-05-16-win64.zip')
+ else:
+ # TODO: implement downloading for Linux/MacOS?
+ pass
- def getSliceLogicFromSliceNode(self, sliceNode):
- lm = slicer.app.layoutManager()
- sliceLogic = lm.sliceWidget(sliceNode.GetLayoutName()).sliceLogic()
- return sliceLogic
+ success = False
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ for url in urls:
+
+ success = True
+ try:
+ logging.info('Requesting download ffmpeg from %s...' % url)
+ import urllib.request, urllib.error, urllib.parse
+ req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
+ data = urllib.request.urlopen(req).read()
+ with open(filePath, "wb") as f:
+ f.write(data)
+
+ success = self.unzipFfmpeg(filePath, ffmpegTargetDirectory)
+ except:
+ success = False
+
+ if success:
+ break
+
+ qt.QApplication.restoreOverrideCursor()
+ return success
+
+ def getFfmpegPath(self):
+ settings = qt.QSettings()
+ if settings.contains('General/ffmpegPath'):
+ return slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath'))
+ return ''
+
+ def setFfmpegPath(self, ffmpegPath):
+ # don't save it if already saved
+ settings = qt.QSettings()
+ if settings.contains('General/ffmpegPath'):
+ if ffmpegPath == slicer.app.toSlicerHomeAbsolutePath(settings.value('General/ffmpegPath')):
+ return
+ settings.setValue('General/ffmpegPath', slicer.app.toSlicerHomeRelativePath(ffmpegPath))
+
+ def setWatermarkPosition(self, watermarkPosition):
+ self.watermarkPosition = watermarkPosition
+
+ def setWatermarkSizePercent(self, watermarkSizePercent):
+ self.watermarkSizePercent = watermarkSizePercent
+
+ def setWatermarkOpacityPercent(self, watermarkOpacityPercent):
+ self.watermarkOpacityPercent = watermarkOpacityPercent
+
+ def setWatermarkImagePath(self, watermarkImagePath):
+ self.watermarkImagePath = watermarkImagePath
+
+ def getSliceLogicFromSliceNode(self, sliceNode):
+ lm = slicer.app.layoutManager()
+ sliceLogic = lm.sliceWidget(sliceNode.GetLayoutName()).sliceLogic()
+ return sliceLogic
+
+ def getSliceOffsetRange(self, sliceNode):
+ sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+
+ sliceBounds = [0, -1, 0, -1, 0, -1]
+ sliceLogic.GetLowestVolumeSliceBounds(sliceBounds)
+ sliceOffsetMin = sliceBounds[4]
+ sliceOffsetMax = sliceBounds[5]
+
+ # increase range if it is empty
+ # to allow capturing even when no volumes are shown in slice views
+ if sliceOffsetMin == sliceOffsetMax:
+ sliceOffsetMin = sliceLogic.GetSliceOffset() - 100
+ sliceOffsetMax = sliceLogic.GetSliceOffset() + 100
+
+ return sliceOffsetMin, sliceOffsetMax
+
+ def getSliceOffsetResolution(self, sliceNode):
+ sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+
+ sliceOffsetResolution = 1.0
+ sliceSpacing = sliceLogic.GetLowestVolumeSliceSpacing()
+ if sliceSpacing is not None and sliceSpacing[2] > 0:
+ sliceOffsetResolution = sliceSpacing[2]
+
+ return sliceOffsetResolution
+
+ def captureImageFromView(self, view, filename=None, transparentBackground=False, volumeNode=None):
+ """
+ Capture an image of the specified view and store in the specified object.
+
+ :param view: View to capture. If none, all views are captured.
+ :param filename: Filename of the desired output file. If none, no file will be written.
+ :param transparentBackground: Set the background to be transparent for single-view captures.
+ :param volumeNode: Vector volume node to store the capture image. If none, no vector volume node will be updated.
+ """
+ slicer.app.processEvents()
+ if view:
+ if type(view) == slicer.qMRMLSliceView or type(view) == slicer.qMRMLThreeDView:
+ view.forceRender()
+ else:
+ view.repaint()
+ else:
+ slicer.util.forceRenderAllViews()
- def getSliceOffsetRange(self, sliceNode):
- sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+ if view is None:
+ if transparentBackground:
+ logging.warning("Transparent background is only available for single-view capture")
- sliceBounds = [0, -1, 0, -1, 0, -1]
- sliceLogic.GetLowestVolumeSliceBounds(sliceBounds)
- sliceOffsetMin = sliceBounds[4]
- sliceOffsetMax = sliceBounds[5]
+ # no view is specified, capture the entire view layout
- # increase range if it is empty
- # to allow capturing even when no volumes are shown in slice views
- if sliceOffsetMin == sliceOffsetMax:
- sliceOffsetMin = sliceLogic.GetSliceOffset() - 100
- sliceOffsetMax = sliceLogic.GetSliceOffset() + 100
+ # Simply using grabwidget on the view layout frame would grab the screen without background:
+ # img = ctk.ctkWidgetsUtils.grabWidget(slicer.app.layoutManager().viewport())
- return sliceOffsetMin, sliceOffsetMax
+ # Grab the main window and use only the viewport's area
+ allViews = slicer.app.layoutManager().viewport()
+ topLeft = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.topLeft())
+ bottomRight = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.bottomRight())
+ imageSize = bottomRight - topLeft
- def getSliceOffsetResolution(self, sliceNode):
- sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+ if imageSize.x() < 2 or imageSize.y() < 2:
+ # image is too small, most likely it is invalid
+ raise ValueError('Capture image from view failed')
- sliceOffsetResolution = 1.0
- sliceSpacing = sliceLogic.GetLowestVolumeSliceSpacing()
- if sliceSpacing is not None and sliceSpacing[2] > 0:
- sliceOffsetResolution = sliceSpacing[2]
+ img = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow(), qt.QRect(topLeft.x(), topLeft.y(), imageSize.x(), imageSize.y()))
- return sliceOffsetResolution
+ capturedImage = vtk.vtkImageData()
+ ctk.ctkVTKWidgetsUtils.qImageToVTKImageData(img, capturedImage)
- def captureImageFromView(self, view, filename=None, transparentBackground=False, volumeNode=None):
- """
- Capture an image of the specified view and store in the specified object.
+ else:
+ # Capture single view
+
+ rw = view.renderWindow()
+ wti = vtk.vtkWindowToImageFilter()
+
+ if transparentBackground:
+ originalAlphaBitPlanes = rw.GetAlphaBitPlanes()
+ rw.SetAlphaBitPlanes(1)
+ ren = rw.GetRenderers().GetFirstRenderer()
+ originalGradientBackground = ren.GetGradientBackground()
+ ren.SetGradientBackground(False)
+ wti.SetInputBufferTypeToRGBA()
+ rw.Render() # need to render after changing bit planes
+
+ wti.SetInput(rw)
+ wti.Update()
+
+ if transparentBackground:
+ rw.SetAlphaBitPlanes(originalAlphaBitPlanes)
+ ren.SetGradientBackground(originalGradientBackground)
+
+ capturedImage = wti.GetOutput()
+
+ imageSize = capturedImage.GetDimensions()
+
+ if imageSize[0] < 2 or imageSize[1] < 2:
+ # image is too small, most likely it is invalid
+ raise ValueError('Capture image from view failed')
+
+ # Make sure image width and height is even, otherwise encoding may fail
+ imageWidthOdd = (imageSize[0] & 1 == 1)
+ imageHeightOdd = (imageSize[1] & 1 == 1)
+ if imageWidthOdd or imageHeightOdd:
+ imageClipper = vtk.vtkImageClip()
+ imageClipper.SetClipData(True)
+ imageClipper.SetInputData(capturedImage)
+ extent = capturedImage.GetExtent()
+ imageClipper.SetOutputWholeExtent(extent[0], extent[1] - 1 if imageWidthOdd else extent[1],
+ extent[2], extent[3] - 1 if imageHeightOdd else extent[3],
+ extent[4], extent[5])
+ imageClipper.Update()
+ capturedImage = imageClipper.GetOutput()
+
+ capturedImage = self.addWatermark(capturedImage)
+ if volumeNode is not None:
+ if isinstance(volumeNode, slicer.vtkMRMLVectorVolumeNode):
+ ijkToRas = vtk.vtkMatrix4x4()
+ ijkToRas.SetElement(0, 0, -1)
+ ijkToRas.SetElement(1, 1, -1)
+ volumeNode.SetIJKToRASMatrix(ijkToRas)
+ vflip = vtk.vtkImageFlip()
+ vflip.SetInputData(capturedImage)
+ vflip.SetFilteredAxis(1)
+ vflip.Update()
+ volumeNode.SetAndObserveImageData(vflip.GetOutput())
+ else:
+ raise ValueError("Invalid vector volume node.")
+ if filename:
+ writer = self.createImageWriter(filename)
+ writer.SetInputData(capturedImage)
+ writer.SetFileName(filename)
+ writer.Write()
+
+ def createImageWriter(self, filename):
+ name, extension = os.path.splitext(filename)
+ if extension.lower() == '.png':
+ return vtk.vtkPNGWriter()
+ elif extension.lower() == '.jpg' or extension.lower() == '.jpeg':
+ return vtk.vtkJPEGWriter()
+ else:
+ raise ValueError('Unsupported image format based on file name ' + filename)
+
+ def createImageReader(self, filename):
+ name, extension = os.path.splitext(filename)
+ if extension.lower() == '.png':
+ return vtk.vtkPNGReader()
+ elif extension.lower() == '.jpg' or extension.lower() == '.jpeg':
+ return vtk.vtkJPEGReader()
+ else:
+ raise ValueError('Unsupported image format based on file name ' + filename)
+
+ def addWatermark(self, capturedImage):
+
+ if self.watermarkPosition < 0:
+ # no watermark
+ return capturedImage
+
+ watermarkReader = vtk.vtkPNGReader()
+ watermarkReader.SetFileName(self.watermarkImagePath)
+ watermarkReader.Update()
+ watermarkImage = watermarkReader.GetOutput()
+
+ # Add alpha channel, if image is only RGB and not RGBA
+ if watermarkImage.GetNumberOfScalarComponents() == 3:
+ alphaImage = vtk.vtkImageData()
+ alphaImage.SetDimensions(watermarkImage.GetDimensions())
+ alphaImage.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
+ alphaImage.GetPointData().GetScalars().Fill(255)
+ appendRGBA = vtk.vtkImageAppendComponents()
+ appendRGBA.AddInputData(watermarkImage)
+ appendRGBA.AddInputData(alphaImage)
+ appendRGBA.Update()
+ watermarkImage = appendRGBA.GetOutput()
+
+ watermarkSize = [0] * 3
+ watermarkImage.GetDimensions(watermarkSize)
+ capturedImageSize = [0] * 3
+ capturedImage.GetDimensions(capturedImageSize)
+ spacing = 100.0 / self.watermarkSizePercent
+ watermarkResize = vtk.vtkImageReslice()
+ watermarkResize.SetInterpolationModeToCubic()
+ watermarkResize.SetInputData(watermarkImage)
+ watermarkResize.SetOutputExtent(capturedImage.GetExtent())
+ watermarkResize.SetOutputSpacing(spacing, spacing, 1)
+ position = self.watermarkPositionPresets[self.watermarkPosition]["position"](capturedImageSize, watermarkSize, spacing)
+ watermarkResize.SetOutputOrigin(position[0], position[1], 0.0)
+ watermarkResize.Update()
+
+ blend = vtk.vtkImageBlend()
+ blend.SetOpacity(0, 1.0 - self.watermarkOpacityPercent * 0.01)
+ blend.SetOpacity(1, self.watermarkOpacityPercent * 0.01)
+ blend.AddInputData(capturedImage)
+ blend.AddInputData(watermarkResize.GetOutput())
+ blend.Update()
+
+ return blend.GetOutput()
+
+ def viewFromNode(self, viewNode):
+ if not viewNode:
+ raise ValueError('Invalid view node.')
+ elif viewNode.IsA("vtkMRMLSliceNode"):
+ return slicer.app.layoutManager().sliceWidget(viewNode.GetLayoutName()).sliceView()
+ elif viewNode.IsA("vtkMRMLViewNode"):
+ renderView = None
+ lm = slicer.app.layoutManager()
+ for widgetIndex in range(lm.threeDViewCount):
+ view = lm.threeDWidget(widgetIndex).threeDView()
+ if viewNode == view.mrmlViewNode():
+ renderView = view
+ break
+ if not renderView:
+ raise ValueError('Selected 3D view is not visible in the current layout.')
+ return renderView
+ elif viewNode.IsA("vtkMRMLPlotViewNode"):
+ renderView = None
+ lm = slicer.app.layoutManager()
+ for viewIndex in range(lm.plotViewCount):
+ if viewNode == lm.plotWidget(viewIndex).mrmlPlotViewNode():
+ renderView = lm.plotWidget(viewIndex).plotView()
+ break
+ if not renderView:
+ raise ValueError('Selected 3D view is not visible in the current layout.')
+ return renderView
+ else:
+ raise ValueError('Invalid view node.')
+
+ def captureSliceSweep(self, sliceNode, startSliceOffset, endSliceOffset, numberOfImages,
+ outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):
+
+ self.cancelRequested = False
+
+ if not captureAllViews and not sliceNode.IsMappedInLayout():
+ raise ValueError('Selected slice view is not visible in the current layout.')
+
+ if not os.path.exists(outputDir):
+ os.makedirs(outputDir)
+ filePathPattern = os.path.join(outputDir, outputFilenamePattern)
+
+ sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+ originalSliceOffset = sliceLogic.GetSliceOffset()
+
+ sliceView = self.viewFromNode(sliceNode)
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ offsetStepSize = (endSliceOffset - startSliceOffset) / (numberOfImages - 1)
+ for offsetIndex in range(numberOfImages):
+ filename = filePathPattern % offsetIndex
+ self.addLog("Write " + filename)
+ sliceLogic.SetSliceOffset(startSliceOffset + offsetIndex * offsetStepSize)
+ self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
+ if self.cancelRequested:
+ break
+
+ sliceLogic.SetSliceOffset(originalSliceOffset)
+ if self.cancelRequested:
+ raise ValueError('User requested cancel.')
+
+ def captureSliceFade(self, sliceNode, numberOfImages, outputDir,
+ outputFilenamePattern, captureAllViews=None, transparentBackground=False):
+
+ self.cancelRequested = False
+
+ if not captureAllViews and not sliceNode.IsMappedInLayout():
+ raise ValueError('Selected slice view is not visible in the current layout.')
+
+ if not os.path.exists(outputDir):
+ os.makedirs(outputDir)
+ filePathPattern = os.path.join(outputDir, outputFilenamePattern)
+
+ sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
+ sliceView = self.viewFromNode(sliceNode)
+ compositeNode = sliceLogic.GetSliceCompositeNode()
+ originalForegroundOpacity = compositeNode.GetForegroundOpacity()
+ startForegroundOpacity = 0.0
+ endForegroundOpacity = 1.0
+ opacityStepSize = (endForegroundOpacity - startForegroundOpacity) / (numberOfImages - 1)
+ for offsetIndex in range(numberOfImages):
+ filename = filePathPattern % offsetIndex
+ self.addLog("Write " + filename)
+ compositeNode.SetForegroundOpacity(startForegroundOpacity + offsetIndex * opacityStepSize)
+ self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
+ if self.cancelRequested:
+ break
+
+ compositeNode.SetForegroundOpacity(originalForegroundOpacity)
+
+ if self.cancelRequested:
+ raise ValueError('User requested cancel.')
+
+ def capture3dViewRotation(self, viewNode, startRotation, endRotation, numberOfImages, rotationAxis,
+ outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):
+ """
+ Acquire a set of screenshots of the 3D view while rotating it.
+ """
+
+ self.cancelRequested = False
+
+ if not os.path.exists(outputDir):
+ os.makedirs(outputDir)
+ filePathPattern = os.path.join(outputDir, outputFilenamePattern)
+
+ renderView = self.viewFromNode(viewNode)
+
+ # Save original orientation and go to start orientation
+ originalPitchRollYawIncrement = renderView.pitchRollYawIncrement
+ originalDirection = renderView.pitchDirection
+ renderView.setPitchRollYawIncrement(-startRotation)
+ if rotationAxis == AXIS_YAW:
+ renderView.yawDirection = renderView.YawRight
+ renderView.yaw()
+ else:
+ renderView.pitchDirection = renderView.PitchDown
+ renderView.pitch()
+
+ # Rotate step-by-step
+ rotationStepSize = (endRotation - startRotation) / (numberOfImages - 1)
+ renderView.setPitchRollYawIncrement(rotationStepSize)
+ if rotationAxis == AXIS_YAW:
+ renderView.yawDirection = renderView.YawLeft
+ else:
+ renderView.pitchDirection = renderView.PitchUp
+ for offsetIndex in range(numberOfImages):
+ if not self.cancelRequested:
+ filename = filePathPattern % offsetIndex
+ self.addLog("Write " + filename)
+ self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
+ if rotationAxis == AXIS_YAW:
+ renderView.yaw()
+ else:
+ renderView.pitch()
+
+ # Restore original orientation and rotation step size & direction
+ if rotationAxis == AXIS_YAW:
+ renderView.yawDirection = renderView.YawRight
+ renderView.yaw()
+ renderView.setPitchRollYawIncrement(endRotation)
+ renderView.yaw()
+ renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
+ renderView.yawDirection = originalDirection
+ else:
+ renderView.pitchDirection = renderView.PitchDown
+ renderView.pitch()
+ renderView.setPitchRollYawIncrement(endRotation)
+ renderView.pitch()
+ renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
+ renderView.pitchDirection = originalDirection
- :param view: View to capture. If none, all views are captured.
- :param filename: Filename of the desired output file. If none, no file will be written.
- :param transparentBackground: Set the background to be transparent for single-view captures.
- :param volumeNode: Vector volume node to store the capture image. If none, no vector volume node will be updated.
- """
- slicer.app.processEvents()
- if view:
- if type(view) == slicer.qMRMLSliceView or type(view) == slicer.qMRMLThreeDView:
- view.forceRender()
- else:
- view.repaint()
- else:
- slicer.util.forceRenderAllViews()
-
- if view is None:
- if transparentBackground:
- logging.warning("Transparent background is only available for single-view capture")
-
- # no view is specified, capture the entire view layout
-
- # Simply using grabwidget on the view layout frame would grab the screen without background:
- # img = ctk.ctkWidgetsUtils.grabWidget(slicer.app.layoutManager().viewport())
-
- # Grab the main window and use only the viewport's area
- allViews = slicer.app.layoutManager().viewport()
- topLeft = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.topLeft())
- bottomRight = allViews.mapTo(slicer.util.mainWindow(), allViews.rect.bottomRight())
- imageSize = bottomRight - topLeft
-
- if imageSize.x() < 2 or imageSize.y() < 2:
- # image is too small, most likely it is invalid
- raise ValueError('Capture image from view failed')
-
- img = ctk.ctkWidgetsUtils.grabWidget(slicer.util.mainWindow(), qt.QRect(topLeft.x(), topLeft.y(), imageSize.x(), imageSize.y()))
-
- capturedImage = vtk.vtkImageData()
- ctk.ctkVTKWidgetsUtils.qImageToVTKImageData(img, capturedImage)
-
- else:
- # Capture single view
-
- rw = view.renderWindow()
- wti = vtk.vtkWindowToImageFilter()
-
- if transparentBackground:
- originalAlphaBitPlanes = rw.GetAlphaBitPlanes()
- rw.SetAlphaBitPlanes(1)
- ren = rw.GetRenderers().GetFirstRenderer()
- originalGradientBackground = ren.GetGradientBackground()
- ren.SetGradientBackground(False)
- wti.SetInputBufferTypeToRGBA()
- rw.Render() # need to render after changing bit planes
-
- wti.SetInput(rw)
- wti.Update()
-
- if transparentBackground:
- rw.SetAlphaBitPlanes(originalAlphaBitPlanes)
- ren.SetGradientBackground(originalGradientBackground)
-
- capturedImage = wti.GetOutput()
-
- imageSize = capturedImage.GetDimensions()
-
- if imageSize[0] < 2 or imageSize[1] < 2:
- # image is too small, most likely it is invalid
- raise ValueError('Capture image from view failed')
-
- # Make sure image width and height is even, otherwise encoding may fail
- imageWidthOdd = (imageSize[0] & 1 == 1)
- imageHeightOdd = (imageSize[1] & 1 == 1)
- if imageWidthOdd or imageHeightOdd:
- imageClipper = vtk.vtkImageClip()
- imageClipper.SetClipData(True)
- imageClipper.SetInputData(capturedImage)
- extent = capturedImage.GetExtent()
- imageClipper.SetOutputWholeExtent(extent[0], extent[1] - 1 if imageWidthOdd else extent[1],
- extent[2], extent[3] - 1 if imageHeightOdd else extent[3],
- extent[4], extent[5])
- imageClipper.Update()
- capturedImage = imageClipper.GetOutput()
-
- capturedImage = self.addWatermark(capturedImage)
- if volumeNode is not None:
- if isinstance(volumeNode, slicer.vtkMRMLVectorVolumeNode):
- ijkToRas = vtk.vtkMatrix4x4()
- ijkToRas.SetElement(0, 0, -1)
- ijkToRas.SetElement(1, 1, -1)
- volumeNode.SetIJKToRASMatrix(ijkToRas)
- vflip = vtk.vtkImageFlip()
- vflip.SetInputData(capturedImage)
- vflip.SetFilteredAxis(1)
- vflip.Update()
- volumeNode.SetAndObserveImageData(vflip.GetOutput())
- else:
- raise ValueError("Invalid vector volume node.")
- if filename:
- writer = self.createImageWriter(filename)
- writer.SetInputData(capturedImage)
- writer.SetFileName(filename)
- writer.Write()
-
- def createImageWriter(self, filename):
- name, extension = os.path.splitext(filename)
- if extension.lower() == '.png':
- return vtk.vtkPNGWriter()
- elif extension.lower() == '.jpg' or extension.lower() == '.jpeg':
- return vtk.vtkJPEGWriter()
- else:
- raise ValueError('Unsupported image format based on file name ' + filename)
-
- def createImageReader(self, filename):
- name, extension = os.path.splitext(filename)
- if extension.lower() == '.png':
- return vtk.vtkPNGReader()
- elif extension.lower() == '.jpg' or extension.lower() == '.jpeg':
- return vtk.vtkJPEGReader()
- else:
- raise ValueError('Unsupported image format based on file name ' + filename)
-
- def addWatermark(self, capturedImage):
-
- if self.watermarkPosition < 0:
- # no watermark
- return capturedImage
-
- watermarkReader = vtk.vtkPNGReader()
- watermarkReader.SetFileName(self.watermarkImagePath)
- watermarkReader.Update()
- watermarkImage = watermarkReader.GetOutput()
-
- # Add alpha channel, if image is only RGB and not RGBA
- if watermarkImage.GetNumberOfScalarComponents() == 3:
- alphaImage = vtk.vtkImageData()
- alphaImage.SetDimensions(watermarkImage.GetDimensions())
- alphaImage.AllocateScalars(vtk.VTK_UNSIGNED_CHAR, 1)
- alphaImage.GetPointData().GetScalars().Fill(255)
- appendRGBA = vtk.vtkImageAppendComponents()
- appendRGBA.AddInputData(watermarkImage)
- appendRGBA.AddInputData(alphaImage)
- appendRGBA.Update()
- watermarkImage = appendRGBA.GetOutput()
-
- watermarkSize = [0] * 3
- watermarkImage.GetDimensions(watermarkSize)
- capturedImageSize = [0] * 3
- capturedImage.GetDimensions(capturedImageSize)
- spacing = 100.0 / self.watermarkSizePercent
- watermarkResize = vtk.vtkImageReslice()
- watermarkResize.SetInterpolationModeToCubic()
- watermarkResize.SetInputData(watermarkImage)
- watermarkResize.SetOutputExtent(capturedImage.GetExtent())
- watermarkResize.SetOutputSpacing(spacing, spacing, 1)
- position = self.watermarkPositionPresets[self.watermarkPosition]["position"](capturedImageSize, watermarkSize, spacing)
- watermarkResize.SetOutputOrigin(position[0], position[1], 0.0)
- watermarkResize.Update()
-
- blend = vtk.vtkImageBlend()
- blend.SetOpacity(0, 1.0 - self.watermarkOpacityPercent * 0.01)
- blend.SetOpacity(1, self.watermarkOpacityPercent * 0.01)
- blend.AddInputData(capturedImage)
- blend.AddInputData(watermarkResize.GetOutput())
- blend.Update()
-
- return blend.GetOutput()
-
- def viewFromNode(self, viewNode):
- if not viewNode:
- raise ValueError('Invalid view node.')
- elif viewNode.IsA("vtkMRMLSliceNode"):
- return slicer.app.layoutManager().sliceWidget(viewNode.GetLayoutName()).sliceView()
- elif viewNode.IsA("vtkMRMLViewNode"):
- renderView = None
- lm = slicer.app.layoutManager()
- for widgetIndex in range(lm.threeDViewCount):
- view = lm.threeDWidget(widgetIndex).threeDView()
- if viewNode == view.mrmlViewNode():
- renderView = view
- break
- if not renderView:
- raise ValueError('Selected 3D view is not visible in the current layout.')
- return renderView
- elif viewNode.IsA("vtkMRMLPlotViewNode"):
- renderView = None
- lm = slicer.app.layoutManager()
- for viewIndex in range(lm.plotViewCount):
- if viewNode == lm.plotWidget(viewIndex).mrmlPlotViewNode():
- renderView = lm.plotWidget(viewIndex).plotView()
- break
- if not renderView:
- raise ValueError('Selected 3D view is not visible in the current layout.')
- return renderView
- else:
- raise ValueError('Invalid view node.')
-
- def captureSliceSweep(self, sliceNode, startSliceOffset, endSliceOffset, numberOfImages,
- outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):
-
- self.cancelRequested = False
-
- if not captureAllViews and not sliceNode.IsMappedInLayout():
- raise ValueError('Selected slice view is not visible in the current layout.')
-
- if not os.path.exists(outputDir):
- os.makedirs(outputDir)
- filePathPattern = os.path.join(outputDir, outputFilenamePattern)
-
- sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
- originalSliceOffset = sliceLogic.GetSliceOffset()
-
- sliceView = self.viewFromNode(sliceNode)
- compositeNode = sliceLogic.GetSliceCompositeNode()
- offsetStepSize = (endSliceOffset - startSliceOffset) / (numberOfImages - 1)
- for offsetIndex in range(numberOfImages):
- filename = filePathPattern % offsetIndex
- self.addLog("Write " + filename)
- sliceLogic.SetSliceOffset(startSliceOffset + offsetIndex * offsetStepSize)
- self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
- if self.cancelRequested:
- break
-
- sliceLogic.SetSliceOffset(originalSliceOffset)
- if self.cancelRequested:
- raise ValueError('User requested cancel.')
-
- def captureSliceFade(self, sliceNode, numberOfImages, outputDir,
- outputFilenamePattern, captureAllViews=None, transparentBackground=False):
-
- self.cancelRequested = False
-
- if not captureAllViews and not sliceNode.IsMappedInLayout():
- raise ValueError('Selected slice view is not visible in the current layout.')
-
- if not os.path.exists(outputDir):
- os.makedirs(outputDir)
- filePathPattern = os.path.join(outputDir, outputFilenamePattern)
-
- sliceLogic = self.getSliceLogicFromSliceNode(sliceNode)
- sliceView = self.viewFromNode(sliceNode)
- compositeNode = sliceLogic.GetSliceCompositeNode()
- originalForegroundOpacity = compositeNode.GetForegroundOpacity()
- startForegroundOpacity = 0.0
- endForegroundOpacity = 1.0
- opacityStepSize = (endForegroundOpacity - startForegroundOpacity) / (numberOfImages - 1)
- for offsetIndex in range(numberOfImages):
- filename = filePathPattern % offsetIndex
- self.addLog("Write " + filename)
- compositeNode.SetForegroundOpacity(startForegroundOpacity + offsetIndex * opacityStepSize)
- self.captureImageFromView(None if captureAllViews else sliceView, filename, transparentBackground)
- if self.cancelRequested:
- break
-
- compositeNode.SetForegroundOpacity(originalForegroundOpacity)
-
- if self.cancelRequested:
- raise ValueError('User requested cancel.')
-
- def capture3dViewRotation(self, viewNode, startRotation, endRotation, numberOfImages, rotationAxis,
- outputDir, outputFilenamePattern, captureAllViews=None, transparentBackground=False):
- """
- Acquire a set of screenshots of the 3D view while rotating it.
- """
+ if self.cancelRequested:
+ raise ValueError('User requested cancel.')
- self.cancelRequested = False
-
- if not os.path.exists(outputDir):
- os.makedirs(outputDir)
- filePathPattern = os.path.join(outputDir, outputFilenamePattern)
-
- renderView = self.viewFromNode(viewNode)
-
- # Save original orientation and go to start orientation
- originalPitchRollYawIncrement = renderView.pitchRollYawIncrement
- originalDirection = renderView.pitchDirection
- renderView.setPitchRollYawIncrement(-startRotation)
- if rotationAxis == AXIS_YAW:
- renderView.yawDirection = renderView.YawRight
- renderView.yaw()
- else:
- renderView.pitchDirection = renderView.PitchDown
- renderView.pitch()
-
- # Rotate step-by-step
- rotationStepSize = (endRotation - startRotation) / (numberOfImages - 1)
- renderView.setPitchRollYawIncrement(rotationStepSize)
- if rotationAxis == AXIS_YAW:
- renderView.yawDirection = renderView.YawLeft
- else:
- renderView.pitchDirection = renderView.PitchUp
- for offsetIndex in range(numberOfImages):
- if not self.cancelRequested:
- filename = filePathPattern % offsetIndex
- self.addLog("Write " + filename)
- self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
- if rotationAxis == AXIS_YAW:
- renderView.yaw()
- else:
- renderView.pitch()
-
- # Restore original orientation and rotation step size & direction
- if rotationAxis == AXIS_YAW:
- renderView.yawDirection = renderView.YawRight
- renderView.yaw()
- renderView.setPitchRollYawIncrement(endRotation)
- renderView.yaw()
- renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
- renderView.yawDirection = originalDirection
- else:
- renderView.pitchDirection = renderView.PitchDown
- renderView.pitch()
- renderView.setPitchRollYawIncrement(endRotation)
- renderView.pitch()
- renderView.setPitchRollYawIncrement(originalPitchRollYawIncrement)
- renderView.pitchDirection = originalDirection
-
- if self.cancelRequested:
- raise ValueError('User requested cancel.')
-
- def captureSequence(self, viewNode, sequenceBrowserNode, sequenceStartIndex,
+ def captureSequence(self, viewNode, sequenceBrowserNode, sequenceStartIndex,
sequenceEndIndex, numberOfImages, outputDir, outputFilenamePattern,
captureAllViews=None, transparentBackground=False):
- """
- Acquire a set of screenshots of a view while iterating through a sequence.
- """
+ """
+ Acquire a set of screenshots of a view while iterating through a sequence.
+ """
+
+ self.cancelRequested = False
+
+ if not os.path.exists(outputDir):
+ os.makedirs(outputDir)
+ filePathPattern = os.path.join(outputDir, outputFilenamePattern)
+
+ originalSelectedItemNumber = sequenceBrowserNode.GetSelectedItemNumber()
+
+ renderView = self.viewFromNode(viewNode)
+ stepSize = (sequenceEndIndex - sequenceStartIndex) / (numberOfImages - 1)
+ for offsetIndex in range(numberOfImages):
+ sequenceBrowserNode.SetSelectedItemNumber(int(sequenceStartIndex + offsetIndex * stepSize))
+ filename = filePathPattern % offsetIndex
+ self.addLog("Write " + filename)
+ self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
+ if self.cancelRequested:
+ break
+
+ sequenceBrowserNode.SetSelectedItemNumber(originalSelectedItemNumber)
+ if self.cancelRequested:
+ raise ValueError('User requested cancel.')
+
+ def createLightboxImage(self, numberOfColumns, outputDir, imageFileNamePattern, numberOfImages, lightboxImageFilename):
+ self.addLog("Export to lightbox image...")
+ filePathPattern = os.path.join(outputDir, imageFileNamePattern)
+ import math
+ numberOfRows = int(math.ceil(numberOfImages / numberOfColumns))
+ imageMarginSizePixels = 5
+ for row in range(numberOfRows):
+ for column in range(numberOfColumns):
+ imageIndex = row * numberOfColumns + column
+ if imageIndex >= numberOfImages:
+ break
+ sourceFilename = filePathPattern % imageIndex
+ reader = self.createImageReader(sourceFilename)
+ reader.SetFileName(sourceFilename)
+ reader.Update()
+ image = reader.GetOutput()
+
+ if imageIndex == 0:
+ # First image, initialize output lightbox image
+ imageDimensions = image.GetDimensions()
+ lightboxImageExtent = [0, numberOfColumns * imageDimensions[0] + (numberOfColumns - 1) * imageMarginSizePixels - 1,
+ 0, numberOfRows * imageDimensions[1] + (numberOfRows - 1) * imageMarginSizePixels - 1,
+ 0, 0]
+ lightboxCanvas = vtk.vtkImageCanvasSource2D()
+ lightboxCanvas.SetNumberOfScalarComponents(3)
+ lightboxCanvas.SetScalarTypeToUnsignedChar()
+ lightboxCanvas.SetExtent(lightboxImageExtent)
+ # Fill background with black
+ lightboxCanvas.SetDrawColor(50, 50, 50)
+ lightboxCanvas.FillBox(*lightboxImageExtent[0:4])
+
+ drawingPosition = [column * (imageDimensions[0] + imageMarginSizePixels),
+ lightboxImageExtent[3] - (row + 1) * imageDimensions[1] - row * imageMarginSizePixels + 1]
+ lightboxCanvas.DrawImage(drawingPosition[0], drawingPosition[1], image)
+
+ lightboxCanvas.Update()
+ outputLightboxImageFilePath = os.path.join(outputDir, lightboxImageFilename)
+ writer = self.createImageWriter(outputLightboxImageFilePath)
+ writer.SetFileName(outputLightboxImageFilePath)
+ writer.SetInputData(lightboxCanvas.GetOutput())
+ writer.Write()
+
+ self.addLog("Lighbox image saved to file: " + outputLightboxImageFilePath)
+
+ def createVideo(self, frameRate, extraOptions, outputDir, imageFileNamePattern, videoFileName):
+ self.addLog("Export to video...")
+
+ # Get ffmpeg
+ import os.path
+ ffmpegPath = os.path.abspath(self.getFfmpegPath())
+ if not ffmpegPath:
+ raise ValueError("Video creation failed: ffmpeg executable path is not defined")
+ if not os.path.isfile(ffmpegPath):
+ raise ValueError("Video creation failed: ffmpeg executable path is invalid: " + ffmpegPath)
- self.cancelRequested = False
-
- if not os.path.exists(outputDir):
- os.makedirs(outputDir)
- filePathPattern = os.path.join(outputDir, outputFilenamePattern)
-
- originalSelectedItemNumber = sequenceBrowserNode.GetSelectedItemNumber()
-
- renderView = self.viewFromNode(viewNode)
- stepSize = (sequenceEndIndex - sequenceStartIndex) / (numberOfImages - 1)
- for offsetIndex in range(numberOfImages):
- sequenceBrowserNode.SetSelectedItemNumber(int(sequenceStartIndex + offsetIndex * stepSize))
- filename = filePathPattern % offsetIndex
- self.addLog("Write " + filename)
- self.captureImageFromView(None if captureAllViews else renderView, filename, transparentBackground)
- if self.cancelRequested:
- break
-
- sequenceBrowserNode.SetSelectedItemNumber(originalSelectedItemNumber)
- if self.cancelRequested:
- raise ValueError('User requested cancel.')
-
- def createLightboxImage(self, numberOfColumns, outputDir, imageFileNamePattern, numberOfImages, lightboxImageFilename):
- self.addLog("Export to lightbox image...")
- filePathPattern = os.path.join(outputDir, imageFileNamePattern)
- import math
- numberOfRows = int(math.ceil(numberOfImages / numberOfColumns))
- imageMarginSizePixels = 5
- for row in range(numberOfRows):
- for column in range(numberOfColumns):
- imageIndex = row * numberOfColumns + column
- if imageIndex >= numberOfImages:
- break
- sourceFilename = filePathPattern % imageIndex
- reader = self.createImageReader(sourceFilename)
- reader.SetFileName(sourceFilename)
- reader.Update()
- image = reader.GetOutput()
-
- if imageIndex == 0:
- # First image, initialize output lightbox image
- imageDimensions = image.GetDimensions()
- lightboxImageExtent = [0, numberOfColumns * imageDimensions[0] + (numberOfColumns - 1) * imageMarginSizePixels - 1,
- 0, numberOfRows * imageDimensions[1] + (numberOfRows - 1) * imageMarginSizePixels - 1,
- 0, 0]
- lightboxCanvas = vtk.vtkImageCanvasSource2D()
- lightboxCanvas.SetNumberOfScalarComponents(3)
- lightboxCanvas.SetScalarTypeToUnsignedChar()
- lightboxCanvas.SetExtent(lightboxImageExtent)
- # Fill background with black
- lightboxCanvas.SetDrawColor(50, 50, 50)
- lightboxCanvas.FillBox(*lightboxImageExtent[0:4])
-
- drawingPosition = [column * (imageDimensions[0] + imageMarginSizePixels),
- lightboxImageExtent[3] - (row + 1) * imageDimensions[1] - row * imageMarginSizePixels + 1]
- lightboxCanvas.DrawImage(drawingPosition[0], drawingPosition[1], image)
-
- lightboxCanvas.Update()
- outputLightboxImageFilePath = os.path.join(outputDir, lightboxImageFilename)
- writer = self.createImageWriter(outputLightboxImageFilePath)
- writer.SetFileName(outputLightboxImageFilePath)
- writer.SetInputData(lightboxCanvas.GetOutput())
- writer.Write()
-
- self.addLog("Lighbox image saved to file: " + outputLightboxImageFilePath)
-
- def createVideo(self, frameRate, extraOptions, outputDir, imageFileNamePattern, videoFileName):
- self.addLog("Export to video...")
-
- # Get ffmpeg
- import os.path
- ffmpegPath = os.path.abspath(self.getFfmpegPath())
- if not ffmpegPath:
- raise ValueError("Video creation failed: ffmpeg executable path is not defined")
- if not os.path.isfile(ffmpegPath):
- raise ValueError("Video creation failed: ffmpeg executable path is invalid: " + ffmpegPath)
-
- filePathPattern = os.path.join(outputDir, imageFileNamePattern)
- outputVideoFilePath = os.path.join(outputDir, videoFileName)
- ffmpegParams = [ffmpegPath,
- "-nostdin", # disable stdin (to prevent hang when running Slicer as background process)
- "-y", # overwrite without asking
- "-r", str(frameRate),
- "-start_number", "0",
- "-i", str(filePathPattern)]
- ffmpegParams += [_f for _f in extraOptions.split(' ') if _f]
- ffmpegParams.append(outputVideoFilePath)
-
- self.addLog("Start ffmpeg:\n" + ' '.join(ffmpegParams))
-
- import subprocess
- p = subprocess.Popen(ffmpegParams, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=outputDir)
- stdout, stderr = p.communicate()
- if p.returncode != 0:
- self.addLog("ffmpeg error output: " + stderr.decode())
- raise ValueError("ffmpeg returned with error")
- else:
- self.addLog("Video export succeeded to file: " + outputVideoFilePath)
- logging.debug("ffmpeg standard output: " + stdout.decode())
- logging.debug("ffmpeg error output: " + stderr.decode())
-
- def deleteTemporaryFiles(self, outputDir, imageFileNamePattern, numberOfImages):
- """
- Delete files after a video has been created from them.
- """
- import os
- filePathPattern = os.path.join(outputDir, imageFileNamePattern)
- for imageIndex in range(numberOfImages):
- filename = filePathPattern % imageIndex
- logging.debug("Delete temporary file " + filename)
- os.remove(filename)
-
- def getNextAvailableFileName(self, outputDir, outputFilenamePattern, snapshotIndex):
- """
- Find a file index that does not overwrite any existing file.
- """
- if not os.path.exists(outputDir):
- os.makedirs(outputDir)
- filePathPattern = os.path.join(outputDir, outputFilenamePattern)
- filename = None
- while True:
- filename = filePathPattern % snapshotIndex
- if not os.path.exists(filename):
- # found an available file name
- break
- snapshotIndex += 1
- return [filename, snapshotIndex]
+ filePathPattern = os.path.join(outputDir, imageFileNamePattern)
+ outputVideoFilePath = os.path.join(outputDir, videoFileName)
+ ffmpegParams = [ffmpegPath,
+ "-nostdin", # disable stdin (to prevent hang when running Slicer as background process)
+ "-y", # overwrite without asking
+ "-r", str(frameRate),
+ "-start_number", "0",
+ "-i", str(filePathPattern)]
+ ffmpegParams += [_f for _f in extraOptions.split(' ') if _f]
+ ffmpegParams.append(outputVideoFilePath)
+
+ self.addLog("Start ffmpeg:\n" + ' '.join(ffmpegParams))
+
+ import subprocess
+ p = subprocess.Popen(ffmpegParams, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=outputDir)
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ self.addLog("ffmpeg error output: " + stderr.decode())
+ raise ValueError("ffmpeg returned with error")
+ else:
+ self.addLog("Video export succeeded to file: " + outputVideoFilePath)
+ logging.debug("ffmpeg standard output: " + stdout.decode())
+ logging.debug("ffmpeg error output: " + stderr.decode())
+
+ def deleteTemporaryFiles(self, outputDir, imageFileNamePattern, numberOfImages):
+ """
+ Delete files after a video has been created from them.
+ """
+ import os
+ filePathPattern = os.path.join(outputDir, imageFileNamePattern)
+ for imageIndex in range(numberOfImages):
+ filename = filePathPattern % imageIndex
+ logging.debug("Delete temporary file " + filename)
+ os.remove(filename)
+
+ def getNextAvailableFileName(self, outputDir, outputFilenamePattern, snapshotIndex):
+ """
+ Find a file index that does not overwrite any existing file.
+ """
+ if not os.path.exists(outputDir):
+ os.makedirs(outputDir)
+ filePathPattern = os.path.join(outputDir, outputFilenamePattern)
+ filename = None
+ while True:
+ filename = filePathPattern % snapshotIndex
+ if not os.path.exists(filename):
+ # found an available file name
+ break
+ snapshotIndex += 1
+ return [filename, snapshotIndex]
class ScreenCaptureTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
- import SampleData
- self.image1 = SampleData.downloadSample('MRBrainTumor1')
- self.image2 = SampleData.downloadSample('MRBrainTumor2')
-
- # make the output volume appear in all the slice views
- selectionNode = slicer.app.applicationLogic().GetSelectionNode()
- selectionNode.SetActiveVolumeID(self.image1.GetID())
- selectionNode.SetSecondaryVolumeID(self.image2.GetID())
- slicer.app.applicationLogic().PropagateVolumeSelection(1)
-
- # Show slice and 3D views
- layoutManager = slicer.app.layoutManager()
- layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView)
- for sliceViewNodeId in ['vtkMRMLSliceNodeRed', 'vtkMRMLSliceNodeYellow', 'vtkMRMLSliceNodeGreen']:
- slicer.mrmlScene.GetNodeByID(sliceViewNodeId).SetSliceVisible(True)
-
- self.tempDir = slicer.app.temporaryPath + '/ScreenCaptureTest'
- self.numberOfImages = 10
- self.imageFileNamePattern = "image_%05d.png"
-
- self.logic = ScreenCaptureLogic()
-
- def verifyAndDeleteWrittenFiles(self):
- import os
- filePathPattern = os.path.join(self.tempDir, self.imageFileNamePattern)
- for imageIndex in range(self.numberOfImages):
- filename = filePathPattern % imageIndex
- self.assertTrue(os.path.exists(filename))
- self.logic.deleteTemporaryFiles(self.tempDir, self.imageFileNamePattern, self.numberOfImages)
- for imageIndex in range(self.numberOfImages):
- filename = filePathPattern % imageIndex
- self.assertFalse(os.path.exists(filename))
-
- def runTest(self):
- """Run as few or as many tests as needed here.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.setUp()
- self.test_SliceSweep()
- self.test_SliceFade()
- self.test_3dViewRotation()
- self.test_VolumeNodeUpdate()
-
- def test_SliceSweep(self):
- self.delayDisplay("Testing SliceSweep")
- viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed')
- self.assertIsNotNone(viewNode)
- self.logic.captureSliceSweep(viewNode, -125, 75, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
- self.verifyAndDeleteWrittenFiles()
- self.delayDisplay('Testing SliceSweep completed successfully')
-
- def test_SliceFade(self):
- self.delayDisplay("Testing SliceFade")
- viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed')
- self.assertIsNotNone(viewNode)
- self.logic.captureSliceFade(viewNode, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
- self.verifyAndDeleteWrittenFiles()
- self.delayDisplay('Testing SliceFade completed successfully')
-
- def test_3dViewRotation(self):
- self.delayDisplay("Testing 3D view rotation")
- viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLViewNode1')
- self.assertIsNotNone(viewNode)
- self.logic.capture3dViewRotation(viewNode, -180, 180, self.numberOfImages, AXIS_YAW, self.tempDir, self.imageFileNamePattern)
- self.verifyAndDeleteWrittenFiles()
- self.delayDisplay('Testing 3D view rotation completed successfully')
-
- def test_VolumeNodeUpdate(self):
- self.delayDisplay("Testing VolumeNode update")
- viewNode = None # Capture All Views
- volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode")
- self.assertIsNotNone(volumeNode)
- self.assertIsNone(volumeNode.GetImageData())
- self.logic.captureImageFromView(viewNode, volumeNode=volumeNode)
- self.assertIsNotNone(volumeNode.GetImageData())
- self.delayDisplay('Testing VolumeNode update completed successfully')
+
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+ import SampleData
+ self.image1 = SampleData.downloadSample('MRBrainTumor1')
+ self.image2 = SampleData.downloadSample('MRBrainTumor2')
+
+ # make the output volume appear in all the slice views
+ selectionNode = slicer.app.applicationLogic().GetSelectionNode()
+ selectionNode.SetActiveVolumeID(self.image1.GetID())
+ selectionNode.SetSecondaryVolumeID(self.image2.GetID())
+ slicer.app.applicationLogic().PropagateVolumeSelection(1)
+
+ # Show slice and 3D views
+ layoutManager = slicer.app.layoutManager()
+ layoutManager.setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutFourUpView)
+ for sliceViewNodeId in ['vtkMRMLSliceNodeRed', 'vtkMRMLSliceNodeYellow', 'vtkMRMLSliceNodeGreen']:
+ slicer.mrmlScene.GetNodeByID(sliceViewNodeId).SetSliceVisible(True)
+
+ self.tempDir = slicer.app.temporaryPath + '/ScreenCaptureTest'
+ self.numberOfImages = 10
+ self.imageFileNamePattern = "image_%05d.png"
+
+ self.logic = ScreenCaptureLogic()
+
+ def verifyAndDeleteWrittenFiles(self):
+ import os
+ filePathPattern = os.path.join(self.tempDir, self.imageFileNamePattern)
+ for imageIndex in range(self.numberOfImages):
+ filename = filePathPattern % imageIndex
+ self.assertTrue(os.path.exists(filename))
+ self.logic.deleteTemporaryFiles(self.tempDir, self.imageFileNamePattern, self.numberOfImages)
+ for imageIndex in range(self.numberOfImages):
+ filename = filePathPattern % imageIndex
+ self.assertFalse(os.path.exists(filename))
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SliceSweep()
+ self.test_SliceFade()
+ self.test_3dViewRotation()
+ self.test_VolumeNodeUpdate()
+
+ def test_SliceSweep(self):
+ self.delayDisplay("Testing SliceSweep")
+ viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed')
+ self.assertIsNotNone(viewNode)
+ self.logic.captureSliceSweep(viewNode, -125, 75, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
+ self.verifyAndDeleteWrittenFiles()
+ self.delayDisplay('Testing SliceSweep completed successfully')
+
+ def test_SliceFade(self):
+ self.delayDisplay("Testing SliceFade")
+ viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed')
+ self.assertIsNotNone(viewNode)
+ self.logic.captureSliceFade(viewNode, self.numberOfImages, self.tempDir, self.imageFileNamePattern)
+ self.verifyAndDeleteWrittenFiles()
+ self.delayDisplay('Testing SliceFade completed successfully')
+
+ def test_3dViewRotation(self):
+ self.delayDisplay("Testing 3D view rotation")
+ viewNode = slicer.mrmlScene.GetNodeByID('vtkMRMLViewNode1')
+ self.assertIsNotNone(viewNode)
+ self.logic.capture3dViewRotation(viewNode, -180, 180, self.numberOfImages, AXIS_YAW, self.tempDir, self.imageFileNamePattern)
+ self.verifyAndDeleteWrittenFiles()
+ self.delayDisplay('Testing 3D view rotation completed successfully')
+
+ def test_VolumeNodeUpdate(self):
+ self.delayDisplay("Testing VolumeNode update")
+ viewNode = None # Capture All Views
+ volumeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLVectorVolumeNode")
+ self.assertIsNotNone(volumeNode)
+ self.assertIsNone(volumeNode.GetImageData())
+ self.logic.captureImageFromView(viewNode, volumeNode=volumeNode)
+ self.assertIsNotNone(volumeNode.GetImageData())
+ self.delayDisplay('Testing VolumeNode update completed successfully')
diff --git a/Modules/Scripted/SegmentEditor/SegmentEditor.py b/Modules/Scripted/SegmentEditor/SegmentEditor.py
index 64e93128a96..f034a214b99 100644
--- a/Modules/Scripted/SegmentEditor/SegmentEditor.py
+++ b/Modules/Scripted/SegmentEditor/SegmentEditor.py
@@ -7,173 +7,173 @@
# SegmentEditor
#
class SegmentEditor(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Segment Editor"
- self.parent.categories = ["", "Segmentation"]
- self.parent.dependencies = ["Segmentations", "SubjectHierarchy"]
- self.parent.contributors = ["Csaba Pinter (Queen's University), Andras Lasso (Queen's University)"]
- self.parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Segment Editor"
+ self.parent.categories = ["", "Segmentation"]
+ self.parent.dependencies = ["Segmentations", "SubjectHierarchy"]
+ self.parent.contributors = ["Csaba Pinter (Queen's University), Andras Lasso (Queen's University)"]
+ self.parent.helpText = """
This module allows editing segmentation objects by directly drawing and using segmentaiton tools on the contained segments.
Representations other than the labelmap one (which is used for editing) are automatically updated real-time,
so for example the closed surface can be visualized as edited in the 3D view.
"""
- self.parent.helpText += parent.defaultDocumentationLink
- self.parent.acknowledgementText = """
+ self.parent.helpText += parent.defaultDocumentationLink
+ self.parent.acknowledgementText = """
This work is part of SparKit project, funded by Cancer Care Ontario (CCO)'s ACRU program
and Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO).
"""
- def setup(self):
- # Register subject hierarchy plugin
- import SubjectHierarchyPlugins
- scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
- scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentEditorSubjectHierarchyPlugin.filePath)
+ def setup(self):
+ # Register subject hierarchy plugin
+ import SubjectHierarchyPlugins
+ scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
+ scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentEditorSubjectHierarchyPlugin.filePath)
#
# SegmentEditorWidget
#
class SegmentEditorWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
- def __init__(self, parent):
- ScriptedLoadableModuleWidget.__init__(self, parent)
- VTKObservationMixin.__init__(self)
-
- # Members
- self.parameterSetNode = None
- self.editor = None
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # Add margin to the sides
- self.layout.setContentsMargins(4, 0, 4, 0)
-
- #
- # Segment editor widget
- #
- import qSlicerSegmentationsModuleWidgetsPythonQt
- self.editor = qSlicerSegmentationsModuleWidgetsPythonQt.qMRMLSegmentEditorWidget()
- self.editor.setMaximumNumberOfUndoStates(10)
- # Set parameter node first so that the automatic selections made when the scene is set are saved
- self.selectParameterNode()
- self.editor.setMRMLScene(slicer.mrmlScene)
- self.layout.addWidget(self.editor)
-
- # Observe editor effect registrations to make sure that any effects that are registered
- # later will show up in the segment editor widget. For example, if Segment Editor is set
- # as startup module, additional effects are registered after the segment editor widget is created.
- self.effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance()
- self.effectFactorySingleton.connect('effectRegistered(QString)', self.editorEffectRegistered)
-
- # Connect observers to scene events
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneEndImport)
-
- def editorEffectRegistered(self):
- self.editor.updateEffectList()
-
- def selectParameterNode(self):
- # Select parameter set node if one is found in the scene, and create one otherwise
- segmentEditorSingletonTag = "SegmentEditor"
- segmentEditorNode = slicer.mrmlScene.GetSingletonNode(segmentEditorSingletonTag, "vtkMRMLSegmentEditorNode")
- if segmentEditorNode is None:
- segmentEditorNode = slicer.mrmlScene.CreateNodeByClass("vtkMRMLSegmentEditorNode")
- segmentEditorNode.UnRegister(None)
- segmentEditorNode.SetSingletonTag(segmentEditorSingletonTag)
- segmentEditorNode = slicer.mrmlScene.AddNode(segmentEditorNode)
- if self.parameterSetNode == segmentEditorNode:
- # nothing changed
- return
- self.parameterSetNode = segmentEditorNode
- self.editor.setMRMLSegmentEditorNode(self.parameterSetNode)
-
- def getDefaultMasterVolumeNodeID(self):
- layoutManager = slicer.app.layoutManager()
- firstForegroundVolumeID = None
- # Use first background volume node in any of the displayed layouts.
- # If no beackground volume node is in any slice view then use the first
- # foreground volume node.
- for sliceViewName in layoutManager.sliceViewNames():
- sliceWidget = layoutManager.sliceWidget(sliceViewName)
- if not sliceWidget:
- continue
- compositeNode = sliceWidget.mrmlSliceCompositeNode()
- if compositeNode.GetBackgroundVolumeID():
- return compositeNode.GetBackgroundVolumeID()
- if compositeNode.GetForegroundVolumeID() and not firstForegroundVolumeID:
- firstForegroundVolumeID = compositeNode.GetForegroundVolumeID()
- # No background volume was found, so use the foreground volume (if any was found)
- return firstForegroundVolumeID
-
- def enter(self):
- """Runs whenever the module is reopened
- """
- if self.editor.turnOffLightboxes():
- slicer.util.warningDisplay('Segment Editor is not compatible with slice viewers in light box mode.'
- 'Views are being reset.', windowTitle='Segment Editor')
-
- # Allow switching between effects and selected segment using keyboard shortcuts
- self.editor.installKeyboardShortcuts()
-
- # Set parameter set node if absent
- self.selectParameterNode()
- self.editor.updateWidgetFromMRML()
-
- # If no segmentation node exists then create one so that the user does not have to create one manually
- if not self.editor.segmentationNodeID():
- segmentationNode = slicer.mrmlScene.GetFirstNode(None, "vtkMRMLSegmentationNode")
- if not segmentationNode:
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- self.editor.setSegmentationNode(segmentationNode)
- if not self.editor.masterVolumeNodeID():
- masterVolumeNodeID = self.getDefaultMasterVolumeNodeID()
- self.editor.setMasterVolumeNodeID(masterVolumeNodeID)
-
- def exit(self):
- self.editor.setActiveEffect(None)
- self.editor.uninstallKeyboardShortcuts()
- self.editor.removeViewObservations()
-
- def onSceneStartClose(self, caller, event):
- self.parameterSetNode = None
- self.editor.setSegmentationNode(None)
- self.editor.removeViewObservations()
-
- def onSceneEndClose(self, caller, event):
- if self.parent.isEntered:
- self.selectParameterNode()
- self.editor.updateWidgetFromMRML()
-
- def onSceneEndImport(self, caller, event):
- if self.parent.isEntered:
- self.selectParameterNode()
- self.editor.updateWidgetFromMRML()
-
- def cleanup(self):
- self.removeObservers()
- self.effectFactorySingleton.disconnect('effectRegistered(QString)', self.editorEffectRegistered)
+ def __init__(self, parent):
+ ScriptedLoadableModuleWidget.__init__(self, parent)
+ VTKObservationMixin.__init__(self)
+
+ # Members
+ self.parameterSetNode = None
+ self.editor = None
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Add margin to the sides
+ self.layout.setContentsMargins(4, 0, 4, 0)
+
+ #
+ # Segment editor widget
+ #
+ import qSlicerSegmentationsModuleWidgetsPythonQt
+ self.editor = qSlicerSegmentationsModuleWidgetsPythonQt.qMRMLSegmentEditorWidget()
+ self.editor.setMaximumNumberOfUndoStates(10)
+ # Set parameter node first so that the automatic selections made when the scene is set are saved
+ self.selectParameterNode()
+ self.editor.setMRMLScene(slicer.mrmlScene)
+ self.layout.addWidget(self.editor)
+
+ # Observe editor effect registrations to make sure that any effects that are registered
+ # later will show up in the segment editor widget. For example, if Segment Editor is set
+ # as startup module, additional effects are registered after the segment editor widget is created.
+ self.effectFactorySingleton = slicer.qSlicerSegmentEditorEffectFactory.instance()
+ self.effectFactorySingleton.connect('effectRegistered(QString)', self.editorEffectRegistered)
+
+ # Connect observers to scene events
+ self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
+ self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
+ self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndImportEvent, self.onSceneEndImport)
+
+ def editorEffectRegistered(self):
+ self.editor.updateEffectList()
+
+ def selectParameterNode(self):
+ # Select parameter set node if one is found in the scene, and create one otherwise
+ segmentEditorSingletonTag = "SegmentEditor"
+ segmentEditorNode = slicer.mrmlScene.GetSingletonNode(segmentEditorSingletonTag, "vtkMRMLSegmentEditorNode")
+ if segmentEditorNode is None:
+ segmentEditorNode = slicer.mrmlScene.CreateNodeByClass("vtkMRMLSegmentEditorNode")
+ segmentEditorNode.UnRegister(None)
+ segmentEditorNode.SetSingletonTag(segmentEditorSingletonTag)
+ segmentEditorNode = slicer.mrmlScene.AddNode(segmentEditorNode)
+ if self.parameterSetNode == segmentEditorNode:
+ # nothing changed
+ return
+ self.parameterSetNode = segmentEditorNode
+ self.editor.setMRMLSegmentEditorNode(self.parameterSetNode)
+
+ def getDefaultMasterVolumeNodeID(self):
+ layoutManager = slicer.app.layoutManager()
+ firstForegroundVolumeID = None
+ # Use first background volume node in any of the displayed layouts.
+ # If no beackground volume node is in any slice view then use the first
+ # foreground volume node.
+ for sliceViewName in layoutManager.sliceViewNames():
+ sliceWidget = layoutManager.sliceWidget(sliceViewName)
+ if not sliceWidget:
+ continue
+ compositeNode = sliceWidget.mrmlSliceCompositeNode()
+ if compositeNode.GetBackgroundVolumeID():
+ return compositeNode.GetBackgroundVolumeID()
+ if compositeNode.GetForegroundVolumeID() and not firstForegroundVolumeID:
+ firstForegroundVolumeID = compositeNode.GetForegroundVolumeID()
+ # No background volume was found, so use the foreground volume (if any was found)
+ return firstForegroundVolumeID
+
+ def enter(self):
+ """Runs whenever the module is reopened
+ """
+ if self.editor.turnOffLightboxes():
+ slicer.util.warningDisplay('Segment Editor is not compatible with slice viewers in light box mode.'
+ 'Views are being reset.', windowTitle='Segment Editor')
+
+ # Allow switching between effects and selected segment using keyboard shortcuts
+ self.editor.installKeyboardShortcuts()
+
+ # Set parameter set node if absent
+ self.selectParameterNode()
+ self.editor.updateWidgetFromMRML()
+
+ # If no segmentation node exists then create one so that the user does not have to create one manually
+ if not self.editor.segmentationNodeID():
+ segmentationNode = slicer.mrmlScene.GetFirstNode(None, "vtkMRMLSegmentationNode")
+ if not segmentationNode:
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ self.editor.setSegmentationNode(segmentationNode)
+ if not self.editor.masterVolumeNodeID():
+ masterVolumeNodeID = self.getDefaultMasterVolumeNodeID()
+ self.editor.setMasterVolumeNodeID(masterVolumeNodeID)
+
+ def exit(self):
+ self.editor.setActiveEffect(None)
+ self.editor.uninstallKeyboardShortcuts()
+ self.editor.removeViewObservations()
+
+ def onSceneStartClose(self, caller, event):
+ self.parameterSetNode = None
+ self.editor.setSegmentationNode(None)
+ self.editor.removeViewObservations()
+
+ def onSceneEndClose(self, caller, event):
+ if self.parent.isEntered:
+ self.selectParameterNode()
+ self.editor.updateWidgetFromMRML()
+
+ def onSceneEndImport(self, caller, event):
+ if self.parent.isEntered:
+ self.selectParameterNode()
+ self.editor.updateWidgetFromMRML()
+
+ def cleanup(self):
+ self.removeObservers()
+ self.effectFactorySingleton.disconnect('effectRegistered(QString)', self.editorEffectRegistered)
class SegmentEditorTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
"""
- slicer.mrmlScene.Clear(0)
-
- def runTest(self):
- """Currently no testing functionality.
+ This is the test case for your scripted module.
"""
- self.setUp()
- self.test_SegmentEditor1()
- def test_SegmentEditor1(self):
- """Add test here later.
- """
- self.delayDisplay("Starting the test")
- self.delayDisplay('Test passed!')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Currently no testing functionality.
+ """
+ self.setUp()
+ self.test_SegmentEditor1()
+
+ def test_SegmentEditor1(self):
+ """Add test here later.
+ """
+ self.delayDisplay("Starting the test")
+ self.delayDisplay('Test passed!')
diff --git a/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py b/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py
index c56b5c70254..1393d24a63c 100644
--- a/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py
+++ b/Modules/Scripted/SegmentEditor/SubjectHierarchyPlugins/SegmentEditorSubjectHierarchyPlugin.py
@@ -8,136 +8,136 @@
class SegmentEditorSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin):
- """ Scripted subject hierarchy plugin for the Segment Editor module.
-
- This is also an example for scripted plugins, so includes all possible methods.
- The methods that are not needed (i.e. the default implementation in
- qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
- omitted in plugins created based on this one.
- """
-
- # Necessary static member to be able to set python source to scripted subject hierarchy plugin
- filePath = __file__
-
- def __init__(self, scriptedPlugin):
- scriptedPlugin.name = 'SegmentEditor'
- AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
-
- self.segmentEditorAction = qt.QAction("Segment this...", scriptedPlugin)
- self.segmentEditorAction.connect("triggered()", self.onSegment)
-
- def canAddNodeToSubjectHierarchy(self, node, parentItemID):
- # This plugin cannot own any items (it's not a role but a function plugin),
- # but the it can be decided the following way:
- # if node is not None and node.IsA("vtkMRMLMyNode"):
- # return 1.0
- return 0.0
-
- def canOwnSubjectHierarchyItem(self, itemID):
- # This plugin cannot own any items (it's not a role but a function plugin),
- # but the it can be decided the following way:
- # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- # shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- # associatedNode = shNode.GetItemDataNode(itemID)
- # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode"):
- # return 1.0
- return 0.0
-
- def roleForPlugin(self):
- # As this plugin cannot own any items, it doesn't have a role either
- return "N/A"
-
- def helpText(self):
- # return (""
- # ""
- # "SegmentEditor module subject hierarchy help text"
- # ""
- # "
"
- # ""
- # ""
- # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
- # ""
- # "
\n")
- return ""
-
- def icon(self, itemID):
- # As this plugin cannot own any items, it doesn't have an icon either
- # import os
- # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png')
- # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath):
- # return qt.QIcon(iconPath)
- # Item unknown by plugin
- return qt.QIcon()
-
- def visibilityIcon(self, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
-
- def editProperties(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
-
- def itemContextMenuActions(self):
- return [self.segmentEditorAction]
-
- def onSegment(self):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- currentItemID = pluginHandlerSingleton.currentItem()
- if not currentItemID:
- logging.error("Invalid current item")
- return
-
- shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
- volumeNode = shNode.GetItemDataNode(currentItemID)
-
- # Switch to Segment Editor module
- pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentEditor')
- editorWidget = slicer.modules.segmenteditor.widgetRepresentation().self()
-
- # Create new segmentation only if there is no segmentation node, or the current segmentation is not empty
- # (switching to the module will create an empty segmentation if there is none in the scene, but not otherwise)
- segmentationNode = editorWidget.parameterSetNode.GetSegmentationNode()
- if segmentationNode is None or segmentationNode.GetSegmentation().GetNumberOfSegments() > 0:
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- editorWidget.parameterSetNode.SetAndObserveSegmentationNode(segmentationNode)
- # Name segmentation node based on the volume
- segmentationNode.SetName(volumeNode.GetName() + '_Segmentation')
-
- # Set master volume
- editorWidget.parameterSetNode.SetAndObserveMasterVolumeNode(volumeNode)
-
- # Place segmentation under the master volume in subject hierarchy
- segmentationShItemID = shNode.GetItemByDataNode(segmentationNode)
- shNode.SetItemParent(segmentationShItemID, shNode.GetItemParent(currentItemID))
-
- def sceneContextMenuActions(self):
- return []
-
- def showContextMenuActionsForItem(self, itemID):
- # Scene
- if not itemID:
- # No scene context menu actions in this plugin
- return
-
- # Volume but not LabelMap
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- if (pluginHandlerSingleton.pluginByName('Volumes').canOwnSubjectHierarchyItem(itemID)
- and not pluginHandlerSingleton.pluginByName('LabelMaps').canOwnSubjectHierarchyItem(itemID)):
- # Get current item
- currentItemID = pluginHandlerSingleton.currentItem()
- if not currentItemID:
- logging.error("Invalid current item")
- return
- self.segmentEditorAction.visible = True
-
- def tooltip(self, itemID):
- # As this plugin cannot own any items, it doesn't provide tooltip either
- return ""
-
- def setDisplayVisibility(self, itemID, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
-
- def getDisplayVisibility(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
+ """ Scripted subject hierarchy plugin for the Segment Editor module.
+
+ This is also an example for scripted plugins, so includes all possible methods.
+ The methods that are not needed (i.e. the default implementation in
+ qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
+ omitted in plugins created based on this one.
+ """
+
+ # Necessary static member to be able to set python source to scripted subject hierarchy plugin
+ filePath = __file__
+
+ def __init__(self, scriptedPlugin):
+ scriptedPlugin.name = 'SegmentEditor'
+ AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
+
+ self.segmentEditorAction = qt.QAction("Segment this...", scriptedPlugin)
+ self.segmentEditorAction.connect("triggered()", self.onSegment)
+
+ def canAddNodeToSubjectHierarchy(self, node, parentItemID):
+ # This plugin cannot own any items (it's not a role but a function plugin),
+ # but the it can be decided the following way:
+ # if node is not None and node.IsA("vtkMRMLMyNode"):
+ # return 1.0
+ return 0.0
+
+ def canOwnSubjectHierarchyItem(self, itemID):
+ # This plugin cannot own any items (it's not a role but a function plugin),
+ # but the it can be decided the following way:
+ # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ # shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ # associatedNode = shNode.GetItemDataNode(itemID)
+ # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode"):
+ # return 1.0
+ return 0.0
+
+ def roleForPlugin(self):
+ # As this plugin cannot own any items, it doesn't have a role either
+ return "N/A"
+
+ def helpText(self):
+ # return (""
+ # ""
+ # "SegmentEditor module subject hierarchy help text"
+ # ""
+ # "
"
+ # ""
+ # ""
+ # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
+ # ""
+ # "
\n")
+ return ""
+
+ def icon(self, itemID):
+ # As this plugin cannot own any items, it doesn't have an icon either
+ # import os
+ # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png')
+ # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath):
+ # return qt.QIcon(iconPath)
+ # Item unknown by plugin
+ return qt.QIcon()
+
+ def visibilityIcon(self, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
+
+ def editProperties(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
+
+ def itemContextMenuActions(self):
+ return [self.segmentEditorAction]
+
+ def onSegment(self):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ currentItemID = pluginHandlerSingleton.currentItem()
+ if not currentItemID:
+ logging.error("Invalid current item")
+ return
+
+ shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
+ volumeNode = shNode.GetItemDataNode(currentItemID)
+
+ # Switch to Segment Editor module
+ pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentEditor')
+ editorWidget = slicer.modules.segmenteditor.widgetRepresentation().self()
+
+ # Create new segmentation only if there is no segmentation node, or the current segmentation is not empty
+ # (switching to the module will create an empty segmentation if there is none in the scene, but not otherwise)
+ segmentationNode = editorWidget.parameterSetNode.GetSegmentationNode()
+ if segmentationNode is None or segmentationNode.GetSegmentation().GetNumberOfSegments() > 0:
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ editorWidget.parameterSetNode.SetAndObserveSegmentationNode(segmentationNode)
+ # Name segmentation node based on the volume
+ segmentationNode.SetName(volumeNode.GetName() + '_Segmentation')
+
+ # Set master volume
+ editorWidget.parameterSetNode.SetAndObserveMasterVolumeNode(volumeNode)
+
+ # Place segmentation under the master volume in subject hierarchy
+ segmentationShItemID = shNode.GetItemByDataNode(segmentationNode)
+ shNode.SetItemParent(segmentationShItemID, shNode.GetItemParent(currentItemID))
+
+ def sceneContextMenuActions(self):
+ return []
+
+ def showContextMenuActionsForItem(self, itemID):
+ # Scene
+ if not itemID:
+ # No scene context menu actions in this plugin
+ return
+
+ # Volume but not LabelMap
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ if (pluginHandlerSingleton.pluginByName('Volumes').canOwnSubjectHierarchyItem(itemID)
+ and not pluginHandlerSingleton.pluginByName('LabelMaps').canOwnSubjectHierarchyItem(itemID)):
+ # Get current item
+ currentItemID = pluginHandlerSingleton.currentItem()
+ if not currentItemID:
+ logging.error("Invalid current item")
+ return
+ self.segmentEditorAction.visible = True
+
+ def tooltip(self, itemID):
+ # As this plugin cannot own any items, it doesn't provide tooltip either
+ return ""
+
+ def setDisplayVisibility(self, itemID, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
+
+ def getDisplayVisibility(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatistics.py b/Modules/Scripted/SegmentStatistics/SegmentStatistics.py
index 2673b66396f..b120ca1021d 100644
--- a/Modules/Scripted/SegmentStatistics/SegmentStatistics.py
+++ b/Modules/Scripted/SegmentStatistics/SegmentStatistics.py
@@ -11,17 +11,17 @@
class SegmentStatistics(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Segment Statistics"
- self.parent.categories = ["Quantification"]
- self.parent.dependencies = ["SubjectHierarchy"]
- self.parent.contributors = ["Andras Lasso (PerkLab), Christian Bauer (University of Iowa), Steve Pieper (Isomics)"]
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Segment Statistics"
+ self.parent.categories = ["Quantification"]
+ self.parent.dependencies = ["SubjectHierarchy"]
+ self.parent.contributors = ["Andras Lasso (PerkLab), Christian Bauer (University of Iowa), Steve Pieper (Isomics)"]
+ self.parent.helpText = """
Use this module to calculate counts and volumes for segments plus statistics on the grayscale background volume.
Computed fields:
Segment labelmap statistics (LM): voxel count, volume mm3, volume cm3.
@@ -32,247 +32,247 @@ def __init__(self, parent):
Closed surface statistics (CS): surface mm2, volume mm3, volume cm3 (computed from closed surface).
Requires segment closed surface representation.
"""
- self.parent.helpText += parent.defaultDocumentationLink
- self.parent.acknowledgementText = """
+ self.parent.helpText += parent.defaultDocumentationLink
+ self.parent.acknowledgementText = """
Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details.
"""
- def setup(self):
- # Register subject hierarchy plugin
- import SubjectHierarchyPlugins
- scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
- scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentStatisticsSubjectHierarchyPlugin.filePath)
+ def setup(self):
+ # Register subject hierarchy plugin
+ import SubjectHierarchyPlugins
+ scriptedPlugin = slicer.qSlicerSubjectHierarchyScriptedPlugin(None)
+ scriptedPlugin.setPythonSource(SubjectHierarchyPlugins.SegmentStatisticsSubjectHierarchyPlugin.filePath)
class SegmentStatisticsWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- self.logic = SegmentStatisticsLogic()
- self.grayscaleNode = None
- self.labelNode = None
- self.parameterNode = None
- self.parameterNodeObserver = None
-
- # Instantiate and connect widgets ...
- #
-
- # Parameter set selector
- self.parameterNodeSelector = slicer.qMRMLNodeComboBox()
- self.parameterNodeSelector.nodeTypes = ["vtkMRMLScriptedModuleNode"]
- self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "SegmentStatistics")
- self.parameterNodeSelector.selectNodeUponCreation = True
- self.parameterNodeSelector.addEnabled = True
- self.parameterNodeSelector.renameEnabled = True
- self.parameterNodeSelector.removeEnabled = True
- self.parameterNodeSelector.noneEnabled = False
- self.parameterNodeSelector.showHidden = True
- self.parameterNodeSelector.showChildNodeTypes = False
- self.parameterNodeSelector.baseName = "SegmentStatistics"
- self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene)
- self.parameterNodeSelector.setToolTip("Pick parameter set")
- self.layout.addWidget(self.parameterNodeSelector)
-
- # Inputs
- inputsCollapsibleButton = ctk.ctkCollapsibleButton()
- inputsCollapsibleButton.text = "Inputs"
- self.layout.addWidget(inputsCollapsibleButton)
- inputsFormLayout = qt.QFormLayout(inputsCollapsibleButton)
-
- # Segmentation selector
- self.segmentationSelector = slicer.qMRMLNodeComboBox()
- self.segmentationSelector.nodeTypes = ["vtkMRMLSegmentationNode"]
- self.segmentationSelector.addEnabled = False
- self.segmentationSelector.removeEnabled = True
- self.segmentationSelector.renameEnabled = True
- self.segmentationSelector.setMRMLScene(slicer.mrmlScene)
- self.segmentationSelector.setToolTip("Pick the segmentation to compute statistics for")
- inputsFormLayout.addRow("Segmentation:", self.segmentationSelector)
-
- # Scalar volume selector
- self.scalarSelector = slicer.qMRMLNodeComboBox()
- self.scalarSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
- self.scalarSelector.addEnabled = False
- self.scalarSelector.removeEnabled = True
- self.scalarSelector.renameEnabled = True
- self.scalarSelector.noneEnabled = True
- self.scalarSelector.showChildNodeTypes = False
- self.scalarSelector.setMRMLScene(slicer.mrmlScene)
- self.scalarSelector.setToolTip("Select the scalar volume for intensity statistics calculations")
- inputsFormLayout.addRow("Scalar volume:", self.scalarSelector)
-
- # Output table selector
- outputCollapsibleButton = ctk.ctkCollapsibleButton()
- outputCollapsibleButton.text = "Output"
- self.layout.addWidget(outputCollapsibleButton)
- outputFormLayout = qt.QFormLayout(outputCollapsibleButton)
-
- self.outputTableSelector = slicer.qMRMLNodeComboBox()
- self.outputTableSelector.noneDisplay = "Create new table"
- self.outputTableSelector.setMRMLScene(slicer.mrmlScene)
- self.outputTableSelector.nodeTypes = ["vtkMRMLTableNode"]
- self.outputTableSelector.addEnabled = True
- self.outputTableSelector.selectNodeUponCreation = True
- self.outputTableSelector.renameEnabled = True
- self.outputTableSelector.removeEnabled = True
- self.outputTableSelector.noneEnabled = True
- self.outputTableSelector.setToolTip("Select the table where statistics will be saved into")
- self.outputTableSelector.setCurrentNode(None)
-
- outputFormLayout.addRow("Output table:", self.outputTableSelector)
-
- # Parameter set
- parametersCollapsibleButton = ctk.ctkCollapsibleButton()
- parametersCollapsibleButton.text = "Advanced"
- parametersCollapsibleButton.collapsed = True
- self.layout.addWidget(parametersCollapsibleButton)
- self.parametersLayout = qt.QFormLayout(parametersCollapsibleButton)
-
- # Edit parameter set button to open SegmentStatisticsParameterEditorDialog
- # Note: we add the plugins' option widgets to the module widget instead of using the editor dialog
- # self.editParametersButton = qt.QPushButton("Edit Parameter Set")
- # self.editParametersButton.toolTip = "Editor Statistics Plugin Parameter Set."
- # self.parametersLayout.addRow(self.editParametersButton)
- # self.editParametersButton.connect('clicked()', self.onEditParameters)
- # add caclulator's option widgets
- self.addPluginOptionWidgets()
-
- # Apply Button
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Calculate Statistics."
- self.applyButton.enabled = False
- self.parent.layout().addWidget(self.applyButton)
-
- # Add vertical spacer
- self.parent.layout().addStretch(1)
-
- # connections
- self.applyButton.connect('clicked()', self.onApply)
- self.scalarSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
- self.segmentationSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
- self.outputTableSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
- self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
- self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onParameterSetSelected)
-
- self.parameterNodeSelector.setCurrentNode(self.logic.getParameterNode())
- self.onNodeSelectionChanged()
- self.onParameterSetSelected()
-
- def enter(self):
- """Runs whenever the module is reopened
- """
- if self.parameterNodeSelector.currentNode() is None:
- parameterNode = self.logic.getParameterNode()
- slicer.mrmlScene.AddNode(parameterNode)
- self.parameterNodeSelector.setCurrentNode(parameterNode)
- if self.segmentationSelector.currentNode() is None:
- segmentationNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode")
- self.segmentationSelector.setCurrentNode(segmentationNode)
-
- def cleanup(self):
- if self.parameterNode and self.parameterNodeObserver:
- self.parameterNode.RemoveObserver(self.parameterNodeObserver)
-
- def onNodeSelectionChanged(self):
- self.applyButton.enabled = (self.segmentationSelector.currentNode() is not None and
- self.parameterNodeSelector.currentNode() is not None)
- if self.segmentationSelector.currentNode():
- self.outputTableSelector.baseName = self.segmentationSelector.currentNode().GetName() + ' statistics'
-
- def onApply(self):
- """Calculate the label statistics
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
- if not self.outputTableSelector.currentNode():
- newTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode")
- self.outputTableSelector.setCurrentNode(newTable)
- # Lock GUI
- self.applyButton.text = "Working..."
- self.applyButton.setEnabled(False)
- slicer.app.processEvents()
- # set up parameters for computation
- self.logic.getParameterNode().SetParameter("Segmentation", self.segmentationSelector.currentNode().GetID())
- if self.scalarSelector.currentNode():
- self.logic.getParameterNode().SetParameter("ScalarVolume", self.scalarSelector.currentNode().GetID())
- else:
- self.logic.getParameterNode().UnsetParameter("ScalarVolume")
- self.logic.getParameterNode().SetParameter("MeasurementsTable", self.outputTableSelector.currentNode().GetID())
- # Compute statistics
- self.logic.computeStatistics()
- self.logic.exportToTable(self.outputTableSelector.currentNode())
- self.logic.showTable(self.outputTableSelector.currentNode())
-
- # Unlock GUI
- self.applyButton.setEnabled(True)
- self.applyButton.text = "Apply"
-
- def onEditParameters(self, pluginName=None):
- """Open dialog box to edit plugin's parameters"""
- if self.parameterNodeSelector.currentNode():
- SegmentStatisticsParameterEditorDialog.editParameters(self.parameterNodeSelector.currentNode(), pluginName)
-
- def addPluginOptionWidgets(self):
- self.pluginEnabledCheckboxes = {}
- self.parametersLayout.addRow(qt.QLabel("Enabled segment statistics plugins:"))
- for plugin in self.logic.plugins:
- checkbox = qt.QCheckBox(plugin.name + " Statistics")
- checkbox.checked = True
- checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
- optionButton = qt.QPushButton("Options")
- from functools import partial
- optionButton.connect('clicked()', partial(self.onEditParameters, plugin.name))
- editWidget = qt.QWidget()
- editWidget.setLayout(qt.QHBoxLayout())
- editWidget.layout().margin = 0
- editWidget.layout().addWidget(checkbox, 0)
- editWidget.layout().addStretch(1)
- editWidget.layout().addWidget(optionButton, 0)
- self.pluginEnabledCheckboxes[plugin.name] = checkbox
- self.parametersLayout.addRow(editWidget)
- # embed widgets for editing plugin' parameters
- # for plugin in self.logic.plugins:
- # pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox()
- # pluginOptionsCollapsibleButton.setTitle( plugin.name )
- # pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton)
- # pluginOptionsFormLayout.addRow(plugin.optionsWidget)
- # self.parametersLayout.addRow(pluginOptionsCollapsibleButton)
-
- def onParameterSetSelected(self):
- if self.parameterNode and self.parameterNodeObserver:
- self.parameterNode.RemoveObserver(self.parameterNodeObserver)
- self.parameterNode = self.parameterNodeSelector.currentNode()
- if self.parameterNode:
- self.logic.setParameterNode(self.parameterNode)
- self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent,
- self.updateGuiFromParameterNode)
- self.updateGuiFromParameterNode()
-
- def updateGuiFromParameterNode(self, caller=None, event=None):
- if not self.parameterNode:
- return
- for plugin in self.logic.plugins:
- pluginName = plugin.__class__.__name__
- parameter = pluginName + '.enabled'
- checkbox = self.pluginEnabledCheckboxes[plugin.name]
- value = self.parameterNode.GetParameter(parameter) == 'True'
- if checkbox.checked != value:
- previousState = checkbox.blockSignals(True)
- checkbox.checked = value
- checkbox.blockSignals(previousState)
-
- def updateParameterNodeFromGui(self):
- if not self.parameterNode:
- return
- for plugin in self.logic.plugins:
- pluginName = plugin.__class__.__name__
- parameter = pluginName + '.enabled'
- checkbox = self.pluginEnabledCheckboxes[plugin.name]
- self.parameterNode.SetParameter(parameter, str(checkbox.checked))
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ self.logic = SegmentStatisticsLogic()
+ self.grayscaleNode = None
+ self.labelNode = None
+ self.parameterNode = None
+ self.parameterNodeObserver = None
+
+ # Instantiate and connect widgets ...
+ #
+
+ # Parameter set selector
+ self.parameterNodeSelector = slicer.qMRMLNodeComboBox()
+ self.parameterNodeSelector.nodeTypes = ["vtkMRMLScriptedModuleNode"]
+ self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "SegmentStatistics")
+ self.parameterNodeSelector.selectNodeUponCreation = True
+ self.parameterNodeSelector.addEnabled = True
+ self.parameterNodeSelector.renameEnabled = True
+ self.parameterNodeSelector.removeEnabled = True
+ self.parameterNodeSelector.noneEnabled = False
+ self.parameterNodeSelector.showHidden = True
+ self.parameterNodeSelector.showChildNodeTypes = False
+ self.parameterNodeSelector.baseName = "SegmentStatistics"
+ self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene)
+ self.parameterNodeSelector.setToolTip("Pick parameter set")
+ self.layout.addWidget(self.parameterNodeSelector)
+
+ # Inputs
+ inputsCollapsibleButton = ctk.ctkCollapsibleButton()
+ inputsCollapsibleButton.text = "Inputs"
+ self.layout.addWidget(inputsCollapsibleButton)
+ inputsFormLayout = qt.QFormLayout(inputsCollapsibleButton)
+
+ # Segmentation selector
+ self.segmentationSelector = slicer.qMRMLNodeComboBox()
+ self.segmentationSelector.nodeTypes = ["vtkMRMLSegmentationNode"]
+ self.segmentationSelector.addEnabled = False
+ self.segmentationSelector.removeEnabled = True
+ self.segmentationSelector.renameEnabled = True
+ self.segmentationSelector.setMRMLScene(slicer.mrmlScene)
+ self.segmentationSelector.setToolTip("Pick the segmentation to compute statistics for")
+ inputsFormLayout.addRow("Segmentation:", self.segmentationSelector)
+
+ # Scalar volume selector
+ self.scalarSelector = slicer.qMRMLNodeComboBox()
+ self.scalarSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
+ self.scalarSelector.addEnabled = False
+ self.scalarSelector.removeEnabled = True
+ self.scalarSelector.renameEnabled = True
+ self.scalarSelector.noneEnabled = True
+ self.scalarSelector.showChildNodeTypes = False
+ self.scalarSelector.setMRMLScene(slicer.mrmlScene)
+ self.scalarSelector.setToolTip("Select the scalar volume for intensity statistics calculations")
+ inputsFormLayout.addRow("Scalar volume:", self.scalarSelector)
+
+ # Output table selector
+ outputCollapsibleButton = ctk.ctkCollapsibleButton()
+ outputCollapsibleButton.text = "Output"
+ self.layout.addWidget(outputCollapsibleButton)
+ outputFormLayout = qt.QFormLayout(outputCollapsibleButton)
+
+ self.outputTableSelector = slicer.qMRMLNodeComboBox()
+ self.outputTableSelector.noneDisplay = "Create new table"
+ self.outputTableSelector.setMRMLScene(slicer.mrmlScene)
+ self.outputTableSelector.nodeTypes = ["vtkMRMLTableNode"]
+ self.outputTableSelector.addEnabled = True
+ self.outputTableSelector.selectNodeUponCreation = True
+ self.outputTableSelector.renameEnabled = True
+ self.outputTableSelector.removeEnabled = True
+ self.outputTableSelector.noneEnabled = True
+ self.outputTableSelector.setToolTip("Select the table where statistics will be saved into")
+ self.outputTableSelector.setCurrentNode(None)
+
+ outputFormLayout.addRow("Output table:", self.outputTableSelector)
+
+ # Parameter set
+ parametersCollapsibleButton = ctk.ctkCollapsibleButton()
+ parametersCollapsibleButton.text = "Advanced"
+ parametersCollapsibleButton.collapsed = True
+ self.layout.addWidget(parametersCollapsibleButton)
+ self.parametersLayout = qt.QFormLayout(parametersCollapsibleButton)
+
+ # Edit parameter set button to open SegmentStatisticsParameterEditorDialog
+ # Note: we add the plugins' option widgets to the module widget instead of using the editor dialog
+ # self.editParametersButton = qt.QPushButton("Edit Parameter Set")
+ # self.editParametersButton.toolTip = "Editor Statistics Plugin Parameter Set."
+ # self.parametersLayout.addRow(self.editParametersButton)
+ # self.editParametersButton.connect('clicked()', self.onEditParameters)
+ # add caclulator's option widgets
+ self.addPluginOptionWidgets()
+
+ # Apply Button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Calculate Statistics."
+ self.applyButton.enabled = False
+ self.parent.layout().addWidget(self.applyButton)
+
+ # Add vertical spacer
+ self.parent.layout().addStretch(1)
+
+ # connections
+ self.applyButton.connect('clicked()', self.onApply)
+ self.scalarSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
+ self.segmentationSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
+ self.outputTableSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
+ self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onNodeSelectionChanged)
+ self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onParameterSetSelected)
+
+ self.parameterNodeSelector.setCurrentNode(self.logic.getParameterNode())
+ self.onNodeSelectionChanged()
+ self.onParameterSetSelected()
+
+ def enter(self):
+ """Runs whenever the module is reopened
+ """
+ if self.parameterNodeSelector.currentNode() is None:
+ parameterNode = self.logic.getParameterNode()
+ slicer.mrmlScene.AddNode(parameterNode)
+ self.parameterNodeSelector.setCurrentNode(parameterNode)
+ if self.segmentationSelector.currentNode() is None:
+ segmentationNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLSegmentationNode")
+ self.segmentationSelector.setCurrentNode(segmentationNode)
+
+ def cleanup(self):
+ if self.parameterNode and self.parameterNodeObserver:
+ self.parameterNode.RemoveObserver(self.parameterNodeObserver)
+
+ def onNodeSelectionChanged(self):
+ self.applyButton.enabled = (self.segmentationSelector.currentNode() is not None and
+ self.parameterNodeSelector.currentNode() is not None)
+ if self.segmentationSelector.currentNode():
+ self.outputTableSelector.baseName = self.segmentationSelector.currentNode().GetName() + ' statistics'
+
+ def onApply(self):
+ """Calculate the label statistics
+ """
+
+ with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
+ if not self.outputTableSelector.currentNode():
+ newTable = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTableNode")
+ self.outputTableSelector.setCurrentNode(newTable)
+ # Lock GUI
+ self.applyButton.text = "Working..."
+ self.applyButton.setEnabled(False)
+ slicer.app.processEvents()
+ # set up parameters for computation
+ self.logic.getParameterNode().SetParameter("Segmentation", self.segmentationSelector.currentNode().GetID())
+ if self.scalarSelector.currentNode():
+ self.logic.getParameterNode().SetParameter("ScalarVolume", self.scalarSelector.currentNode().GetID())
+ else:
+ self.logic.getParameterNode().UnsetParameter("ScalarVolume")
+ self.logic.getParameterNode().SetParameter("MeasurementsTable", self.outputTableSelector.currentNode().GetID())
+ # Compute statistics
+ self.logic.computeStatistics()
+ self.logic.exportToTable(self.outputTableSelector.currentNode())
+ self.logic.showTable(self.outputTableSelector.currentNode())
+
+ # Unlock GUI
+ self.applyButton.setEnabled(True)
+ self.applyButton.text = "Apply"
+
+ def onEditParameters(self, pluginName=None):
+ """Open dialog box to edit plugin's parameters"""
+ if self.parameterNodeSelector.currentNode():
+ SegmentStatisticsParameterEditorDialog.editParameters(self.parameterNodeSelector.currentNode(), pluginName)
+
+ def addPluginOptionWidgets(self):
+ self.pluginEnabledCheckboxes = {}
+ self.parametersLayout.addRow(qt.QLabel("Enabled segment statistics plugins:"))
+ for plugin in self.logic.plugins:
+ checkbox = qt.QCheckBox(plugin.name + " Statistics")
+ checkbox.checked = True
+ checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
+ optionButton = qt.QPushButton("Options")
+ from functools import partial
+ optionButton.connect('clicked()', partial(self.onEditParameters, plugin.name))
+ editWidget = qt.QWidget()
+ editWidget.setLayout(qt.QHBoxLayout())
+ editWidget.layout().margin = 0
+ editWidget.layout().addWidget(checkbox, 0)
+ editWidget.layout().addStretch(1)
+ editWidget.layout().addWidget(optionButton, 0)
+ self.pluginEnabledCheckboxes[plugin.name] = checkbox
+ self.parametersLayout.addRow(editWidget)
+ # embed widgets for editing plugin' parameters
+ # for plugin in self.logic.plugins:
+ # pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox()
+ # pluginOptionsCollapsibleButton.setTitle( plugin.name )
+ # pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton)
+ # pluginOptionsFormLayout.addRow(plugin.optionsWidget)
+ # self.parametersLayout.addRow(pluginOptionsCollapsibleButton)
+
+ def onParameterSetSelected(self):
+ if self.parameterNode and self.parameterNodeObserver:
+ self.parameterNode.RemoveObserver(self.parameterNodeObserver)
+ self.parameterNode = self.parameterNodeSelector.currentNode()
+ if self.parameterNode:
+ self.logic.setParameterNode(self.parameterNode)
+ self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent,
+ self.updateGuiFromParameterNode)
+ self.updateGuiFromParameterNode()
+
+ def updateGuiFromParameterNode(self, caller=None, event=None):
+ if not self.parameterNode:
+ return
+ for plugin in self.logic.plugins:
+ pluginName = plugin.__class__.__name__
+ parameter = pluginName + '.enabled'
+ checkbox = self.pluginEnabledCheckboxes[plugin.name]
+ value = self.parameterNode.GetParameter(parameter) == 'True'
+ if checkbox.checked != value:
+ previousState = checkbox.blockSignals(True)
+ checkbox.checked = value
+ checkbox.blockSignals(previousState)
+
+ def updateParameterNodeFromGui(self):
+ if not self.parameterNode:
+ return
+ for plugin in self.logic.plugins:
+ pluginName = plugin.__class__.__name__
+ parameter = pluginName + '.enabled'
+ checkbox = self.pluginEnabledCheckboxes[plugin.name]
+ self.parameterNode.SetParameter(parameter, str(checkbox.checked))
class SegmentStatisticsParameterEditorDialog(qt.QDialog):
@@ -282,691 +282,691 @@ class SegmentStatisticsParameterEditorDialog(qt.QDialog):
@staticmethod
def editParameters(parameterNode, pluginName=None):
- """Executes a modal dialog to edit a segment statistics parameter node if a pluginName is specified, only
- options for this plugin are displayed"
- """
- dialog = SegmentStatisticsParameterEditorDialog(parent=None, parameterNode=parameterNode,
- pluginName=pluginName)
- return dialog.exec_()
+ """Executes a modal dialog to edit a segment statistics parameter node if a pluginName is specified, only
+ options for this plugin are displayed"
+ """
+ dialog = SegmentStatisticsParameterEditorDialog(parent=None, parameterNode=parameterNode,
+ pluginName=pluginName)
+ return dialog.exec_()
def __init__(self, parent=None, parameterNode=None, pluginName=None):
- super(qt.QDialog, self).__init__(parent)
- self.title = "Edit Segment Statistics Parameters"
- self.parameterNode = parameterNode
- self.pluginName = pluginName
- self.logic = SegmentStatisticsLogic() # for access to plugins and editor widgets
- self.logic.setParameterNode(self.parameterNode)
- self.setup()
+ super(qt.QDialog, self).__init__(parent)
+ self.title = "Edit Segment Statistics Parameters"
+ self.parameterNode = parameterNode
+ self.pluginName = pluginName
+ self.logic = SegmentStatisticsLogic() # for access to plugins and editor widgets
+ self.logic.setParameterNode(self.parameterNode)
+ self.setup()
def setParameterNode(self, parameterNode):
- """Set the parameter node the dialog will operate on"""
- if parameterNode == self.parameterNode:
- return
- self.parameterNode = parameterNode
- self.logic.setParameterNode(self.parameterNode)
+ """Set the parameter node the dialog will operate on"""
+ if parameterNode == self.parameterNode:
+ return
+ self.parameterNode = parameterNode
+ self.logic.setParameterNode(self.parameterNode)
def setup(self):
- self.setLayout(qt.QVBoxLayout())
-
- self.descriptionLabel = qt.QLabel("Edit segment statistics plugin parameters:", 0)
-
- self.doneButton = qt.QPushButton("Done")
- self.doneButton.toolTip = "Finish editing."
- doneWidget = qt.QWidget(self)
- doneWidget.setLayout(qt.QHBoxLayout())
- doneWidget.layout().addStretch(1)
- doneWidget.layout().addWidget(self.doneButton, 0)
-
- parametersScrollArea = qt.QScrollArea(self)
- self.parametersWidget = qt.QWidget(parametersScrollArea)
- self.parametersLayout = qt.QFormLayout(self.parametersWidget)
- self._addPluginOptionWidgets()
- parametersScrollArea.setWidget(self.parametersWidget)
- parametersScrollArea.widgetResizable = True
- parametersScrollArea.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
- parametersScrollArea.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
-
- self.layout().addWidget(self.descriptionLabel, 0)
- self.layout().addWidget(parametersScrollArea, 1)
- self.layout().addWidget(doneWidget, 0)
- self.doneButton.connect('clicked()', lambda: self.done(1))
+ self.setLayout(qt.QVBoxLayout())
+
+ self.descriptionLabel = qt.QLabel("Edit segment statistics plugin parameters:", 0)
+
+ self.doneButton = qt.QPushButton("Done")
+ self.doneButton.toolTip = "Finish editing."
+ doneWidget = qt.QWidget(self)
+ doneWidget.setLayout(qt.QHBoxLayout())
+ doneWidget.layout().addStretch(1)
+ doneWidget.layout().addWidget(self.doneButton, 0)
+
+ parametersScrollArea = qt.QScrollArea(self)
+ self.parametersWidget = qt.QWidget(parametersScrollArea)
+ self.parametersLayout = qt.QFormLayout(self.parametersWidget)
+ self._addPluginOptionWidgets()
+ parametersScrollArea.setWidget(self.parametersWidget)
+ parametersScrollArea.widgetResizable = True
+ parametersScrollArea.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
+ parametersScrollArea.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAsNeeded)
+
+ self.layout().addWidget(self.descriptionLabel, 0)
+ self.layout().addWidget(parametersScrollArea, 1)
+ self.layout().addWidget(doneWidget, 0)
+ self.doneButton.connect('clicked()', lambda: self.done(1))
def _addPluginOptionWidgets(self):
- description = "Edit segment statistics plugin parameters:"
- if self.pluginName:
- description = "Edit " + self.pluginName + " plugin parameters:"
- self.descriptionLabel.text = description
- if self.pluginName:
- for plugin in self.logic.plugins:
- if plugin.name == self.pluginName:
- self.parametersLayout.addRow(plugin.optionsWidget)
- else:
- for plugin in self.logic.plugins:
- pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox(self.parametersWidget)
- pluginOptionsCollapsibleButton.setTitle(plugin.name)
- pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton)
- pluginOptionsFormLayout.addRow(plugin.optionsWidget)
- self.parametersLayout.addRow(pluginOptionsCollapsibleButton)
+ description = "Edit segment statistics plugin parameters:"
+ if self.pluginName:
+ description = "Edit " + self.pluginName + " plugin parameters:"
+ self.descriptionLabel.text = description
+ if self.pluginName:
+ for plugin in self.logic.plugins:
+ if plugin.name == self.pluginName:
+ self.parametersLayout.addRow(plugin.optionsWidget)
+ else:
+ for plugin in self.logic.plugins:
+ pluginOptionsCollapsibleButton = ctk.ctkCollapsibleGroupBox(self.parametersWidget)
+ pluginOptionsCollapsibleButton.setTitle(plugin.name)
+ pluginOptionsFormLayout = qt.QFormLayout(pluginOptionsCollapsibleButton)
+ pluginOptionsFormLayout.addRow(plugin.optionsWidget)
+ self.parametersLayout.addRow(pluginOptionsCollapsibleButton)
class SegmentStatisticsLogic(ScriptedLoadableModuleLogic):
- """Implement the logic to calculate label statistics.
- Nodes are passed in as arguments.
- Results are stored as 'statistics' instance variable.
- Additional plugins for computation of other statistical measurements may be registered.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
- registeredPlugins = [LabelmapSegmentStatisticsPlugin, ScalarVolumeSegmentStatisticsPlugin,
- ClosedSurfaceSegmentStatisticsPlugin]
-
- @staticmethod
- def registerPlugin(plugin):
- """Register a subclass of SegmentStatisticsPluginBase for calculation of additional measurements"""
- if not isinstance(plugin, SegmentStatisticsPluginBase):
- return
- for key in plugin.keys:
- if key.count(".") > 0:
- logging.warning("Plugin keys should not contain extra '.' as it might mix pluginname.measurementkey in "
- "the parameter node")
- if not plugin.__class__ in SegmentStatisticsLogic.registeredPlugins:
- SegmentStatisticsLogic.registeredPlugins.append(plugin.__class__)
- else:
- logging.warning("SegmentStatisticsLogic.registerPlugin will not register plugin because \
- another plugin with the same name has already been registered")
-
- def __init__(self, parent=None):
- ScriptedLoadableModuleLogic.__init__(self, parent)
- self.plugins = [x() for x in SegmentStatisticsLogic.registeredPlugins]
-
- self.isSingletonParameterNode = False
- self.parameterNode = None
-
- self.keys = ["Segment"]
- self.notAvailableValueString = ""
- self.reset()
-
- def getParameterNode(self):
- """Returns the current parameter node and creates one if it doesn't exist yet"""
- if not self.parameterNode:
- self.setParameterNode(ScriptedLoadableModuleLogic.getParameterNode(self))
- return self.parameterNode
-
- def setParameterNode(self, parameterNode):
- """Set the current parameter node and initialize all unset parameters to their default values"""
- if self.parameterNode == parameterNode:
- return
- self.setDefaultParameters(parameterNode)
- self.parameterNode = parameterNode
- for plugin in self.plugins:
- plugin.setParameterNode(parameterNode)
-
- def setDefaultParameters(self, parameterNode):
- """Set all plugins to enabled and all plugins' parameters to their default value"""
- for plugin in self.plugins:
- plugin.setDefaultParameters(parameterNode)
- if not parameterNode.GetParameter('visibleSegmentsOnly'):
- parameterNode.SetParameter('visibleSegmentsOnly', str(True))
-
- def getStatistics(self):
- """Get the calculated statistical measurements"""
- params = self.getParameterNode()
- if not hasattr(params, 'statistics'):
- params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}}
- return params.statistics
-
- def reset(self):
- """Clear all computation results"""
- self.keys = ["Segment"]
- for plugin in self.plugins:
- self.keys += [plugin.toLongKey(k) for k in plugin.keys]
- params = self.getParameterNode()
- params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}}
-
- def computeStatistics(self):
- """Compute statistical measures for all (visible) segments"""
- self.reset()
-
- segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
- transformedSegmentationNode = None
- try:
- if not segmentationNode.GetParentTransformNode() is None:
- # Create a temporary segmentation and harden the transform to ensure that the statistics are calculated
- # in world coordinates
- transformedSegmentationNode = slicer.vtkMRMLSegmentationNode()
- transformedSegmentationNode.Copy(segmentationNode)
- transformedSegmentationNode.HideFromEditorsOn()
- slicer.mrmlScene.AddNode(transformedSegmentationNode)
- transformedSegmentationNode.HardenTransform()
- self.getParameterNode().SetParameter("Segmentation", transformedSegmentationNode.GetID())
-
- # Get segment ID list
- visibleSegmentIds = vtk.vtkStringArray()
- if self.getParameterNode().GetParameter('visibleSegmentsOnly') == 'True':
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
- else:
- segmentationNode.GetSegmentation().GetSegmentIDs(visibleSegmentIds)
- if visibleSegmentIds.GetNumberOfValues() == 0:
- logging.debug("computeStatistics will not return any results: there are no visible segments")
-
- # update statistics for all segment IDs
- for segmentIndex in range(visibleSegmentIds.GetNumberOfValues()):
- segmentID = visibleSegmentIds.GetValue(segmentIndex)
- self.updateStatisticsForSegment(segmentID)
- finally:
- if not transformedSegmentationNode is None:
- # We made a copy and hardened the segmentation transform
- self.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- slicer.mrmlScene.RemoveNode(transformedSegmentationNode)
-
- def updateStatisticsForSegment(self, segmentID):
- """
- Update statistical measures for specified segment.
- Note: This will not change or reset measurement results of other segments
+ """Implement the logic to calculate label statistics.
+ Nodes are passed in as arguments.
+ Results are stored as 'statistics' instance variable.
+ Additional plugins for computation of other statistical measurements may be registered.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
+ registeredPlugins = [LabelmapSegmentStatisticsPlugin, ScalarVolumeSegmentStatisticsPlugin,
+ ClosedSurfaceSegmentStatisticsPlugin]
- segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
-
- if not segmentationNode.GetSegmentation().GetSegment(segmentID):
- logging.debug("updateStatisticsForSegment will not update any results because the segment doesn't exist")
- return
-
- segment = segmentationNode.GetSegmentation().GetSegment(segmentID)
- statistics = self.getStatistics()
- if segmentID not in statistics["SegmentIDs"]:
- statistics["SegmentIDs"].append(segmentID)
- statistics[segmentID, "Segment"] = segment.GetName()
-
- # apply all enabled plugins
- for plugin in self.plugins:
- pluginName = plugin.__class__.__name__
- if self.getParameterNode().GetParameter(pluginName + '.enabled') == 'True':
- stats = plugin.computeStatistics(segmentID)
- for key in stats:
- statistics[segmentID, pluginName + '.' + key] = stats[key]
- statistics["MeasurementInfo"][pluginName + '.' + key] = plugin.getMeasurementInfo(key)
-
- def getPluginByKey(self, key):
- """Get plugin responsible for obtaining measurement value for given key"""
- for plugin in self.plugins:
- if plugin.toShortKey(key) in plugin.keys:
- return plugin
- return None
-
- def getMeasurementInfo(self, key):
- """Get information (name, description, units, ...) about the measurement for the given key"""
- plugin = self.getPluginByKey(key)
- if plugin:
- return plugin.getMeasurementInfo(plugin.toShortKey(key))
- return None
-
- def getStatisticsValueAsString(self, segmentID, key):
- statistics = self.getStatistics()
- if (segmentID, key) in statistics:
- value = statistics[segmentID, key]
- if isinstance(value, float):
- return "%0.3f" % value # round to 3 decimals
- else:
- return str(value)
- else:
- return self.notAvailableValueString
-
- def getNonEmptyKeys(self):
- # Fill columns
- statistics = self.getStatistics()
- nonEmptyKeys = []
- for key in self.keys:
- for segmentID in statistics["SegmentIDs"]:
- if (segmentID, key) in statistics:
- nonEmptyKeys.append(key)
- break
- return nonEmptyKeys
-
- def getHeaderNames(self, nonEmptyKeysOnly=True):
- # Derive column header names based on: (a) DICOM information if present,
- # (b) measurement info name if present (c) measurement key as fallback.
- # Duplicate names get a postfix [1][2]... to make them unique
- # Initial and unique column header names are returned
- keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
- statistics = self.getStatistics()
- headerNames = []
- for key in keys:
- name = key
- info = statistics['MeasurementInfo'][key] if key in statistics['MeasurementInfo'] else {}
- entry = slicer.vtkCodedEntry()
- dicomBasedName = False
- if info:
- if 'DICOM.DerivationCode' in info and info['DICOM.DerivationCode']:
- entry.SetFromString(info['DICOM.DerivationCode'])
- name = entry.GetCodeMeaning()
- dicomBasedName = True
- elif 'DICOM.QuantityCode' in info and info['DICOM.QuantityCode']:
- entry.SetFromString(info['DICOM.QuantityCode'])
- name = entry.GetCodeMeaning()
- dicomBasedName = True
- elif 'name' in info and info['name']:
- name = info['name']
- if dicomBasedName and 'DICOM.UnitsCode' in info and info['DICOM.UnitsCode']:
- entry.SetFromString(info['DICOM.UnitsCode'])
- units = entry.GetCodeValue()
- if len(units) > 0 and units[0] == '[' and units[-1] == ']': units = units[1:-1]
- if len(units) > 0: name += ' [' + units + ']'
- elif 'units' in info and info['units'] and len(info['units']) > 0:
- units = info['units']
- name += ' [' + units + ']'
- headerNames.append(name)
- uniqueHeaderNames = list(headerNames)
- for duplicateName in {name for name in uniqueHeaderNames if uniqueHeaderNames.count(name) > 1}:
- j = 1
- for i in range(len(uniqueHeaderNames)):
- if uniqueHeaderNames[i] == duplicateName:
- uniqueHeaderNames[i] = duplicateName + ' (' + str(j) + ')'
- j += 1
- headerNames = {keys[i]: headerNames[i] for i in range(len(keys))}
- uniqueHeaderNames = {keys[i]: uniqueHeaderNames[i] for i in range(len(keys))}
- return headerNames, uniqueHeaderNames
-
- def exportToTable(self, table, nonEmptyKeysOnly=True):
- """
- Export statistics to table node
- """
- tableWasModified = table.StartModify()
- table.RemoveAllColumns()
-
- keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
- columnHeaderNames, uniqueColumnHeaderNames = self.getHeaderNames(nonEmptyKeysOnly)
-
- # Define table columns
- statistics = self.getStatistics()
- for key in keys:
- # create table column appropriate for data type; currently supported: float, int, long, string
- measurements = [statistics[segmentID, key] for segmentID in statistics["SegmentIDs"] if
- (segmentID, key) in statistics]
- if len(measurements) == 0: # there were not measurements and therefore use the default "string" representation
- col = table.AddColumn()
- elif isinstance(measurements[0], int):
- col = table.AddColumn(vtk.vtkLongArray())
- elif isinstance(measurements[0], float):
- col = table.AddColumn(vtk.vtkDoubleArray())
- elif isinstance(measurements[0], list):
- length = len(measurements[0])
- if length == 0:
- col = table.AddColumn()
- else:
- value = measurements[0][0]
- if isinstance(value, int):
- array = vtk.vtkLongArray()
- array.SetNumberOfComponents(length)
- col = table.AddColumn(array)
- elif isinstance(value, float):
- array = vtk.vtkDoubleArray()
- array.SetNumberOfComponents(length)
- col = table.AddColumn(array)
- else:
- col = table.AddColumn()
- else: # default
- col = table.AddColumn()
- plugin = self.getPluginByKey(key)
- columnName = uniqueColumnHeaderNames[key]
- longColumnName = columnHeaderNames[key]
- col.SetName(columnName)
- if plugin:
- table.SetColumnProperty(columnName, "Plugin", plugin.name)
- longColumnName += '
Computed by ' + plugin.name + ' Statistics plugin'
- table.SetColumnLongName(columnName, longColumnName)
- measurementInfo = statistics["MeasurementInfo"][key] if key in statistics["MeasurementInfo"] else {}
- if measurementInfo:
- for mik, miv in measurementInfo.items():
- if mik == 'description':
- table.SetColumnDescription(columnName, str(miv))
- elif mik == 'units':
- table.SetColumnUnitLabel(columnName, str(miv))
- elif mik == 'componentNames':
- componentNames = miv
- array = table.GetTable().GetColumnByName(columnName)
- componentIndex = 0
- for componentName in miv:
- array.SetComponentName(componentIndex, componentName)
- componentIndex += 1
- else:
- table.SetColumnProperty(columnName, str(mik), str(miv))
-
- # Fill columns
- for segmentID in statistics["SegmentIDs"]:
- rowIndex = table.AddEmptyRow()
- columnIndex = 0
- for key in keys:
- value = statistics[segmentID, key] if (segmentID, key) in statistics else None
- if value is None and key != 'Segment':
- value = float('nan')
- if isinstance(value, list):
- for i in range(len(value)):
- table.GetTable().GetColumn(columnIndex).SetComponent(rowIndex, i, value[i])
+ @staticmethod
+ def registerPlugin(plugin):
+ """Register a subclass of SegmentStatisticsPluginBase for calculation of additional measurements"""
+ if not isinstance(plugin, SegmentStatisticsPluginBase):
+ return
+ for key in plugin.keys:
+ if key.count(".") > 0:
+ logging.warning("Plugin keys should not contain extra '.' as it might mix pluginname.measurementkey in "
+ "the parameter node")
+ if not plugin.__class__ in SegmentStatisticsLogic.registeredPlugins:
+ SegmentStatisticsLogic.registeredPlugins.append(plugin.__class__)
else:
- table.GetTable().GetColumn(columnIndex).SetValue(rowIndex, value)
- columnIndex += 1
+ logging.warning("SegmentStatisticsLogic.registerPlugin will not register plugin because \
+ another plugin with the same name has already been registered")
- table.Modified()
- table.EndModify(tableWasModified)
+ def __init__(self, parent=None):
+ ScriptedLoadableModuleLogic.__init__(self, parent)
+ self.plugins = [x() for x in SegmentStatisticsLogic.registeredPlugins]
- def showTable(self, table):
- """
- Switch to a layout where tables are visible and show the selected table
- """
- currentLayout = slicer.app.layoutManager().layout
- layoutWithTable = slicer.modules.tables.logic().GetLayoutWithTable(currentLayout)
- slicer.app.layoutManager().setLayout(layoutWithTable)
- slicer.app.applicationLogic().GetSelectionNode().SetActiveTableID(table.GetID())
- slicer.app.applicationLogic().PropagateTableSelection()
+ self.isSingletonParameterNode = False
+ self.parameterNode = None
- def exportToString(self, nonEmptyKeysOnly=True):
- """
- Returns string with comma separated values, with header keys in quotes.
- """
- keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
- # Header
- csv = '"' + '","'.join(keys) + '"'
- # Rows
- statistics = self.getStatistics()
- for segmentID in statistics["SegmentIDs"]:
- csv += "\n" + str(statistics[segmentID, keys[0]])
- for key in keys[1:]:
+ self.keys = ["Segment"]
+ self.notAvailableValueString = ""
+ self.reset()
+
+ def getParameterNode(self):
+ """Returns the current parameter node and creates one if it doesn't exist yet"""
+ if not self.parameterNode:
+ self.setParameterNode(ScriptedLoadableModuleLogic.getParameterNode(self))
+ return self.parameterNode
+
+ def setParameterNode(self, parameterNode):
+ """Set the current parameter node and initialize all unset parameters to their default values"""
+ if self.parameterNode == parameterNode:
+ return
+ self.setDefaultParameters(parameterNode)
+ self.parameterNode = parameterNode
+ for plugin in self.plugins:
+ plugin.setParameterNode(parameterNode)
+
+ def setDefaultParameters(self, parameterNode):
+ """Set all plugins to enabled and all plugins' parameters to their default value"""
+ for plugin in self.plugins:
+ plugin.setDefaultParameters(parameterNode)
+ if not parameterNode.GetParameter('visibleSegmentsOnly'):
+ parameterNode.SetParameter('visibleSegmentsOnly', str(True))
+
+ def getStatistics(self):
+ """Get the calculated statistical measurements"""
+ params = self.getParameterNode()
+ if not hasattr(params, 'statistics'):
+ params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}}
+ return params.statistics
+
+ def reset(self):
+ """Clear all computation results"""
+ self.keys = ["Segment"]
+ for plugin in self.plugins:
+ self.keys += [plugin.toLongKey(k) for k in plugin.keys]
+ params = self.getParameterNode()
+ params.statistics = {"SegmentIDs": [], "MeasurementInfo": {}}
+
+ def computeStatistics(self):
+ """Compute statistical measures for all (visible) segments"""
+ self.reset()
+
+ segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
+ transformedSegmentationNode = None
+ try:
+ if not segmentationNode.GetParentTransformNode() is None:
+ # Create a temporary segmentation and harden the transform to ensure that the statistics are calculated
+ # in world coordinates
+ transformedSegmentationNode = slicer.vtkMRMLSegmentationNode()
+ transformedSegmentationNode.Copy(segmentationNode)
+ transformedSegmentationNode.HideFromEditorsOn()
+ slicer.mrmlScene.AddNode(transformedSegmentationNode)
+ transformedSegmentationNode.HardenTransform()
+ self.getParameterNode().SetParameter("Segmentation", transformedSegmentationNode.GetID())
+
+ # Get segment ID list
+ visibleSegmentIds = vtk.vtkStringArray()
+ if self.getParameterNode().GetParameter('visibleSegmentsOnly') == 'True':
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
+ else:
+ segmentationNode.GetSegmentation().GetSegmentIDs(visibleSegmentIds)
+ if visibleSegmentIds.GetNumberOfValues() == 0:
+ logging.debug("computeStatistics will not return any results: there are no visible segments")
+
+ # update statistics for all segment IDs
+ for segmentIndex in range(visibleSegmentIds.GetNumberOfValues()):
+ segmentID = visibleSegmentIds.GetValue(segmentIndex)
+ self.updateStatisticsForSegment(segmentID)
+ finally:
+ if not transformedSegmentationNode is None:
+ # We made a copy and hardened the segmentation transform
+ self.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ slicer.mrmlScene.RemoveNode(transformedSegmentationNode)
+
+ def updateStatisticsForSegment(self, segmentID):
+ """
+ Update statistical measures for specified segment.
+ Note: This will not change or reset measurement results of other segments
+ """
+
+ segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
+
+ if not segmentationNode.GetSegmentation().GetSegment(segmentID):
+ logging.debug("updateStatisticsForSegment will not update any results because the segment doesn't exist")
+ return
+
+ segment = segmentationNode.GetSegmentation().GetSegment(segmentID)
+ statistics = self.getStatistics()
+ if segmentID not in statistics["SegmentIDs"]:
+ statistics["SegmentIDs"].append(segmentID)
+ statistics[segmentID, "Segment"] = segment.GetName()
+
+ # apply all enabled plugins
+ for plugin in self.plugins:
+ pluginName = plugin.__class__.__name__
+ if self.getParameterNode().GetParameter(pluginName + '.enabled') == 'True':
+ stats = plugin.computeStatistics(segmentID)
+ for key in stats:
+ statistics[segmentID, pluginName + '.' + key] = stats[key]
+ statistics["MeasurementInfo"][pluginName + '.' + key] = plugin.getMeasurementInfo(key)
+
+ def getPluginByKey(self, key):
+ """Get plugin responsible for obtaining measurement value for given key"""
+ for plugin in self.plugins:
+ if plugin.toShortKey(key) in plugin.keys:
+ return plugin
+ return None
+
+ def getMeasurementInfo(self, key):
+ """Get information (name, description, units, ...) about the measurement for the given key"""
+ plugin = self.getPluginByKey(key)
+ if plugin:
+ return plugin.getMeasurementInfo(plugin.toShortKey(key))
+ return None
+
+ def getStatisticsValueAsString(self, segmentID, key):
+ statistics = self.getStatistics()
if (segmentID, key) in statistics:
- csv += "," + str(statistics[segmentID, key])
+ value = statistics[segmentID, key]
+ if isinstance(value, float):
+ return "%0.3f" % value # round to 3 decimals
+ else:
+ return str(value)
else:
- csv += ","
- return csv
-
- def exportToCSVFile(self, fileName, nonEmptyKeysOnly=True):
- fp = open(fileName, "w")
- fp.write(self.exportToString(nonEmptyKeysOnly))
- fp.close()
+ return self.notAvailableValueString
+
+ def getNonEmptyKeys(self):
+ # Fill columns
+ statistics = self.getStatistics()
+ nonEmptyKeys = []
+ for key in self.keys:
+ for segmentID in statistics["SegmentIDs"]:
+ if (segmentID, key) in statistics:
+ nonEmptyKeys.append(key)
+ break
+ return nonEmptyKeys
+
+ def getHeaderNames(self, nonEmptyKeysOnly=True):
+ # Derive column header names based on: (a) DICOM information if present,
+ # (b) measurement info name if present (c) measurement key as fallback.
+ # Duplicate names get a postfix [1][2]... to make them unique
+ # Initial and unique column header names are returned
+ keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
+ statistics = self.getStatistics()
+ headerNames = []
+ for key in keys:
+ name = key
+ info = statistics['MeasurementInfo'][key] if key in statistics['MeasurementInfo'] else {}
+ entry = slicer.vtkCodedEntry()
+ dicomBasedName = False
+ if info:
+ if 'DICOM.DerivationCode' in info and info['DICOM.DerivationCode']:
+ entry.SetFromString(info['DICOM.DerivationCode'])
+ name = entry.GetCodeMeaning()
+ dicomBasedName = True
+ elif 'DICOM.QuantityCode' in info and info['DICOM.QuantityCode']:
+ entry.SetFromString(info['DICOM.QuantityCode'])
+ name = entry.GetCodeMeaning()
+ dicomBasedName = True
+ elif 'name' in info and info['name']:
+ name = info['name']
+ if dicomBasedName and 'DICOM.UnitsCode' in info and info['DICOM.UnitsCode']:
+ entry.SetFromString(info['DICOM.UnitsCode'])
+ units = entry.GetCodeValue()
+ if len(units) > 0 and units[0] == '[' and units[-1] == ']': units = units[1:-1]
+ if len(units) > 0: name += ' [' + units + ']'
+ elif 'units' in info and info['units'] and len(info['units']) > 0:
+ units = info['units']
+ name += ' [' + units + ']'
+ headerNames.append(name)
+ uniqueHeaderNames = list(headerNames)
+ for duplicateName in {name for name in uniqueHeaderNames if uniqueHeaderNames.count(name) > 1}:
+ j = 1
+ for i in range(len(uniqueHeaderNames)):
+ if uniqueHeaderNames[i] == duplicateName:
+ uniqueHeaderNames[i] = duplicateName + ' (' + str(j) + ')'
+ j += 1
+ headerNames = {keys[i]: headerNames[i] for i in range(len(keys))}
+ uniqueHeaderNames = {keys[i]: uniqueHeaderNames[i] for i in range(len(keys))}
+ return headerNames, uniqueHeaderNames
+
+ def exportToTable(self, table, nonEmptyKeysOnly=True):
+ """
+ Export statistics to table node
+ """
+ tableWasModified = table.StartModify()
+ table.RemoveAllColumns()
+
+ keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
+ columnHeaderNames, uniqueColumnHeaderNames = self.getHeaderNames(nonEmptyKeysOnly)
+
+ # Define table columns
+ statistics = self.getStatistics()
+ for key in keys:
+ # create table column appropriate for data type; currently supported: float, int, long, string
+ measurements = [statistics[segmentID, key] for segmentID in statistics["SegmentIDs"] if
+ (segmentID, key) in statistics]
+ if len(measurements) == 0: # there were not measurements and therefore use the default "string" representation
+ col = table.AddColumn()
+ elif isinstance(measurements[0], int):
+ col = table.AddColumn(vtk.vtkLongArray())
+ elif isinstance(measurements[0], float):
+ col = table.AddColumn(vtk.vtkDoubleArray())
+ elif isinstance(measurements[0], list):
+ length = len(measurements[0])
+ if length == 0:
+ col = table.AddColumn()
+ else:
+ value = measurements[0][0]
+ if isinstance(value, int):
+ array = vtk.vtkLongArray()
+ array.SetNumberOfComponents(length)
+ col = table.AddColumn(array)
+ elif isinstance(value, float):
+ array = vtk.vtkDoubleArray()
+ array.SetNumberOfComponents(length)
+ col = table.AddColumn(array)
+ else:
+ col = table.AddColumn()
+ else: # default
+ col = table.AddColumn()
+ plugin = self.getPluginByKey(key)
+ columnName = uniqueColumnHeaderNames[key]
+ longColumnName = columnHeaderNames[key]
+ col.SetName(columnName)
+ if plugin:
+ table.SetColumnProperty(columnName, "Plugin", plugin.name)
+ longColumnName += '
Computed by ' + plugin.name + ' Statistics plugin'
+ table.SetColumnLongName(columnName, longColumnName)
+ measurementInfo = statistics["MeasurementInfo"][key] if key in statistics["MeasurementInfo"] else {}
+ if measurementInfo:
+ for mik, miv in measurementInfo.items():
+ if mik == 'description':
+ table.SetColumnDescription(columnName, str(miv))
+ elif mik == 'units':
+ table.SetColumnUnitLabel(columnName, str(miv))
+ elif mik == 'componentNames':
+ componentNames = miv
+ array = table.GetTable().GetColumnByName(columnName)
+ componentIndex = 0
+ for componentName in miv:
+ array.SetComponentName(componentIndex, componentName)
+ componentIndex += 1
+ else:
+ table.SetColumnProperty(columnName, str(mik), str(miv))
+
+ # Fill columns
+ for segmentID in statistics["SegmentIDs"]:
+ rowIndex = table.AddEmptyRow()
+ columnIndex = 0
+ for key in keys:
+ value = statistics[segmentID, key] if (segmentID, key) in statistics else None
+ if value is None and key != 'Segment':
+ value = float('nan')
+ if isinstance(value, list):
+ for i in range(len(value)):
+ table.GetTable().GetColumn(columnIndex).SetComponent(rowIndex, i, value[i])
+ else:
+ table.GetTable().GetColumn(columnIndex).SetValue(rowIndex, value)
+ columnIndex += 1
+
+ table.Modified()
+ table.EndModify(tableWasModified)
+
+ def showTable(self, table):
+ """
+ Switch to a layout where tables are visible and show the selected table
+ """
+ currentLayout = slicer.app.layoutManager().layout
+ layoutWithTable = slicer.modules.tables.logic().GetLayoutWithTable(currentLayout)
+ slicer.app.layoutManager().setLayout(layoutWithTable)
+ slicer.app.applicationLogic().GetSelectionNode().SetActiveTableID(table.GetID())
+ slicer.app.applicationLogic().PropagateTableSelection()
+
+ def exportToString(self, nonEmptyKeysOnly=True):
+ """
+ Returns string with comma separated values, with header keys in quotes.
+ """
+ keys = self.getNonEmptyKeys() if nonEmptyKeysOnly else self.keys
+ # Header
+ csv = '"' + '","'.join(keys) + '"'
+ # Rows
+ statistics = self.getStatistics()
+ for segmentID in statistics["SegmentIDs"]:
+ csv += "\n" + str(statistics[segmentID, keys[0]])
+ for key in keys[1:]:
+ if (segmentID, key) in statistics:
+ csv += "," + str(statistics[segmentID, key])
+ else:
+ csv += ","
+ return csv
+
+ def exportToCSVFile(self, fileName, nonEmptyKeysOnly=True):
+ fp = open(fileName, "w")
+ fp.write(self.exportToString(nonEmptyKeysOnly))
+ fp.close()
class SegmentStatisticsTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear(0)
-
- def runTest(self, scenario=None):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_SegmentStatisticsBasic()
-
- self.setUp()
- self.test_SegmentStatisticsPlugins()
-
- def test_SegmentStatisticsBasic(self):
"""
- This tests some aspects of the label statistics
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting test_SegmentStatisticsBasic")
-
- import SampleData
- from SegmentStatistics import SegmentStatisticsLogic
-
- self.delayDisplay("Load master volume")
-
- masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
-
- self.delayDisplay("Create segmentation containing a few spheres")
-
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- segmentationNode.CreateDefaultDisplayNodes()
- segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
-
- # Geometry for each segment is defined by: radius, posX, posY, posZ
- segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64],
- [12, 31, 33, 27], [17, -42, 30, 27]]
- for segmentGeometry in segmentGeometries:
- sphereSource = vtk.vtkSphereSource()
- sphereSource.SetRadius(segmentGeometry[0])
- sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
- sphereSource.Update()
- uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test")
- segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID)
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
- self.delayDisplay("Compute statistics")
-
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
- segStatLogic.computeStatistics()
-
- self.delayDisplay("Check a few numerical results")
- self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
- self.assertEqual(segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel_count"], 380)
-
- self.delayDisplay("Export results to table")
- resultsTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(resultsTableNode)
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
-
- self.delayDisplay("Export results to string")
- logging.info(segStatLogic.exportToString())
-
- outputFilename = slicer.app.temporaryPath + '/SegmentStatisticsTestOutput.csv'
- self.delayDisplay("Export results to CSV file: " + outputFilename)
- segStatLogic.exportToCSVFile(outputFilename)
-
- self.delayDisplay('test_SegmentStatisticsBasic passed!')
-
- def test_SegmentStatisticsPlugins(self):
- """
- This tests some aspects of the segment statistics plugins
- """
-
- self.delayDisplay("Starting test_SegmentStatisticsPlugins")
-
- import vtkSegmentationCorePython as vtkSegmentationCore
- import SampleData
- from SegmentStatistics import SegmentStatisticsLogic
-
- self.delayDisplay("Load master volume")
-
- masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
-
- self.delayDisplay("Create segmentation containing a few spheres")
-
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- segmentationNode.CreateDefaultDisplayNodes()
- segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
-
- # Geometry for each segment is defined by: radius, posX, posY, posZ
- segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64],
- [12, 31, 33, 27], [17, -42, 30, 27]]
- for segmentGeometry in segmentGeometries:
- sphereSource = vtk.vtkSphereSource()
- sphereSource.SetRadius(segmentGeometry[0])
- sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
- sphereSource.Update()
- segment = vtkSegmentationCore.vtkSegment()
- uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test")
- segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID)
-
- # test calculating only measurements for selected segments
- self.delayDisplay("Test calculating only measurements for individual segments")
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
- segStatLogic.updateStatisticsForSegment('Test_2')
- resultsTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(resultsTableNode)
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
- with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel count"]
- # assert there are no result for this segment
- segStatLogic.updateStatisticsForSegment('Test_4')
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
- self.assertEqual(segStatLogic.getStatistics()["Test_4", "LabelmapSegmentStatisticsPlugin.voxel_count"], 380)
- with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_5", "ScalarVolumeSegmentStatisticsPlugin.voxel count"]
- # assert there are no result for this segment
-
- # calculate measurements for all segments
- segStatLogic.computeStatistics()
- self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
- self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281)
-
- # test updating measurements for segments one by one
- self.delayDisplay("Update some segments in the segmentation")
- segmentGeometriesNew = [[5, -6, 30, 28], [21, 0, 65, 32]]
- # We add/remove representations, so we temporarily block segment modifications
- # to make sure display managers don't try to access data while it is in an
- # inconsistent state.
- wasModified = segmentationNode.StartModify()
- for i in range(len(segmentGeometriesNew)):
- segmentGeometry = segmentGeometriesNew[i]
- sphereSource = vtk.vtkSphereSource()
- sphereSource.SetRadius(segmentGeometry[0])
- sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
- sphereSource.Update()
- segment = segmentationNode.GetSegmentation().GetNthSegment(i)
- segment.RemoveAllRepresentations()
- closedSurfaceName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
- segment.AddRepresentation(closedSurfaceName,
- sphereSource.GetOutput())
- segmentationNode.EndModify(wasModified)
- self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
- self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281)
- segStatLogic.updateStatisticsForSegment('Test_1')
- self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
- self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281)
- segStatLogic.updateStatisticsForSegment('Test')
- self.assertTrue(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 2948)
- self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281)
-
- # test enabling/disabling of individual measurements
- self.delayDisplay("Test disabling of individual measurements")
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(False))
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(False))
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertFalse('Number of voxels [voxels] (1)' in columnHeaders)
- self.assertTrue('Volume [mm3] (1)' in columnHeaders)
- self.assertFalse('Volume [cm3] (3)' in columnHeaders)
-
- self.delayDisplay("Test re-enabling of individual measurements")
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(True))
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(True))
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders)
- self.assertTrue('Volume [mm3] (1)' in columnHeaders)
- self.assertTrue('Volume [cm3] (1)' in columnHeaders)
-
- # test enabling/disabling of individual plugins
- self.delayDisplay("Test disabling of plugin")
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(False))
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertFalse('Number of voxels [voxels] (3)' in columnHeaders)
- self.assertFalse('Volume [mm3] (3)' in columnHeaders)
- self.assertTrue('Volume [mm3] (2)' in columnHeaders)
-
- self.delayDisplay("Test re-enabling of plugin")
- segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(True))
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders)
- self.assertTrue('Volume [mm3] (3)' in columnHeaders)
-
- # test unregistering/registering of plugins
- self.delayDisplay("Test of removing all registered plugins")
- SegmentStatisticsLogic.registeredPlugins = [] # remove all registered plugins
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertEqual(len(columnHeaders), 1) # only header element should be "Segment"
- self.assertEqual(columnHeaders[0], "Segment") # only header element should be "Segment"
-
- self.delayDisplay("Test registering plugins")
- SegmentStatisticsLogic.registerPlugin(LabelmapSegmentStatisticsPlugin())
- SegmentStatisticsLogic.registerPlugin(ScalarVolumeSegmentStatisticsPlugin())
- SegmentStatisticsLogic.registerPlugin(ClosedSurfaceSegmentStatisticsPlugin())
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
- segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
- segStatLogic.computeStatistics()
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
- columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
- self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders)
- self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders)
- self.assertTrue('Surface area [mm2]' in columnHeaders)
-
- self.delayDisplay('test_SegmentStatisticsPlugins passed!')
+ def runTest(self, scenario=None):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_SegmentStatisticsBasic()
+
+ self.setUp()
+ self.test_SegmentStatisticsPlugins()
+
+ def test_SegmentStatisticsBasic(self):
+ """
+ This tests some aspects of the label statistics
+ """
+
+ self.delayDisplay("Starting test_SegmentStatisticsBasic")
+
+ import SampleData
+ from SegmentStatistics import SegmentStatisticsLogic
+
+ self.delayDisplay("Load master volume")
+
+ masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
+
+ self.delayDisplay("Create segmentation containing a few spheres")
+
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ segmentationNode.CreateDefaultDisplayNodes()
+ segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
+
+ # Geometry for each segment is defined by: radius, posX, posY, posZ
+ segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64],
+ [12, 31, 33, 27], [17, -42, 30, 27]]
+ for segmentGeometry in segmentGeometries:
+ sphereSource = vtk.vtkSphereSource()
+ sphereSource.SetRadius(segmentGeometry[0])
+ sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
+ sphereSource.Update()
+ uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test")
+ segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID)
+
+ self.delayDisplay("Compute statistics")
+
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
+ segStatLogic.computeStatistics()
+
+ self.delayDisplay("Check a few numerical results")
+ self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
+ self.assertEqual(segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel_count"], 380)
+
+ self.delayDisplay("Export results to table")
+ resultsTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(resultsTableNode)
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+
+ self.delayDisplay("Export results to string")
+ logging.info(segStatLogic.exportToString())
+
+ outputFilename = slicer.app.temporaryPath + '/SegmentStatisticsTestOutput.csv'
+ self.delayDisplay("Export results to CSV file: " + outputFilename)
+ segStatLogic.exportToCSVFile(outputFilename)
+
+ self.delayDisplay('test_SegmentStatisticsBasic passed!')
+
+ def test_SegmentStatisticsPlugins(self):
+ """
+ This tests some aspects of the segment statistics plugins
+ """
+
+ self.delayDisplay("Starting test_SegmentStatisticsPlugins")
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ import SampleData
+ from SegmentStatistics import SegmentStatisticsLogic
+
+ self.delayDisplay("Load master volume")
+
+ masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
+
+ self.delayDisplay("Create segmentation containing a few spheres")
+
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ segmentationNode.CreateDefaultDisplayNodes()
+ segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
+
+ # Geometry for each segment is defined by: radius, posX, posY, posZ
+ segmentGeometries = [[10, -6, 30, 28], [20, 0, 65, 32], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 64],
+ [12, 31, 33, 27], [17, -42, 30, 27]]
+ for segmentGeometry in segmentGeometries:
+ sphereSource = vtk.vtkSphereSource()
+ sphereSource.SetRadius(segmentGeometry[0])
+ sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
+ sphereSource.Update()
+ segment = vtkSegmentationCore.vtkSegment()
+ uniqueSegmentID = segmentationNode.GetSegmentation().GenerateUniqueSegmentID("Test")
+ segmentationNode.AddSegmentFromClosedSurfaceRepresentation(sphereSource.GetOutput(), uniqueSegmentID)
+
+ # test calculating only measurements for selected segments
+ self.delayDisplay("Test calculating only measurements for individual segments")
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
+ segStatLogic.updateStatisticsForSegment('Test_2')
+ resultsTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(resultsTableNode)
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
+ with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_4", "ScalarVolumeSegmentStatisticsPlugin.voxel count"]
+ # assert there are no result for this segment
+ segStatLogic.updateStatisticsForSegment('Test_4')
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ self.assertEqual(segStatLogic.getStatistics()["Test_2", "LabelmapSegmentStatisticsPlugin.voxel_count"], 9807)
+ self.assertEqual(segStatLogic.getStatistics()["Test_4", "LabelmapSegmentStatisticsPlugin.voxel_count"], 380)
+ with self.assertRaises(KeyError): segStatLogic.getStatistics()["Test_5", "ScalarVolumeSegmentStatisticsPlugin.voxel count"]
+ # assert there are no result for this segment
+
+ # calculate measurements for all segments
+ segStatLogic.computeStatistics()
+ self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
+ self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281)
+
+ # test updating measurements for segments one by one
+ self.delayDisplay("Update some segments in the segmentation")
+ segmentGeometriesNew = [[5, -6, 30, 28], [21, 0, 65, 32]]
+ # We add/remove representations, so we temporarily block segment modifications
+ # to make sure display managers don't try to access data while it is in an
+ # inconsistent state.
+ wasModified = segmentationNode.StartModify()
+ for i in range(len(segmentGeometriesNew)):
+ segmentGeometry = segmentGeometriesNew[i]
+ sphereSource = vtk.vtkSphereSource()
+ sphereSource.SetRadius(segmentGeometry[0])
+ sphereSource.SetCenter(segmentGeometry[1], segmentGeometry[2], segmentGeometry[3])
+ sphereSource.Update()
+ segment = segmentationNode.GetSegmentation().GetNthSegment(i)
+ segment.RemoveAllRepresentations()
+ closedSurfaceName = vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName()
+ segment.AddRepresentation(closedSurfaceName,
+ sphereSource.GetOutput())
+ segmentationNode.EndModify(wasModified)
+ self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
+ self.assertEqual(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"], 23281)
+ segStatLogic.updateStatisticsForSegment('Test_1')
+ self.assertEqual(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"], 2948)
+ self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281)
+ segStatLogic.updateStatisticsForSegment('Test')
+ self.assertTrue(segStatLogic.getStatistics()["Test", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 2948)
+ self.assertTrue(segStatLogic.getStatistics()["Test_1", "LabelmapSegmentStatisticsPlugin.voxel_count"] != 23281)
+
+ # test enabling/disabling of individual measurements
+ self.delayDisplay("Test disabling of individual measurements")
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(False))
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(False))
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertFalse('Number of voxels [voxels] (1)' in columnHeaders)
+ self.assertTrue('Volume [mm3] (1)' in columnHeaders)
+ self.assertFalse('Volume [cm3] (3)' in columnHeaders)
+
+ self.delayDisplay("Test re-enabling of individual measurements")
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.voxel_count.enabled", str(True))
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.volume_cm3.enabled", str(True))
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders)
+ self.assertTrue('Volume [mm3] (1)' in columnHeaders)
+ self.assertTrue('Volume [cm3] (1)' in columnHeaders)
+
+ # test enabling/disabling of individual plugins
+ self.delayDisplay("Test disabling of plugin")
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(False))
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertFalse('Number of voxels [voxels] (3)' in columnHeaders)
+ self.assertFalse('Volume [mm3] (3)' in columnHeaders)
+ self.assertTrue('Volume [mm3] (2)' in columnHeaders)
+
+ self.delayDisplay("Test re-enabling of plugin")
+ segStatLogic.getParameterNode().SetParameter("LabelmapSegmentStatisticsPlugin.enabled", str(True))
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders)
+ self.assertTrue('Volume [mm3] (3)' in columnHeaders)
+
+ # test unregistering/registering of plugins
+ self.delayDisplay("Test of removing all registered plugins")
+ SegmentStatisticsLogic.registeredPlugins = [] # remove all registered plugins
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertEqual(len(columnHeaders), 1) # only header element should be "Segment"
+ self.assertEqual(columnHeaders[0], "Segment") # only header element should be "Segment"
+
+ self.delayDisplay("Test registering plugins")
+ SegmentStatisticsLogic.registerPlugin(LabelmapSegmentStatisticsPlugin())
+ SegmentStatisticsLogic.registerPlugin(ScalarVolumeSegmentStatisticsPlugin())
+ SegmentStatisticsLogic.registerPlugin(ClosedSurfaceSegmentStatisticsPlugin())
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.getParameterNode().SetParameter("Segmentation", segmentationNode.GetID())
+ segStatLogic.getParameterNode().SetParameter("ScalarVolume", masterVolumeNode.GetID())
+ segStatLogic.computeStatistics()
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+ columnHeaders = [resultsTableNode.GetColumnName(i) for i in range(resultsTableNode.GetNumberOfColumns())]
+ self.assertTrue('Number of voxels [voxels] (1)' in columnHeaders)
+ self.assertTrue('Number of voxels [voxels] (2)' in columnHeaders)
+ self.assertTrue('Surface area [mm2]' in columnHeaders)
+
+ self.delayDisplay('test_SegmentStatisticsPlugins passed!')
class Slicelet:
- """A slicer slicelet is a module widget that comes up in stand alone mode
- implemented as a python class.
- This class provides common wrapper functionality used by all slicer modlets.
- """
- # TODO: put this in a SliceletLib
- # TODO: parse command line args
-
- def __init__(self, widgetClass=None):
- self.parent = qt.QFrame()
- self.parent.setLayout(qt.QVBoxLayout())
-
- # TODO: should have way to pop up python interactor
- self.buttons = qt.QFrame()
- self.buttons.setLayout(qt.QHBoxLayout())
- self.parent.layout().addWidget(self.buttons)
- self.addDataButton = qt.QPushButton("Add Data")
- self.buttons.layout().addWidget(self.addDataButton)
- self.addDataButton.connect("clicked()", slicer.app.ioManager().openAddDataDialog)
- self.loadSceneButton = qt.QPushButton("Load Scene")
- self.buttons.layout().addWidget(self.loadSceneButton)
- self.loadSceneButton.connect("clicked()", slicer.app.ioManager().openLoadSceneDialog)
-
- if widgetClass:
- self.widget = widgetClass(self.parent)
- self.widget.setup()
- self.parent.show()
+ """A slicer slicelet is a module widget that comes up in stand alone mode
+ implemented as a python class.
+ This class provides common wrapper functionality used by all slicer modlets.
+ """
+ # TODO: put this in a SliceletLib
+ # TODO: parse command line args
+
+ def __init__(self, widgetClass=None):
+ self.parent = qt.QFrame()
+ self.parent.setLayout(qt.QVBoxLayout())
+
+ # TODO: should have way to pop up python interactor
+ self.buttons = qt.QFrame()
+ self.buttons.setLayout(qt.QHBoxLayout())
+ self.parent.layout().addWidget(self.buttons)
+ self.addDataButton = qt.QPushButton("Add Data")
+ self.buttons.layout().addWidget(self.addDataButton)
+ self.addDataButton.connect("clicked()", slicer.app.ioManager().openAddDataDialog)
+ self.loadSceneButton = qt.QPushButton("Load Scene")
+ self.buttons.layout().addWidget(self.loadSceneButton)
+ self.loadSceneButton.connect("clicked()", slicer.app.ioManager().openLoadSceneDialog)
+
+ if widgetClass:
+ self.widget = widgetClass(self.parent)
+ self.widget.setup()
+ self.parent.show()
class SegmentStatisticsSlicelet(Slicelet):
- """ Creates the interface when module is run as a stand alone gui app.
- """
+ """ Creates the interface when module is run as a stand alone gui app.
+ """
- def __init__(self):
- super().__init__(SegmentStatisticsWidget)
+ def __init__(self):
+ super().__init__(SegmentStatisticsWidget)
if __name__ == "__main__":
- # TODO: need a way to access and parse command line arguments
- # TODO: ideally command line args should handle --xml
+ # TODO: need a way to access and parse command line arguments
+ # TODO: ideally command line args should handle --xml
- import sys
- print(sys.argv)
+ import sys
+ print(sys.argv)
- slicelet = SegmentStatisticsSlicelet()
+ slicelet = SegmentStatisticsSlicelet()
diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py
index 454603c6dbd..5514b2ef2a6 100644
--- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py
+++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ClosedSurfaceSegmentStatisticsPlugin.py
@@ -4,68 +4,68 @@
class ClosedSurfaceSegmentStatisticsPlugin(SegmentStatisticsPluginBase):
- """Statistical plugin for closed surfaces"""
+ """Statistical plugin for closed surfaces"""
- def __init__(self):
- super().__init__()
- self.name = "Closed Surface"
- self.keys = ["surface_mm2", "volume_mm3", "volume_cm3"]
- self.defaultKeys = self.keys # calculate all measurements by default
- # ... developer may add extra options to configure other parameters
+ def __init__(self):
+ super().__init__()
+ self.name = "Closed Surface"
+ self.keys = ["surface_mm2", "volume_mm3", "volume_cm3"]
+ self.defaultKeys = self.keys # calculate all measurements by default
+ # ... developer may add extra options to configure other parameters
- def computeStatistics(self, segmentID):
- import vtkSegmentationCorePython as vtkSegmentationCore
- requestedKeys = self.getRequestedKeys()
+ def computeStatistics(self, segmentID):
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ requestedKeys = self.getRequestedKeys()
- segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
+ segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
- if len(requestedKeys) == 0:
- return {}
+ if len(requestedKeys) == 0:
+ return {}
- containsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
- vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
- if not containsClosedSurfaceRepresentation:
- return {}
+ containsClosedSurfaceRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
+ vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName())
+ if not containsClosedSurfaceRepresentation:
+ return {}
- segmentClosedSurface = vtk.vtkPolyData()
- segmentationNode.GetClosedSurfaceRepresentation(segmentID, segmentClosedSurface)
+ segmentClosedSurface = vtk.vtkPolyData()
+ segmentationNode.GetClosedSurfaceRepresentation(segmentID, segmentClosedSurface)
- # Compute statistics
- massProperties = vtk.vtkMassProperties()
- massProperties.SetInputData(segmentClosedSurface)
+ # Compute statistics
+ massProperties = vtk.vtkMassProperties()
+ massProperties.SetInputData(segmentClosedSurface)
- # Add data to statistics list
- ccPerCubicMM = 0.001
- stats = {}
- if "surface_mm2" in requestedKeys:
- stats["surface_mm2"] = massProperties.GetSurfaceArea()
- if "volume_mm3" in requestedKeys:
- stats["volume_mm3"] = massProperties.GetVolume()
- if "volume_cm3" in requestedKeys:
- stats["volume_cm3"] = massProperties.GetVolume() * ccPerCubicMM
- return stats
+ # Add data to statistics list
+ ccPerCubicMM = 0.001
+ stats = {}
+ if "surface_mm2" in requestedKeys:
+ stats["surface_mm2"] = massProperties.GetSurfaceArea()
+ if "volume_mm3" in requestedKeys:
+ stats["volume_mm3"] = massProperties.GetVolume()
+ if "volume_cm3" in requestedKeys:
+ stats["volume_cm3"] = massProperties.GetVolume() * ccPerCubicMM
+ return stats
- def getMeasurementInfo(self, key):
- """Get information (name, description, units, ...) about the measurement for the given key"""
- info = dict()
+ def getMeasurementInfo(self, key):
+ """Get information (name, description, units, ...) about the measurement for the given key"""
+ info = dict()
- # I searched BioPortal, and found seemingly most suitable code.
- # Prefixed with "99" since CHEMINF is not a recognized DICOM coding scheme.
- # See https://bioportal.bioontology.org/ontologies/CHEMINF?p=classes&conceptid=http%3A%2F%2Fsemanticscience.org%2Fresource%2FCHEMINF_000247
- #
- info["surface_mm2"] = \
- self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2",
- quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True),
- unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True))
+ # I searched BioPortal, and found seemingly most suitable code.
+ # Prefixed with "99" since CHEMINF is not a recognized DICOM coding scheme.
+ # See https://bioportal.bioontology.org/ontologies/CHEMINF?p=classes&conceptid=http%3A%2F%2Fsemanticscience.org%2Fresource%2FCHEMINF_000247
+ #
+ info["surface_mm2"] = \
+ self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2",
+ quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True),
+ unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True))
- info["volume_mm3"] = \
- self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
+ info["volume_mm3"] = \
+ self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
- info["volume_cm3"] = \
- self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True))
+ info["volume_cm3"] = \
+ self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True))
- return info[key] if key in info else None
+ return info[key] if key in info else None
diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py
index f4041ac581d..3ec4239adfc 100644
--- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py
+++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/LabelmapSegmentStatisticsPlugin.py
@@ -7,468 +7,468 @@
class LabelmapSegmentStatisticsPlugin(SegmentStatisticsPluginBase):
- """Statistical plugin for Labelmaps"""
-
- def __init__(self):
- super().__init__()
- self.name = "Labelmap"
- self.obbKeys = ["obb_origin_ras", "obb_diameter_mm", "obb_direction_ras_x", "obb_direction_ras_y", "obb_direction_ras_z"]
- self.principalAxisKeys = ["principal_axis_x", "principal_axis_y", "principal_axis_z"]
- self.shapeKeys = [
- "centroid_ras", "feret_diameter_mm", "surface_area_mm2", "roundness", "flatness", "elongation",
- "principal_moments",
- ] + self.principalAxisKeys + self.obbKeys
-
- self.defaultKeys = ["voxel_count", "volume_mm3", "volume_cm3"] # Don't calculate label shape statistics by default since they take longer to compute
- self.keys = self.defaultKeys + self.shapeKeys
- self.keyToShapeStatisticNames = {
- "centroid_ras": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Centroid),
- "feret_diameter_mm": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.FeretDiameter),
- "surface_area_mm2": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Perimeter),
- "roundness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Roundness),
- "flatness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Flatness),
- "elongation": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Elongation),
- "oriented_bounding_box": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.OrientedBoundingBox),
- "obb_origin_ras": "OrientedBoundingBoxOrigin",
- "obb_diameter_mm": "OrientedBoundingBoxSize",
- "obb_direction_ras_x": "OrientedBoundingBoxDirectionX",
- "obb_direction_ras_y": "OrientedBoundingBoxDirectionY",
- "obb_direction_ras_z": "OrientedBoundingBoxDirectionZ",
- "principal_moments": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalMoments),
- "principal_axes": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalAxes),
- "principal_axis_x": "PrincipalAxisX",
- "principal_axis_y": "PrincipalAxisY",
- "principal_axis_z": "PrincipalAxisZ",
- }
- # ... developer may add extra options to configure other parameters
-
- def computeStatistics(self, segmentID):
- import vtkSegmentationCorePython as vtkSegmentationCore
- requestedKeys = self.getRequestedKeys()
-
- segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
-
- if len(requestedKeys) == 0:
- return {}
-
- containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
- vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
- if not containsLabelmapRepresentation:
- return {}
-
- segmentLabelmap = slicer.vtkOrientedImageData()
- segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap)
- if (not segmentLabelmap
- or not segmentLabelmap.GetPointData()
- or not segmentLabelmap.GetPointData().GetScalars()):
- # No input label data
- return {}
-
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(segmentLabelmap)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
- thresh.Update()
-
- # Use binary labelmap as a stencil
- stencil = vtk.vtkImageToImageStencil()
- stencil.SetInputData(thresh.GetOutput())
- stencil.ThresholdByUpper(labelValue)
- stencil.Update()
-
- stat = vtk.vtkImageAccumulate()
- stat.SetInputData(thresh.GetOutput())
- stat.SetStencilData(stencil.GetOutput())
- stat.Update()
-
- # Add data to statistics list
- cubicMMPerVoxel = reduce(lambda x, y: x * y, segmentLabelmap.GetSpacing())
- ccPerCubicMM = 0.001
- stats = {}
- if "voxel_count" in requestedKeys:
- stats["voxel_count"] = stat.GetVoxelCount()
- if "volume_mm3" in requestedKeys:
- stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel
- if "volume_cm3" in requestedKeys:
- stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM
-
- calculateShapeStats = False
- for shapeKey in self.shapeKeys:
- if shapeKey in requestedKeys:
- calculateShapeStats = True
- break
-
- if calculateShapeStats:
- directions = vtk.vtkMatrix4x4()
- segmentLabelmap.GetDirectionMatrix(directions)
-
- # Remove oriented bounding box from requested keys and replace with individual keys
- requestedOptions = requestedKeys
- statFilterOptions = self.shapeKeys
- calculateOBB = (
- "obb_diameter_mm" in requestedKeys or
- "obb_origin_ras" in requestedKeys or
- "obb_direction_ras_x" in requestedKeys or
- "obb_direction_ras_y" in requestedKeys or
- "obb_direction_ras_z" in requestedKeys
- )
-
- if calculateOBB:
- temp = statFilterOptions
- statFilterOptions = []
- for option in temp:
- if not option in self.obbKeys:
- statFilterOptions.append(option)
- statFilterOptions.append("oriented_bounding_box")
-
- temp = requestedOptions
- requestedOptions = []
- for option in temp:
- if not option in self.obbKeys:
- requestedOptions.append(option)
- requestedOptions.append("oriented_bounding_box")
-
- calculatePrincipalAxis = (
- "principal_axis_x" in requestedKeys or
- "principal_axis_y" in requestedKeys or
- "principal_axis_z" in requestedKeys
- )
- if calculatePrincipalAxis:
- temp = statFilterOptions
- statFilterOptions = []
- for option in temp:
- if not option in self.principalAxisKeys:
- statFilterOptions.append(option)
- statFilterOptions.append("principal_axes")
-
- temp = requestedOptions
- requestedOptions = []
- for option in temp:
- if not option in self.principalAxisKeys:
- requestedOptions.append(option)
- requestedOptions.append("principal_axes")
- requestedOptions.append("centroid_ras")
-
- shapeStat = vtkITK.vtkITKLabelShapeStatistics()
- shapeStat.SetInputData(thresh.GetOutput())
- shapeStat.SetDirections(directions)
- for shapeKey in statFilterOptions:
- shapeStat.SetComputeShapeStatistic(self.keyToShapeStatisticNames[shapeKey], shapeKey in requestedOptions)
- shapeStat.Update()
-
- # If segmentation node is transformed, apply that transform to get RAS coordinates
- transformSegmentToRas = vtk.vtkGeneralTransform()
- slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), None, transformSegmentToRas)
-
- statTable = shapeStat.GetOutput()
- if "centroid_ras" in requestedKeys:
- centroidRAS = [0, 0, 0]
- centroidTuple = None
- centroidArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
- if centroidArray is None:
- logging.error("Could not calculate centroid_ras!")
- else:
- centroidTuple = centroidArray.GetTuple(0)
- if centroidTuple is not None:
- transformSegmentToRas.TransformPoint(centroidTuple, centroidRAS)
- stats["centroid_ras"] = centroidRAS
-
- if "roundness" in requestedKeys:
- roundnessTuple = None
- roundnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["roundness"])
- if roundnessArray is None:
- logging.error("Could not calculate roundness!")
- else:
- roundnessTuple = roundnessArray.GetTuple(0)
- if roundnessTuple is not None:
- roundness = roundnessTuple[0]
- stats["roundness"] = roundness
-
- if "flatness" in requestedKeys:
- flatnessTuple = None
- flatnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["flatness"])
- if flatnessArray is None:
- logging.error("Could not calculate flatness!")
- else:
- flatnessTuple = flatnessArray.GetTuple(0)
- if flatnessTuple is not None:
- flatness = flatnessTuple[0]
- stats["flatness"] = flatness
-
- if "elongation" in requestedKeys:
- elongationTuple = None
- elongationArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["elongation"])
- if elongationArray is None:
- logging.error("Could not calculate elongation!")
- else:
- elongationTuple = elongationArray.GetTuple(0)
- if elongationTuple is not None:
- elongation = elongationTuple[0]
- stats["elongation"] = elongation
-
- if "feret_diameter_mm" in requestedKeys:
- feretDiameterTuple = None
- feretDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["feret_diameter_mm"])
- if feretDiameterArray is None:
- logging.error("Could not calculate feret_diameter_mm!")
- else:
- feretDiameterTuple = feretDiameterArray.GetTuple(0)
- if feretDiameterTuple is not None:
- feretDiameter = feretDiameterTuple[0]
- stats["feret_diameter_mm"] = feretDiameter
-
- if "surface_area_mm2" in requestedKeys:
- perimeterTuple = None
- perimeterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["surface_area_mm2"])
- if perimeterArray is None:
- logging.error("Could not calculate surface_area_mm2!")
- else:
- perimeterTuple = perimeterArray.GetTuple(0)
- if perimeterTuple is not None:
- perimeter = perimeterTuple[0]
- stats["surface_area_mm2"] = perimeter
-
- if "obb_origin_ras" in requestedKeys:
- obbOriginTuple = None
- obbOriginRAS = [0, 0, 0]
- obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
- if obbOriginArray is None:
- logging.error("Could not calculate obb_origin_ras!")
- else:
- obbOriginTuple = obbOriginArray.GetTuple(0)
- if obbOriginTuple is not None:
- transformSegmentToRas.TransformPoint(obbOriginTuple, obbOriginRAS)
- stats["obb_origin_ras"] = obbOriginRAS
-
- if "obb_diameter_mm" in requestedKeys:
- obbDiameterMMTuple = None
- obbDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_diameter_mm"])
- if obbDiameterArray is None:
- logging.error("Could not calculate obb_diameter_mm!")
- else:
- obbDiameterMMTuple = obbDiameterArray.GetTuple(0)
- if obbDiameterMMTuple is not None:
- obbDiameterMM = list(obbDiameterMMTuple)
- stats["obb_diameter_mm"] = obbDiameterMM
-
- if "obb_direction_ras_x" in requestedKeys:
- obbOriginTuple = None
- obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
- if obbOriginArray is None:
- logging.error("Could not calculate obb_direction_ras_x!")
- else:
- obbOriginTuple = obbOriginArray.GetTuple(0)
-
- obbDirectionXTuple = None
- obbDirectionXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_x"])
- if obbDirectionXArray is None:
- logging.error("Could not calculate obb_direction_ras_x!")
- else:
- obbDirectionXTuple = obbDirectionXArray.GetTuple(0)
-
- if obbOriginTuple is not None and obbDirectionXTuple is not None:
- obbDirectionX = list(obbDirectionXTuple)
- transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionX, obbDirectionX)
- stats["obb_direction_ras_x"] = obbDirectionX
-
- if "obb_direction_ras_y" in requestedKeys:
- obbOriginTuple = None
- obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
- if obbOriginArray is None:
- logging.error("Could not calculate obb_direction_ras_y!")
- else:
- obbOriginTuple = obbOriginArray.GetTuple(0)
-
- obbDirectionYTuple = None
- obbDirectionYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_y"])
- if obbDirectionYArray is None:
- logging.error("Could not calculate obb_direction_ras_y!")
- else:
- obbDirectionYTuple = obbDirectionYArray.GetTuple(0)
-
- if obbOriginTuple is not None and obbDirectionYTuple is not None:
- obbDirectionY = list(obbDirectionYTuple)
- transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionY, obbDirectionY)
- stats["obb_direction_ras_y"] = obbDirectionY
-
- if "obb_direction_ras_z" in requestedKeys:
- obbOriginTuple = None
- obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
- if obbOriginArray is None:
- logging.error("Could not calculate obb_direction_ras_z!")
- else:
- obbOriginTuple = obbOriginArray.GetTuple(0)
-
- obbDirectionZTuple = None
- obbDirectionZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_z"])
- if obbDirectionZArray is None:
- logging.error("Could not calculate obb_direction_ras_z!")
- else:
- obbDirectionZTuple = obbDirectionZArray.GetTuple(0)
-
- if obbOriginTuple is not None and obbDirectionZTuple is not None:
- obbDirectionZ = list(obbDirectionZTuple)
- transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionZ, obbDirectionZ)
- stats["obb_direction_ras_z"] = obbDirectionZ
-
- if "principal_moments" in requestedKeys:
- principalMomentsTuple = None
- principalMomentsArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_moments"])
- if principalMomentsArray is None:
- logging.error("Could not calculate principal_moments!")
- else:
- principalMomentsTuple = principalMomentsArray.GetTuple(0)
- if principalMomentsTuple is not None:
- principalMoments = list(principalMomentsTuple)
- stats["principal_moments"] = principalMoments
-
- if "principal_axis_x" in requestedKeys:
- centroidRASTuple = None
- centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
- if centroidRASArray is None:
- logging.error("Could not calculate principal_axis_x!")
- else:
- centroidRASTuple = centroidRASArray.GetTuple(0)
-
- principalAxisXTuple = None
- principalAxisXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_x"])
- if principalAxisXArray is None:
- logging.error("Could not calculate principal_axis_x!")
- else:
- principalAxisXTuple = principalAxisXArray.GetTuple(0)
-
- if centroidRASTuple is not None and principalAxisXTuple is not None:
- principalAxisX = list(principalAxisXTuple)
- transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisX, principalAxisX)
- stats["principal_axis_x"] = principalAxisX
-
- if "principal_axis_y" in requestedKeys:
- centroidRASTuple = None
- centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
- if centroidRASArray is None:
- logging.error("Could not calculate principal_axis_y!")
- else:
- centroidRASTuple = centroidRASArray.GetTuple(0)
-
- principalAxisYTuple = None
- principalAxisYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_y"])
- if principalAxisYArray is None:
- logging.error("Could not calculate principal_axis_y!")
- else:
- principalAxisYTuple = principalAxisYArray.GetTuple(0)
-
- if centroidRASTuple is not None and principalAxisYTuple is not None:
- principalAxisY = list(principalAxisYTuple)
- transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisY, principalAxisY)
- stats["principal_axis_y"] = principalAxisY
-
- if "principal_axis_z" in requestedKeys:
- centroidRASTuple = None
- centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
- if centroidRASArray is None:
- logging.error("Could not calculate principal_axis_z!")
- else:
- centroidRASTuple = centroidRASArray.GetTuple(0)
-
- principalAxisZTuple = None
- principalAxisZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_z"])
- if principalAxisZArray is None:
- logging.error("Could not calculate principal_axis_z!")
- else:
- principalAxisZTuple = principalAxisZArray.GetTuple(0)
-
- if centroidRASTuple is not None and principalAxisZTuple is not None:
- principalAxisZ = list(principalAxisZTuple)
- transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisZ, principalAxisZ)
- stats["principal_axis_z"] = principalAxisZ
-
- return stats
-
- def getMeasurementInfo(self, key):
- """Get information (name, description, units, ...) about the measurement for the given key"""
- info = {}
-
- # @fedorov could not find any suitable DICOM quantity code for "number of voxels".
- # DCM has "Number of needles" etc., so probably "Number of voxels"
- # should be added too. Need to discuss with @dclunie. For now, a
- # QIICR private scheme placeholder.
- info["voxel_count"] = \
- self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels",
- quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True),
- unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True))
-
- info["volume_mm3"] = \
- self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
-
- info["volume_cm3"] = \
- self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True),
- measurementMethodDicomCode=self.createCodedEntry("126030", "DCM",
- "Sum of segmented voxel volumes", True))
-
- info["centroid_ras"] = \
- self.createMeasurementInfo(name="Centroid", description="Location of the centroid in RAS", units="", componentNames=["r", "a", "s"])
-
- info["feret_diameter_mm"] = \
- self.createMeasurementInfo(name="Feret diameter mm", description="Feret diameter in mm", units="mm")
-
- info["surface_area_mm2"] = \
- self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2",
- quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True),
- unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True))
-
- info["roundness"] = \
- self.createMeasurementInfo(name="Roundness",
- description="Segment roundness. Calculated from ratio of the area of the hypersphere by the actual area. Value of 1 represents a spherical structure", units="")
-
- info["flatness"] = \
- self.createMeasurementInfo(name="Flatness",
- description="Segment flatness. Calculated from square root of the ratio of the second smallest principal moment by the smallest. Value of 0 represents a flat structure." +
- " ( https://hdl.handle.net/1926/584 )",
- units="")
-
- info["elongation"] = \
- self.createMeasurementInfo(name="Elongation",
- description="Segment elongation. Calculated from square root of the ratio of the second largest principal moment by the second smallest. ( https://hdl.handle.net/1926/584 )",
- units="")
-
- info["oriented_bounding_box"] = \
- self.createMeasurementInfo(name="Oriented bounding box", description="Oriented bounding box", units="")
-
- info["obb_origin_ras"] = \
- self.createMeasurementInfo(name="OBB origin", description="Oriented bounding box origin in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["obb_diameter_mm"] = \
- self.createMeasurementInfo(name="OBB diameter", description="Oriented bounding box diameter in mm", units="mm", componentNames=["x", "y", "z"])
-
- info["obb_direction_ras_x"] = \
- self.createMeasurementInfo(name="OBB X direction", description="Oriented bounding box X direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["obb_direction_ras_y"] = \
- self.createMeasurementInfo(name="OBB Y direction", description="Oriented bounding box Y direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["obb_direction_ras_z"] = \
- self.createMeasurementInfo(name="OBB Z direction", description="Oriented bounding box Z direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["principal_moments"] = \
- self.createMeasurementInfo(name="Principal moments", description="Principal moments of inertia for x, y and z axes",
- units="", componentNames=["x", "y", "z"])
-
- info["principal_axis_x"] = \
- self.createMeasurementInfo(name="Principal X axis", description="Principal X axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["principal_axis_y"] = \
- self.createMeasurementInfo(name="Principal Y axis", description="Principal Y axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- info["principal_axis_z"] = \
- self.createMeasurementInfo(name="Principal Z axis", description="Principal Z axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
-
- return info[key] if key in info else None
+ """Statistical plugin for Labelmaps"""
+
+ def __init__(self):
+ super().__init__()
+ self.name = "Labelmap"
+ self.obbKeys = ["obb_origin_ras", "obb_diameter_mm", "obb_direction_ras_x", "obb_direction_ras_y", "obb_direction_ras_z"]
+ self.principalAxisKeys = ["principal_axis_x", "principal_axis_y", "principal_axis_z"]
+ self.shapeKeys = [
+ "centroid_ras", "feret_diameter_mm", "surface_area_mm2", "roundness", "flatness", "elongation",
+ "principal_moments",
+ ] + self.principalAxisKeys + self.obbKeys
+
+ self.defaultKeys = ["voxel_count", "volume_mm3", "volume_cm3"] # Don't calculate label shape statistics by default since they take longer to compute
+ self.keys = self.defaultKeys + self.shapeKeys
+ self.keyToShapeStatisticNames = {
+ "centroid_ras": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Centroid),
+ "feret_diameter_mm": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.FeretDiameter),
+ "surface_area_mm2": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Perimeter),
+ "roundness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Roundness),
+ "flatness": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Flatness),
+ "elongation": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.Elongation),
+ "oriented_bounding_box": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.OrientedBoundingBox),
+ "obb_origin_ras": "OrientedBoundingBoxOrigin",
+ "obb_diameter_mm": "OrientedBoundingBoxSize",
+ "obb_direction_ras_x": "OrientedBoundingBoxDirectionX",
+ "obb_direction_ras_y": "OrientedBoundingBoxDirectionY",
+ "obb_direction_ras_z": "OrientedBoundingBoxDirectionZ",
+ "principal_moments": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalMoments),
+ "principal_axes": vtkITK.vtkITKLabelShapeStatistics.GetShapeStatisticAsString(vtkITK.vtkITKLabelShapeStatistics.PrincipalAxes),
+ "principal_axis_x": "PrincipalAxisX",
+ "principal_axis_y": "PrincipalAxisY",
+ "principal_axis_z": "PrincipalAxisZ",
+ }
+ # ... developer may add extra options to configure other parameters
+
+ def computeStatistics(self, segmentID):
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ requestedKeys = self.getRequestedKeys()
+
+ segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
+
+ if len(requestedKeys) == 0:
+ return {}
+
+ containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
+ vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
+ if not containsLabelmapRepresentation:
+ return {}
+
+ segmentLabelmap = slicer.vtkOrientedImageData()
+ segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap)
+ if (not segmentLabelmap
+ or not segmentLabelmap.GetPointData()
+ or not segmentLabelmap.GetPointData().GetScalars()):
+ # No input label data
+ return {}
+
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(segmentLabelmap)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+ thresh.Update()
+
+ # Use binary labelmap as a stencil
+ stencil = vtk.vtkImageToImageStencil()
+ stencil.SetInputData(thresh.GetOutput())
+ stencil.ThresholdByUpper(labelValue)
+ stencil.Update()
+
+ stat = vtk.vtkImageAccumulate()
+ stat.SetInputData(thresh.GetOutput())
+ stat.SetStencilData(stencil.GetOutput())
+ stat.Update()
+
+ # Add data to statistics list
+ cubicMMPerVoxel = reduce(lambda x, y: x * y, segmentLabelmap.GetSpacing())
+ ccPerCubicMM = 0.001
+ stats = {}
+ if "voxel_count" in requestedKeys:
+ stats["voxel_count"] = stat.GetVoxelCount()
+ if "volume_mm3" in requestedKeys:
+ stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel
+ if "volume_cm3" in requestedKeys:
+ stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM
+
+ calculateShapeStats = False
+ for shapeKey in self.shapeKeys:
+ if shapeKey in requestedKeys:
+ calculateShapeStats = True
+ break
+
+ if calculateShapeStats:
+ directions = vtk.vtkMatrix4x4()
+ segmentLabelmap.GetDirectionMatrix(directions)
+
+ # Remove oriented bounding box from requested keys and replace with individual keys
+ requestedOptions = requestedKeys
+ statFilterOptions = self.shapeKeys
+ calculateOBB = (
+ "obb_diameter_mm" in requestedKeys or
+ "obb_origin_ras" in requestedKeys or
+ "obb_direction_ras_x" in requestedKeys or
+ "obb_direction_ras_y" in requestedKeys or
+ "obb_direction_ras_z" in requestedKeys
+ )
+
+ if calculateOBB:
+ temp = statFilterOptions
+ statFilterOptions = []
+ for option in temp:
+ if not option in self.obbKeys:
+ statFilterOptions.append(option)
+ statFilterOptions.append("oriented_bounding_box")
+
+ temp = requestedOptions
+ requestedOptions = []
+ for option in temp:
+ if not option in self.obbKeys:
+ requestedOptions.append(option)
+ requestedOptions.append("oriented_bounding_box")
+
+ calculatePrincipalAxis = (
+ "principal_axis_x" in requestedKeys or
+ "principal_axis_y" in requestedKeys or
+ "principal_axis_z" in requestedKeys
+ )
+ if calculatePrincipalAxis:
+ temp = statFilterOptions
+ statFilterOptions = []
+ for option in temp:
+ if not option in self.principalAxisKeys:
+ statFilterOptions.append(option)
+ statFilterOptions.append("principal_axes")
+
+ temp = requestedOptions
+ requestedOptions = []
+ for option in temp:
+ if not option in self.principalAxisKeys:
+ requestedOptions.append(option)
+ requestedOptions.append("principal_axes")
+ requestedOptions.append("centroid_ras")
+
+ shapeStat = vtkITK.vtkITKLabelShapeStatistics()
+ shapeStat.SetInputData(thresh.GetOutput())
+ shapeStat.SetDirections(directions)
+ for shapeKey in statFilterOptions:
+ shapeStat.SetComputeShapeStatistic(self.keyToShapeStatisticNames[shapeKey], shapeKey in requestedOptions)
+ shapeStat.Update()
+
+ # If segmentation node is transformed, apply that transform to get RAS coordinates
+ transformSegmentToRas = vtk.vtkGeneralTransform()
+ slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(), None, transformSegmentToRas)
+
+ statTable = shapeStat.GetOutput()
+ if "centroid_ras" in requestedKeys:
+ centroidRAS = [0, 0, 0]
+ centroidTuple = None
+ centroidArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
+ if centroidArray is None:
+ logging.error("Could not calculate centroid_ras!")
+ else:
+ centroidTuple = centroidArray.GetTuple(0)
+ if centroidTuple is not None:
+ transformSegmentToRas.TransformPoint(centroidTuple, centroidRAS)
+ stats["centroid_ras"] = centroidRAS
+
+ if "roundness" in requestedKeys:
+ roundnessTuple = None
+ roundnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["roundness"])
+ if roundnessArray is None:
+ logging.error("Could not calculate roundness!")
+ else:
+ roundnessTuple = roundnessArray.GetTuple(0)
+ if roundnessTuple is not None:
+ roundness = roundnessTuple[0]
+ stats["roundness"] = roundness
+
+ if "flatness" in requestedKeys:
+ flatnessTuple = None
+ flatnessArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["flatness"])
+ if flatnessArray is None:
+ logging.error("Could not calculate flatness!")
+ else:
+ flatnessTuple = flatnessArray.GetTuple(0)
+ if flatnessTuple is not None:
+ flatness = flatnessTuple[0]
+ stats["flatness"] = flatness
+
+ if "elongation" in requestedKeys:
+ elongationTuple = None
+ elongationArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["elongation"])
+ if elongationArray is None:
+ logging.error("Could not calculate elongation!")
+ else:
+ elongationTuple = elongationArray.GetTuple(0)
+ if elongationTuple is not None:
+ elongation = elongationTuple[0]
+ stats["elongation"] = elongation
+
+ if "feret_diameter_mm" in requestedKeys:
+ feretDiameterTuple = None
+ feretDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["feret_diameter_mm"])
+ if feretDiameterArray is None:
+ logging.error("Could not calculate feret_diameter_mm!")
+ else:
+ feretDiameterTuple = feretDiameterArray.GetTuple(0)
+ if feretDiameterTuple is not None:
+ feretDiameter = feretDiameterTuple[0]
+ stats["feret_diameter_mm"] = feretDiameter
+
+ if "surface_area_mm2" in requestedKeys:
+ perimeterTuple = None
+ perimeterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["surface_area_mm2"])
+ if perimeterArray is None:
+ logging.error("Could not calculate surface_area_mm2!")
+ else:
+ perimeterTuple = perimeterArray.GetTuple(0)
+ if perimeterTuple is not None:
+ perimeter = perimeterTuple[0]
+ stats["surface_area_mm2"] = perimeter
+
+ if "obb_origin_ras" in requestedKeys:
+ obbOriginTuple = None
+ obbOriginRAS = [0, 0, 0]
+ obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
+ if obbOriginArray is None:
+ logging.error("Could not calculate obb_origin_ras!")
+ else:
+ obbOriginTuple = obbOriginArray.GetTuple(0)
+ if obbOriginTuple is not None:
+ transformSegmentToRas.TransformPoint(obbOriginTuple, obbOriginRAS)
+ stats["obb_origin_ras"] = obbOriginRAS
+
+ if "obb_diameter_mm" in requestedKeys:
+ obbDiameterMMTuple = None
+ obbDiameterArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_diameter_mm"])
+ if obbDiameterArray is None:
+ logging.error("Could not calculate obb_diameter_mm!")
+ else:
+ obbDiameterMMTuple = obbDiameterArray.GetTuple(0)
+ if obbDiameterMMTuple is not None:
+ obbDiameterMM = list(obbDiameterMMTuple)
+ stats["obb_diameter_mm"] = obbDiameterMM
+
+ if "obb_direction_ras_x" in requestedKeys:
+ obbOriginTuple = None
+ obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
+ if obbOriginArray is None:
+ logging.error("Could not calculate obb_direction_ras_x!")
+ else:
+ obbOriginTuple = obbOriginArray.GetTuple(0)
+
+ obbDirectionXTuple = None
+ obbDirectionXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_x"])
+ if obbDirectionXArray is None:
+ logging.error("Could not calculate obb_direction_ras_x!")
+ else:
+ obbDirectionXTuple = obbDirectionXArray.GetTuple(0)
+
+ if obbOriginTuple is not None and obbDirectionXTuple is not None:
+ obbDirectionX = list(obbDirectionXTuple)
+ transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionX, obbDirectionX)
+ stats["obb_direction_ras_x"] = obbDirectionX
+
+ if "obb_direction_ras_y" in requestedKeys:
+ obbOriginTuple = None
+ obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
+ if obbOriginArray is None:
+ logging.error("Could not calculate obb_direction_ras_y!")
+ else:
+ obbOriginTuple = obbOriginArray.GetTuple(0)
+
+ obbDirectionYTuple = None
+ obbDirectionYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_y"])
+ if obbDirectionYArray is None:
+ logging.error("Could not calculate obb_direction_ras_y!")
+ else:
+ obbDirectionYTuple = obbDirectionYArray.GetTuple(0)
+
+ if obbOriginTuple is not None and obbDirectionYTuple is not None:
+ obbDirectionY = list(obbDirectionYTuple)
+ transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionY, obbDirectionY)
+ stats["obb_direction_ras_y"] = obbDirectionY
+
+ if "obb_direction_ras_z" in requestedKeys:
+ obbOriginTuple = None
+ obbOriginArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_origin_ras"])
+ if obbOriginArray is None:
+ logging.error("Could not calculate obb_direction_ras_z!")
+ else:
+ obbOriginTuple = obbOriginArray.GetTuple(0)
+
+ obbDirectionZTuple = None
+ obbDirectionZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["obb_direction_ras_z"])
+ if obbDirectionZArray is None:
+ logging.error("Could not calculate obb_direction_ras_z!")
+ else:
+ obbDirectionZTuple = obbDirectionZArray.GetTuple(0)
+
+ if obbOriginTuple is not None and obbDirectionZTuple is not None:
+ obbDirectionZ = list(obbDirectionZTuple)
+ transformSegmentToRas.TransformVectorAtPoint(obbOriginTuple, obbDirectionZ, obbDirectionZ)
+ stats["obb_direction_ras_z"] = obbDirectionZ
+
+ if "principal_moments" in requestedKeys:
+ principalMomentsTuple = None
+ principalMomentsArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_moments"])
+ if principalMomentsArray is None:
+ logging.error("Could not calculate principal_moments!")
+ else:
+ principalMomentsTuple = principalMomentsArray.GetTuple(0)
+ if principalMomentsTuple is not None:
+ principalMoments = list(principalMomentsTuple)
+ stats["principal_moments"] = principalMoments
+
+ if "principal_axis_x" in requestedKeys:
+ centroidRASTuple = None
+ centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
+ if centroidRASArray is None:
+ logging.error("Could not calculate principal_axis_x!")
+ else:
+ centroidRASTuple = centroidRASArray.GetTuple(0)
+
+ principalAxisXTuple = None
+ principalAxisXArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_x"])
+ if principalAxisXArray is None:
+ logging.error("Could not calculate principal_axis_x!")
+ else:
+ principalAxisXTuple = principalAxisXArray.GetTuple(0)
+
+ if centroidRASTuple is not None and principalAxisXTuple is not None:
+ principalAxisX = list(principalAxisXTuple)
+ transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisX, principalAxisX)
+ stats["principal_axis_x"] = principalAxisX
+
+ if "principal_axis_y" in requestedKeys:
+ centroidRASTuple = None
+ centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
+ if centroidRASArray is None:
+ logging.error("Could not calculate principal_axis_y!")
+ else:
+ centroidRASTuple = centroidRASArray.GetTuple(0)
+
+ principalAxisYTuple = None
+ principalAxisYArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_y"])
+ if principalAxisYArray is None:
+ logging.error("Could not calculate principal_axis_y!")
+ else:
+ principalAxisYTuple = principalAxisYArray.GetTuple(0)
+
+ if centroidRASTuple is not None and principalAxisYTuple is not None:
+ principalAxisY = list(principalAxisYTuple)
+ transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisY, principalAxisY)
+ stats["principal_axis_y"] = principalAxisY
+
+ if "principal_axis_z" in requestedKeys:
+ centroidRASTuple = None
+ centroidRASArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["centroid_ras"])
+ if centroidRASArray is None:
+ logging.error("Could not calculate principal_axis_z!")
+ else:
+ centroidRASTuple = centroidRASArray.GetTuple(0)
+
+ principalAxisZTuple = None
+ principalAxisZArray = statTable.GetColumnByName(self.keyToShapeStatisticNames["principal_axis_z"])
+ if principalAxisZArray is None:
+ logging.error("Could not calculate principal_axis_z!")
+ else:
+ principalAxisZTuple = principalAxisZArray.GetTuple(0)
+
+ if centroidRASTuple is not None and principalAxisZTuple is not None:
+ principalAxisZ = list(principalAxisZTuple)
+ transformSegmentToRas.TransformVectorAtPoint(centroidRASTuple, principalAxisZ, principalAxisZ)
+ stats["principal_axis_z"] = principalAxisZ
+
+ return stats
+
+ def getMeasurementInfo(self, key):
+ """Get information (name, description, units, ...) about the measurement for the given key"""
+ info = {}
+
+ # @fedorov could not find any suitable DICOM quantity code for "number of voxels".
+ # DCM has "Number of needles" etc., so probably "Number of voxels"
+ # should be added too. Need to discuss with @dclunie. For now, a
+ # QIICR private scheme placeholder.
+ info["voxel_count"] = \
+ self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels",
+ quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True),
+ unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True))
+
+ info["volume_mm3"] = \
+ self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
+
+ info["volume_cm3"] = \
+ self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True),
+ measurementMethodDicomCode=self.createCodedEntry("126030", "DCM",
+ "Sum of segmented voxel volumes", True))
+
+ info["centroid_ras"] = \
+ self.createMeasurementInfo(name="Centroid", description="Location of the centroid in RAS", units="", componentNames=["r", "a", "s"])
+
+ info["feret_diameter_mm"] = \
+ self.createMeasurementInfo(name="Feret diameter mm", description="Feret diameter in mm", units="mm")
+
+ info["surface_area_mm2"] = \
+ self.createMeasurementInfo(name="Surface mm2", description="Surface area in mm2", units="mm2",
+ quantityDicomCode=self.createCodedEntry("000247", "99CHEMINF", "Surface area", True),
+ unitsDicomCode=self.createCodedEntry("mm2", "UCUM", "squared millimeters", True))
+
+ info["roundness"] = \
+ self.createMeasurementInfo(name="Roundness",
+ description="Segment roundness. Calculated from ratio of the area of the hypersphere by the actual area. Value of 1 represents a spherical structure", units="")
+
+ info["flatness"] = \
+ self.createMeasurementInfo(name="Flatness",
+ description="Segment flatness. Calculated from square root of the ratio of the second smallest principal moment by the smallest. Value of 0 represents a flat structure." +
+ " ( https://hdl.handle.net/1926/584 )",
+ units="")
+
+ info["elongation"] = \
+ self.createMeasurementInfo(name="Elongation",
+ description="Segment elongation. Calculated from square root of the ratio of the second largest principal moment by the second smallest. ( https://hdl.handle.net/1926/584 )",
+ units="")
+
+ info["oriented_bounding_box"] = \
+ self.createMeasurementInfo(name="Oriented bounding box", description="Oriented bounding box", units="")
+
+ info["obb_origin_ras"] = \
+ self.createMeasurementInfo(name="OBB origin", description="Oriented bounding box origin in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["obb_diameter_mm"] = \
+ self.createMeasurementInfo(name="OBB diameter", description="Oriented bounding box diameter in mm", units="mm", componentNames=["x", "y", "z"])
+
+ info["obb_direction_ras_x"] = \
+ self.createMeasurementInfo(name="OBB X direction", description="Oriented bounding box X direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["obb_direction_ras_y"] = \
+ self.createMeasurementInfo(name="OBB Y direction", description="Oriented bounding box Y direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["obb_direction_ras_z"] = \
+ self.createMeasurementInfo(name="OBB Z direction", description="Oriented bounding box Z direction in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["principal_moments"] = \
+ self.createMeasurementInfo(name="Principal moments", description="Principal moments of inertia for x, y and z axes",
+ units="", componentNames=["x", "y", "z"])
+
+ info["principal_axis_x"] = \
+ self.createMeasurementInfo(name="Principal X axis", description="Principal X axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["principal_axis_y"] = \
+ self.createMeasurementInfo(name="Principal Y axis", description="Principal Y axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ info["principal_axis_z"] = \
+ self.createMeasurementInfo(name="Principal Z axis", description="Principal Z axis of rotation in RAS coordinates", units="", componentNames=["r", "a", "s"])
+
+ return info[key] if key in info else None
diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py
index 0fbb193ea7f..e5b44d71751 100644
--- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py
+++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/ScalarVolumeSegmentStatisticsPlugin.py
@@ -4,194 +4,194 @@
class ScalarVolumeSegmentStatisticsPlugin(SegmentStatisticsPluginBase):
- """Statistical plugin for segmentations with scalar volumes"""
-
- def __init__(self):
- super().__init__()
- self.name = "Scalar Volume"
- self.keys = ["voxel_count", "volume_mm3", "volume_cm3", "min", "max", "mean", "median", "stdev"]
- self.defaultKeys = self.keys # calculate all measurements by default
- # ... developer may add extra options to configure other parameters
-
- def computeStatistics(self, segmentID):
- requestedKeys = self.getRequestedKeys()
-
- segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
- grayscaleNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume"))
-
- if len(requestedKeys) == 0:
- return {}
-
- stencil = self.getStencilForVolume(segmentationNode, segmentID, grayscaleNode)
- if not stencil:
- return {}
-
- cubicMMPerVoxel = reduce(lambda x, y: x * y, grayscaleNode.GetSpacing())
- ccPerCubicMM = 0.001
-
- stat = vtk.vtkImageAccumulate()
- stat.SetInputData(grayscaleNode.GetImageData())
- stat.SetStencilData(stencil.GetOutput())
- stat.Update()
-
- medians = vtk.vtkImageHistogramStatistics()
- medians.SetInputData(grayscaleNode.GetImageData())
- medians.SetStencilData(stencil.GetOutput())
- medians.Update()
-
- # create statistics list
- stats = {}
- if "voxel_count" in requestedKeys:
- stats["voxel_count"] = stat.GetVoxelCount()
- if "volume_mm3" in requestedKeys:
- stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel
- if "volume_cm3" in requestedKeys:
- stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM
- if stat.GetVoxelCount() > 0:
- if "min" in requestedKeys:
- stats["min"] = stat.GetMin()[0]
- if "max" in requestedKeys:
- stats["max"] = stat.GetMax()[0]
- if "mean" in requestedKeys:
- stats["mean"] = stat.GetMean()[0]
- if "stdev" in requestedKeys:
- stats["stdev"] = stat.GetStandardDeviation()[0]
- if "median" in requestedKeys:
- stats["median"] = medians.GetMedian()
- return stats
-
- def getStencilForVolume(self, segmentationNode, segmentID, grayscaleNode):
- import vtkSegmentationCorePython as vtkSegmentationCore
-
- containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
- vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
- if not containsLabelmapRepresentation:
- return None
-
- if (not grayscaleNode
- or not grayscaleNode.GetImageData()
- or not grayscaleNode.GetImageData().GetPointData()
- or not grayscaleNode.GetImageData().GetPointData().GetScalars()):
- # Input grayscale node does not contain valid image data
- return None
-
- # Get geometry of grayscale volume node as oriented image data
- # reference geometry in reference node coordinate system
- referenceGeometry_Reference = vtkSegmentationCore.vtkOrientedImageData()
- referenceGeometry_Reference.SetExtent(grayscaleNode.GetImageData().GetExtent())
- ijkToRasMatrix = vtk.vtkMatrix4x4()
- grayscaleNode.GetIJKToRASMatrix(ijkToRasMatrix)
- referenceGeometry_Reference.SetGeometryFromImageToWorldMatrix(ijkToRasMatrix)
-
- # Get transform between grayscale volume and segmentation
- segmentationToReferenceGeometryTransform = vtk.vtkGeneralTransform()
- slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(),
- grayscaleNode.GetParentTransformNode(), segmentationToReferenceGeometryTransform)
-
- segmentLabelmap = vtkSegmentationCore.vtkOrientedImageData()
- segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap)
- if (not segmentLabelmap
- or not segmentLabelmap.GetPointData()
- or not segmentLabelmap.GetPointData().GetScalars()):
- # No input label data
- return None
-
- segmentLabelmap_Reference = vtkSegmentationCore.vtkOrientedImageData()
- vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
- segmentLabelmap, referenceGeometry_Reference, segmentLabelmap_Reference,
- False, # nearest neighbor interpolation
- False, # no padding
- segmentationToReferenceGeometryTransform)
-
- # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
- labelValue = 1
- backgroundValue = 0
- thresh = vtk.vtkImageThreshold()
- thresh.SetInputData(segmentLabelmap_Reference)
- thresh.ThresholdByLower(0)
- thresh.SetInValue(backgroundValue)
- thresh.SetOutValue(labelValue)
- thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
- thresh.Update()
-
- # Use binary labelmap as a stencil
- stencil = vtk.vtkImageToImageStencil()
- stencil.SetInputData(thresh.GetOutput())
- stencil.ThresholdByUpper(labelValue)
- stencil.Update()
-
- return stencil
-
- def getMeasurementInfo(self, key):
- """Get information (name, description, units, ...) about the measurement for the given key"""
-
- scalarVolumeNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume"))
-
- scalarVolumeQuantity = scalarVolumeNode.GetVoxelValueQuantity() if scalarVolumeNode else self.createCodedEntry("", "", "")
- scalarVolumeUnits = scalarVolumeNode.GetVoxelValueUnits() if scalarVolumeNode else self.createCodedEntry("", "", "")
- if not scalarVolumeQuantity:
- scalarVolumeQuantity = self.createCodedEntry("", "", "")
- if not scalarVolumeUnits:
- scalarVolumeUnits = self.createCodedEntry("", "", "")
-
- info = dict()
-
- # @fedorov could not find any suitable DICOM quantity code for "number of voxels".
- # DCM has "Number of needles" etc., so probably "Number of voxels"
- # should be added too. Need to discuss with @dclunie. For now, a
- # QIICR private scheme placeholder.
- # @moselhy also could not find DICOM quantity code for "median"
-
- info["voxel_count"] = \
- self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels",
- quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True),
- unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True))
-
- info["volume_mm3"] = \
- self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
-
- info["volume_cm3"] = \
- self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
- quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
- unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True),
- measurementMethodDicomCode=self.createCodedEntry("126030", "DCM",
- "Sum of segmented voxel volumes", True))
-
- info["min"] = \
- self.createMeasurementInfo(name="Minimum", description="Minimum scalar value",
- units=scalarVolumeUnits.GetCodeMeaning(),
- quantityDicomCode=scalarVolumeQuantity.GetAsString(),
- unitsDicomCode=scalarVolumeUnits.GetAsString(),
- derivationDicomCode=self.createCodedEntry("255605001", "SCT", "Minimum", True))
-
- info["max"] = \
- self.createMeasurementInfo(name="Maximum", description="Maximum scalar value",
- units=scalarVolumeUnits.GetCodeMeaning(),
- quantityDicomCode=scalarVolumeQuantity.GetAsString(),
- unitsDicomCode=scalarVolumeUnits.GetAsString(),
- derivationDicomCode=self.createCodedEntry("56851009", "SCT", "Maximum", True))
-
- info["mean"] = \
- self.createMeasurementInfo(name="Mean", description="Mean scalar value",
- units=scalarVolumeUnits.GetCodeMeaning(),
- quantityDicomCode=scalarVolumeQuantity.GetAsString(),
- unitsDicomCode=scalarVolumeUnits.GetAsString(),
- derivationDicomCode=self.createCodedEntry("373098007", "SCT", "Mean", True))
-
- info["median"] = \
- self.createMeasurementInfo(name="Median", description="Median scalar value",
- units=scalarVolumeUnits.GetCodeMeaning(),
- quantityDicomCode=scalarVolumeQuantity.GetAsString(),
- unitsDicomCode=scalarVolumeUnits.GetAsString(),
- derivationDicomCode=self.createCodedEntry("median", "SCT", "Median", True))
-
- info["stdev"] = \
- self.createMeasurementInfo(name="Standard deviation", description="Standard deviation of scalar values",
- units=scalarVolumeUnits.GetCodeMeaning(),
- quantityDicomCode=scalarVolumeQuantity.GetAsString(),
- unitsDicomCode=scalarVolumeUnits.GetAsString(),
- derivationDicomCode=self.createCodedEntry('386136009', 'SCT', 'Standard Deviation', True))
-
- return info[key] if key in info else None
+ """Statistical plugin for segmentations with scalar volumes"""
+
+ def __init__(self):
+ super().__init__()
+ self.name = "Scalar Volume"
+ self.keys = ["voxel_count", "volume_mm3", "volume_cm3", "min", "max", "mean", "median", "stdev"]
+ self.defaultKeys = self.keys # calculate all measurements by default
+ # ... developer may add extra options to configure other parameters
+
+ def computeStatistics(self, segmentID):
+ requestedKeys = self.getRequestedKeys()
+
+ segmentationNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("Segmentation"))
+ grayscaleNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume"))
+
+ if len(requestedKeys) == 0:
+ return {}
+
+ stencil = self.getStencilForVolume(segmentationNode, segmentID, grayscaleNode)
+ if not stencil:
+ return {}
+
+ cubicMMPerVoxel = reduce(lambda x, y: x * y, grayscaleNode.GetSpacing())
+ ccPerCubicMM = 0.001
+
+ stat = vtk.vtkImageAccumulate()
+ stat.SetInputData(grayscaleNode.GetImageData())
+ stat.SetStencilData(stencil.GetOutput())
+ stat.Update()
+
+ medians = vtk.vtkImageHistogramStatistics()
+ medians.SetInputData(grayscaleNode.GetImageData())
+ medians.SetStencilData(stencil.GetOutput())
+ medians.Update()
+
+ # create statistics list
+ stats = {}
+ if "voxel_count" in requestedKeys:
+ stats["voxel_count"] = stat.GetVoxelCount()
+ if "volume_mm3" in requestedKeys:
+ stats["volume_mm3"] = stat.GetVoxelCount() * cubicMMPerVoxel
+ if "volume_cm3" in requestedKeys:
+ stats["volume_cm3"] = stat.GetVoxelCount() * cubicMMPerVoxel * ccPerCubicMM
+ if stat.GetVoxelCount() > 0:
+ if "min" in requestedKeys:
+ stats["min"] = stat.GetMin()[0]
+ if "max" in requestedKeys:
+ stats["max"] = stat.GetMax()[0]
+ if "mean" in requestedKeys:
+ stats["mean"] = stat.GetMean()[0]
+ if "stdev" in requestedKeys:
+ stats["stdev"] = stat.GetStandardDeviation()[0]
+ if "median" in requestedKeys:
+ stats["median"] = medians.GetMedian()
+ return stats
+
+ def getStencilForVolume(self, segmentationNode, segmentID, grayscaleNode):
+ import vtkSegmentationCorePython as vtkSegmentationCore
+
+ containsLabelmapRepresentation = segmentationNode.GetSegmentation().ContainsRepresentation(
+ vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationBinaryLabelmapRepresentationName())
+ if not containsLabelmapRepresentation:
+ return None
+
+ if (not grayscaleNode
+ or not grayscaleNode.GetImageData()
+ or not grayscaleNode.GetImageData().GetPointData()
+ or not grayscaleNode.GetImageData().GetPointData().GetScalars()):
+ # Input grayscale node does not contain valid image data
+ return None
+
+ # Get geometry of grayscale volume node as oriented image data
+ # reference geometry in reference node coordinate system
+ referenceGeometry_Reference = vtkSegmentationCore.vtkOrientedImageData()
+ referenceGeometry_Reference.SetExtent(grayscaleNode.GetImageData().GetExtent())
+ ijkToRasMatrix = vtk.vtkMatrix4x4()
+ grayscaleNode.GetIJKToRASMatrix(ijkToRasMatrix)
+ referenceGeometry_Reference.SetGeometryFromImageToWorldMatrix(ijkToRasMatrix)
+
+ # Get transform between grayscale volume and segmentation
+ segmentationToReferenceGeometryTransform = vtk.vtkGeneralTransform()
+ slicer.vtkMRMLTransformNode.GetTransformBetweenNodes(segmentationNode.GetParentTransformNode(),
+ grayscaleNode.GetParentTransformNode(), segmentationToReferenceGeometryTransform)
+
+ segmentLabelmap = vtkSegmentationCore.vtkOrientedImageData()
+ segmentationNode.GetBinaryLabelmapRepresentation(segmentID, segmentLabelmap)
+ if (not segmentLabelmap
+ or not segmentLabelmap.GetPointData()
+ or not segmentLabelmap.GetPointData().GetScalars()):
+ # No input label data
+ return None
+
+ segmentLabelmap_Reference = vtkSegmentationCore.vtkOrientedImageData()
+ vtkSegmentationCore.vtkOrientedImageDataResample.ResampleOrientedImageToReferenceOrientedImage(
+ segmentLabelmap, referenceGeometry_Reference, segmentLabelmap_Reference,
+ False, # nearest neighbor interpolation
+ False, # no padding
+ segmentationToReferenceGeometryTransform)
+
+ # We need to know exactly the value of the segment voxels, apply threshold to make force the selected label value
+ labelValue = 1
+ backgroundValue = 0
+ thresh = vtk.vtkImageThreshold()
+ thresh.SetInputData(segmentLabelmap_Reference)
+ thresh.ThresholdByLower(0)
+ thresh.SetInValue(backgroundValue)
+ thresh.SetOutValue(labelValue)
+ thresh.SetOutputScalarType(vtk.VTK_UNSIGNED_CHAR)
+ thresh.Update()
+
+ # Use binary labelmap as a stencil
+ stencil = vtk.vtkImageToImageStencil()
+ stencil.SetInputData(thresh.GetOutput())
+ stencil.ThresholdByUpper(labelValue)
+ stencil.Update()
+
+ return stencil
+
+ def getMeasurementInfo(self, key):
+ """Get information (name, description, units, ...) about the measurement for the given key"""
+
+ scalarVolumeNode = slicer.mrmlScene.GetNodeByID(self.getParameterNode().GetParameter("ScalarVolume"))
+
+ scalarVolumeQuantity = scalarVolumeNode.GetVoxelValueQuantity() if scalarVolumeNode else self.createCodedEntry("", "", "")
+ scalarVolumeUnits = scalarVolumeNode.GetVoxelValueUnits() if scalarVolumeNode else self.createCodedEntry("", "", "")
+ if not scalarVolumeQuantity:
+ scalarVolumeQuantity = self.createCodedEntry("", "", "")
+ if not scalarVolumeUnits:
+ scalarVolumeUnits = self.createCodedEntry("", "", "")
+
+ info = dict()
+
+ # @fedorov could not find any suitable DICOM quantity code for "number of voxels".
+ # DCM has "Number of needles" etc., so probably "Number of voxels"
+ # should be added too. Need to discuss with @dclunie. For now, a
+ # QIICR private scheme placeholder.
+ # @moselhy also could not find DICOM quantity code for "median"
+
+ info["voxel_count"] = \
+ self.createMeasurementInfo(name="Voxel count", description="Number of voxels", units="voxels",
+ quantityDicomCode=self.createCodedEntry("nvoxels", "99QIICR", "Number of voxels", True),
+ unitsDicomCode=self.createCodedEntry("voxels", "UCUM", "voxels", True))
+
+ info["volume_mm3"] = \
+ self.createMeasurementInfo(name="Volume mm3", description="Volume in mm3", units="mm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("mm3", "UCUM", "cubic millimeter", True))
+
+ info["volume_cm3"] = \
+ self.createMeasurementInfo(name="Volume cm3", description="Volume in cm3", units="cm3",
+ quantityDicomCode=self.createCodedEntry("118565006", "SCT", "Volume", True),
+ unitsDicomCode=self.createCodedEntry("cm3", "UCUM", "cubic centimeter", True),
+ measurementMethodDicomCode=self.createCodedEntry("126030", "DCM",
+ "Sum of segmented voxel volumes", True))
+
+ info["min"] = \
+ self.createMeasurementInfo(name="Minimum", description="Minimum scalar value",
+ units=scalarVolumeUnits.GetCodeMeaning(),
+ quantityDicomCode=scalarVolumeQuantity.GetAsString(),
+ unitsDicomCode=scalarVolumeUnits.GetAsString(),
+ derivationDicomCode=self.createCodedEntry("255605001", "SCT", "Minimum", True))
+
+ info["max"] = \
+ self.createMeasurementInfo(name="Maximum", description="Maximum scalar value",
+ units=scalarVolumeUnits.GetCodeMeaning(),
+ quantityDicomCode=scalarVolumeQuantity.GetAsString(),
+ unitsDicomCode=scalarVolumeUnits.GetAsString(),
+ derivationDicomCode=self.createCodedEntry("56851009", "SCT", "Maximum", True))
+
+ info["mean"] = \
+ self.createMeasurementInfo(name="Mean", description="Mean scalar value",
+ units=scalarVolumeUnits.GetCodeMeaning(),
+ quantityDicomCode=scalarVolumeQuantity.GetAsString(),
+ unitsDicomCode=scalarVolumeUnits.GetAsString(),
+ derivationDicomCode=self.createCodedEntry("373098007", "SCT", "Mean", True))
+
+ info["median"] = \
+ self.createMeasurementInfo(name="Median", description="Median scalar value",
+ units=scalarVolumeUnits.GetCodeMeaning(),
+ quantityDicomCode=scalarVolumeQuantity.GetAsString(),
+ unitsDicomCode=scalarVolumeUnits.GetAsString(),
+ derivationDicomCode=self.createCodedEntry("median", "SCT", "Median", True))
+
+ info["stdev"] = \
+ self.createMeasurementInfo(name="Standard deviation", description="Standard deviation of scalar values",
+ units=scalarVolumeUnits.GetCodeMeaning(),
+ quantityDicomCode=scalarVolumeQuantity.GetAsString(),
+ unitsDicomCode=scalarVolumeUnits.GetAsString(),
+ derivationDicomCode=self.createCodedEntry('386136009', 'SCT', 'Standard Deviation', True))
+
+ return info[key] if key in info else None
diff --git a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py
index 0f3a2b4c777..2b7528c4462 100644
--- a/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py
+++ b/Modules/Scripted/SegmentStatistics/SegmentStatisticsPlugins/SegmentStatisticsPluginBase.py
@@ -4,212 +4,212 @@
class SegmentStatisticsPluginBase:
- """Base class for statistics plugins operating on segments.
- Derived classes should specify: self.name, self.keys, self.defaultKeys
- and implement: computeStatistics, getMeasurementInfo
- """
-
- @staticmethod
- def createCodedEntry(codeValue, codingScheme, codeMeaning, returnAsString=False):
- """Create a coded entry and return as string or vtkCodedEntry"""
- entry = slicer.vtkCodedEntry()
- entry.SetValueSchemeMeaning(codeValue, codingScheme, codeMeaning)
- return entry if not returnAsString else entry.GetAsString()
-
- @staticmethod
- def createMeasurementInfo(name, description, units, quantityDicomCode=None, unitsDicomCode=None,
- measurementMethodDicomCode=None, derivationDicomCode=None, componentNames=None):
- """Utility method to create measurement information"""
- info = {
- "name": name,
- "description": description,
- "units": units
- }
- if componentNames:
- info["componentNames"] = componentNames
- if quantityDicomCode:
- info["DICOM.QuantityCode"] = quantityDicomCode
- if unitsDicomCode:
- info["DICOM.UnitsCode"] = unitsDicomCode
- if measurementMethodDicomCode:
- info["DICOM.MeasurementMethodCode"] = measurementMethodDicomCode
- if derivationDicomCode:
- info["DICOM.DerivationCode"] = derivationDicomCode
- return info
-
- def __init__(self):
- #: name of the statistics plugin
- self.name = ""
- #: keys for all supported measurements
- self.keys = []
- #: measurements that will be calculated by default
- self.defaultKeys = []
- self.requestedKeysCheckboxes = {}
- self.parameterNode = None
- self.parameterNodeObserver = None
-
- def __del__(self):
- if self.parameterNode and self.parameterNodeObserver:
- self.parameterNode.RemoveObserver(self.parameterNodeObserver)
-
- def computeStatistics(self, segmentID):
- """Compute measurements for requested keys on the given segment and return
- as dictionary mapping key's to measurement results
+ """Base class for statistics plugins operating on segments.
+ Derived classes should specify: self.name, self.keys, self.defaultKeys
+ and implement: computeStatistics, getMeasurementInfo
"""
- pass
- def getMeasurementInfo(self, key):
- """Get information (name, description, units, ...) about the measurement for the given key.
- Utilize createMeasurementInfo() to create the dictionary containing the measurement information.
- Measurement information should contain at least name, description, and units.
- DICOM codes should be provided where possible.
- """
- if key not in self.keys:
- return None
- return createMeasurementInfo(key, key, None)
-
- def setDefaultParameters(self, parameterNode, overwriteExisting=False):
- # enable plugin
- pluginName = self.__class__.__name__
- parameter = pluginName + '.enabled'
- if not parameterNode.GetParameter(parameter):
- parameterNode.SetParameter(parameter, str(True))
- # enable all default keys
- for key in self.keys:
- parameter = self.toLongKey(key) + '.enabled'
- if not parameterNode.GetParameter(parameter) or overwriteExisting:
- parameterNode.SetParameter(parameter, str(key in self.defaultKeys))
-
- def getRequestedKeys(self):
- if not self.parameterNode:
- return ()
- requestedKeys = [key for key in self.keys if self.parameterNode.GetParameter(self.toLongKey(key) + '.enabled') == 'True']
- return requestedKeys
-
- def toLongKey(self, key):
- # add name of plugin as a prefix for use outside of plugin
- pluginName = self.__class__.__name__
- return pluginName + '.' + key
-
- def toShortKey(self, key):
- # remove prefix used outside of plugin
- pluginName = self.__class__.__name__
- return key[len(pluginName) + 1:] if key.startswith(pluginName + '.') else ''
-
- def setParameterNode(self, parameterNode):
- if self.parameterNode == parameterNode:
- return
- if self.parameterNode and self.parameterNodeObserver:
- self.parameterNode.RemoveObserver(self.parameterNodeObserver)
- self.parameterNode = parameterNode
- if self.parameterNode:
- self.setDefaultParameters(self.parameterNode)
- self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent,
- self.updateGuiFromParameterNode)
- self.createDefaultOptionsWidget()
- self.updateGuiFromParameterNode()
-
- def getParameterNode(self):
- return self.parameterNode
-
- def createDefaultOptionsWidget(self):
- # create list of checkboxes that allow selection of requested keys
- self.optionsWidget = qt.QWidget()
- form = qt.QFormLayout(self.optionsWidget)
-
- # checkbox to enable/disable plugin
- self.pluginCheckbox = qt.QCheckBox(self.name + " plugin enabled")
- self.pluginCheckbox.checked = True
- self.pluginCheckbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
- form.addRow(self.pluginCheckbox)
-
- # select all/none/default buttons
- selectAllNoneFrame = qt.QFrame(self.optionsWidget)
- selectAllNoneFrame.setLayout(qt.QHBoxLayout())
- selectAllNoneFrame.layout().setSpacing(0)
- selectAllNoneFrame.layout().setMargin(0)
- selectAllNoneFrame.layout().addWidget(qt.QLabel("Select measurements: ", self.optionsWidget))
- selectAllButton = qt.QPushButton('all', self.optionsWidget)
- selectAllNoneFrame.layout().addWidget(selectAllButton)
- selectAllButton.connect('clicked()', self.requestAll)
- selectNoneButton = qt.QPushButton('none', self.optionsWidget)
- selectAllNoneFrame.layout().addWidget(selectNoneButton)
- selectNoneButton.connect('clicked()', self.requestNone)
- selectDefaultButton = qt.QPushButton('default', self.optionsWidget)
- selectAllNoneFrame.layout().addWidget(selectDefaultButton)
- selectDefaultButton.connect('clicked()', self.requestDefault)
- form.addRow(selectAllNoneFrame)
-
- # checkboxes for individual keys
- self.requestedKeysCheckboxes = {}
- requestedKeys = self.getRequestedKeys()
- for key in self.keys:
- label = key
- tooltip = "key: " + key
- info = self.getMeasurementInfo(key)
- if info and ("name" in info or "description" in info):
- label = info["name"] if "name" in info else info["description"]
- if "name" in info: tooltip += "\nname: " + str(info["name"])
- if "description" in info: tooltip += "\ndescription: " + str(info["description"])
- if "units" in info: tooltip += "\nunits: " + (str(info["units"]) if info["units"] else "n/a")
- checkbox = qt.QCheckBox(label, self.optionsWidget)
- checkbox.checked = key in requestedKeys
- checkbox.setToolTip(tooltip)
- form.addRow(checkbox)
- self.requestedKeysCheckboxes[key] = checkbox
- checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
-
- def updateGuiFromParameterNode(self, caller=None, event=None):
- if not self.parameterNode:
- return
- pluginName = self.__class__.__name__
- isEnabled = self.parameterNode.GetParameter(pluginName + '.enabled') != 'False'
- self.pluginCheckbox.checked = isEnabled
- for (key, checkbox) in self.requestedKeysCheckboxes.items():
- parameter = self.toLongKey(key) + '.enabled'
- value = self.parameterNode.GetParameter(parameter) == 'True'
- if checkbox.checked != value:
- previousState = checkbox.blockSignals(True)
- checkbox.checked = value
- checkbox.blockSignals(previousState)
- if checkbox.enabled != isEnabled:
- previousState = checkbox.blockSignals(True)
- checkbox.enabled = isEnabled
- checkbox.blockSignals(previousState)
-
- def updateParameterNodeFromGui(self):
- if not self.parameterNode:
- return
- pluginName = self.__class__.__name__
- self.parameterNode.SetParameter(pluginName + '.enabled', str(self.pluginCheckbox.checked))
- for (key, checkbox) in self.requestedKeysCheckboxes.items():
- parameter = self.toLongKey(key) + '.enabled'
- newValue = str(checkbox.checked)
- currentValue = self.parameterNode.GetParameter(parameter)
- if not currentValue or currentValue != newValue:
- self.parameterNode.SetParameter(parameter, newValue)
-
- def requestAll(self):
- if not self.parameterNode:
- return
- for (key, checkbox) in self.requestedKeysCheckboxes.items():
- parameter = self.toLongKey(key) + '.enabled'
- newValue = str(True)
- currentValue = self.parameterNode.GetParameter(parameter)
- if not currentValue or currentValue != newValue:
- self.parameterNode.SetParameter(parameter, newValue)
-
- def requestNone(self):
- if not self.parameterNode:
- return
- for (key, checkbox) in self.requestedKeysCheckboxes.items():
- parameter = self.toLongKey(key) + '.enabled'
- newValue = str(False)
- currentValue = self.parameterNode.GetParameter(parameter)
- if not currentValue or currentValue != newValue:
- self.parameterNode.SetParameter(parameter, newValue)
-
- def requestDefault(self):
- if not self.parameterNode:
- return
- self.setDefaultParameters(self.parameterNode, overwriteExisting=True)
+ @staticmethod
+ def createCodedEntry(codeValue, codingScheme, codeMeaning, returnAsString=False):
+ """Create a coded entry and return as string or vtkCodedEntry"""
+ entry = slicer.vtkCodedEntry()
+ entry.SetValueSchemeMeaning(codeValue, codingScheme, codeMeaning)
+ return entry if not returnAsString else entry.GetAsString()
+
+ @staticmethod
+ def createMeasurementInfo(name, description, units, quantityDicomCode=None, unitsDicomCode=None,
+ measurementMethodDicomCode=None, derivationDicomCode=None, componentNames=None):
+ """Utility method to create measurement information"""
+ info = {
+ "name": name,
+ "description": description,
+ "units": units
+ }
+ if componentNames:
+ info["componentNames"] = componentNames
+ if quantityDicomCode:
+ info["DICOM.QuantityCode"] = quantityDicomCode
+ if unitsDicomCode:
+ info["DICOM.UnitsCode"] = unitsDicomCode
+ if measurementMethodDicomCode:
+ info["DICOM.MeasurementMethodCode"] = measurementMethodDicomCode
+ if derivationDicomCode:
+ info["DICOM.DerivationCode"] = derivationDicomCode
+ return info
+
+ def __init__(self):
+ #: name of the statistics plugin
+ self.name = ""
+ #: keys for all supported measurements
+ self.keys = []
+ #: measurements that will be calculated by default
+ self.defaultKeys = []
+ self.requestedKeysCheckboxes = {}
+ self.parameterNode = None
+ self.parameterNodeObserver = None
+
+ def __del__(self):
+ if self.parameterNode and self.parameterNodeObserver:
+ self.parameterNode.RemoveObserver(self.parameterNodeObserver)
+
+ def computeStatistics(self, segmentID):
+ """Compute measurements for requested keys on the given segment and return
+ as dictionary mapping key's to measurement results
+ """
+ pass
+
+ def getMeasurementInfo(self, key):
+ """Get information (name, description, units, ...) about the measurement for the given key.
+ Utilize createMeasurementInfo() to create the dictionary containing the measurement information.
+ Measurement information should contain at least name, description, and units.
+ DICOM codes should be provided where possible.
+ """
+ if key not in self.keys:
+ return None
+ return createMeasurementInfo(key, key, None)
+
+ def setDefaultParameters(self, parameterNode, overwriteExisting=False):
+ # enable plugin
+ pluginName = self.__class__.__name__
+ parameter = pluginName + '.enabled'
+ if not parameterNode.GetParameter(parameter):
+ parameterNode.SetParameter(parameter, str(True))
+ # enable all default keys
+ for key in self.keys:
+ parameter = self.toLongKey(key) + '.enabled'
+ if not parameterNode.GetParameter(parameter) or overwriteExisting:
+ parameterNode.SetParameter(parameter, str(key in self.defaultKeys))
+
+ def getRequestedKeys(self):
+ if not self.parameterNode:
+ return ()
+ requestedKeys = [key for key in self.keys if self.parameterNode.GetParameter(self.toLongKey(key) + '.enabled') == 'True']
+ return requestedKeys
+
+ def toLongKey(self, key):
+ # add name of plugin as a prefix for use outside of plugin
+ pluginName = self.__class__.__name__
+ return pluginName + '.' + key
+
+ def toShortKey(self, key):
+ # remove prefix used outside of plugin
+ pluginName = self.__class__.__name__
+ return key[len(pluginName) + 1:] if key.startswith(pluginName + '.') else ''
+
+ def setParameterNode(self, parameterNode):
+ if self.parameterNode == parameterNode:
+ return
+ if self.parameterNode and self.parameterNodeObserver:
+ self.parameterNode.RemoveObserver(self.parameterNodeObserver)
+ self.parameterNode = parameterNode
+ if self.parameterNode:
+ self.setDefaultParameters(self.parameterNode)
+ self.parameterNodeObserver = self.parameterNode.AddObserver(vtk.vtkCommand.ModifiedEvent,
+ self.updateGuiFromParameterNode)
+ self.createDefaultOptionsWidget()
+ self.updateGuiFromParameterNode()
+
+ def getParameterNode(self):
+ return self.parameterNode
+
+ def createDefaultOptionsWidget(self):
+ # create list of checkboxes that allow selection of requested keys
+ self.optionsWidget = qt.QWidget()
+ form = qt.QFormLayout(self.optionsWidget)
+
+ # checkbox to enable/disable plugin
+ self.pluginCheckbox = qt.QCheckBox(self.name + " plugin enabled")
+ self.pluginCheckbox.checked = True
+ self.pluginCheckbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
+ form.addRow(self.pluginCheckbox)
+
+ # select all/none/default buttons
+ selectAllNoneFrame = qt.QFrame(self.optionsWidget)
+ selectAllNoneFrame.setLayout(qt.QHBoxLayout())
+ selectAllNoneFrame.layout().setSpacing(0)
+ selectAllNoneFrame.layout().setMargin(0)
+ selectAllNoneFrame.layout().addWidget(qt.QLabel("Select measurements: ", self.optionsWidget))
+ selectAllButton = qt.QPushButton('all', self.optionsWidget)
+ selectAllNoneFrame.layout().addWidget(selectAllButton)
+ selectAllButton.connect('clicked()', self.requestAll)
+ selectNoneButton = qt.QPushButton('none', self.optionsWidget)
+ selectAllNoneFrame.layout().addWidget(selectNoneButton)
+ selectNoneButton.connect('clicked()', self.requestNone)
+ selectDefaultButton = qt.QPushButton('default', self.optionsWidget)
+ selectAllNoneFrame.layout().addWidget(selectDefaultButton)
+ selectDefaultButton.connect('clicked()', self.requestDefault)
+ form.addRow(selectAllNoneFrame)
+
+ # checkboxes for individual keys
+ self.requestedKeysCheckboxes = {}
+ requestedKeys = self.getRequestedKeys()
+ for key in self.keys:
+ label = key
+ tooltip = "key: " + key
+ info = self.getMeasurementInfo(key)
+ if info and ("name" in info or "description" in info):
+ label = info["name"] if "name" in info else info["description"]
+ if "name" in info: tooltip += "\nname: " + str(info["name"])
+ if "description" in info: tooltip += "\ndescription: " + str(info["description"])
+ if "units" in info: tooltip += "\nunits: " + (str(info["units"]) if info["units"] else "n/a")
+ checkbox = qt.QCheckBox(label, self.optionsWidget)
+ checkbox.checked = key in requestedKeys
+ checkbox.setToolTip(tooltip)
+ form.addRow(checkbox)
+ self.requestedKeysCheckboxes[key] = checkbox
+ checkbox.connect('stateChanged(int)', self.updateParameterNodeFromGui)
+
+ def updateGuiFromParameterNode(self, caller=None, event=None):
+ if not self.parameterNode:
+ return
+ pluginName = self.__class__.__name__
+ isEnabled = self.parameterNode.GetParameter(pluginName + '.enabled') != 'False'
+ self.pluginCheckbox.checked = isEnabled
+ for (key, checkbox) in self.requestedKeysCheckboxes.items():
+ parameter = self.toLongKey(key) + '.enabled'
+ value = self.parameterNode.GetParameter(parameter) == 'True'
+ if checkbox.checked != value:
+ previousState = checkbox.blockSignals(True)
+ checkbox.checked = value
+ checkbox.blockSignals(previousState)
+ if checkbox.enabled != isEnabled:
+ previousState = checkbox.blockSignals(True)
+ checkbox.enabled = isEnabled
+ checkbox.blockSignals(previousState)
+
+ def updateParameterNodeFromGui(self):
+ if not self.parameterNode:
+ return
+ pluginName = self.__class__.__name__
+ self.parameterNode.SetParameter(pluginName + '.enabled', str(self.pluginCheckbox.checked))
+ for (key, checkbox) in self.requestedKeysCheckboxes.items():
+ parameter = self.toLongKey(key) + '.enabled'
+ newValue = str(checkbox.checked)
+ currentValue = self.parameterNode.GetParameter(parameter)
+ if not currentValue or currentValue != newValue:
+ self.parameterNode.SetParameter(parameter, newValue)
+
+ def requestAll(self):
+ if not self.parameterNode:
+ return
+ for (key, checkbox) in self.requestedKeysCheckboxes.items():
+ parameter = self.toLongKey(key) + '.enabled'
+ newValue = str(True)
+ currentValue = self.parameterNode.GetParameter(parameter)
+ if not currentValue or currentValue != newValue:
+ self.parameterNode.SetParameter(parameter, newValue)
+
+ def requestNone(self):
+ if not self.parameterNode:
+ return
+ for (key, checkbox) in self.requestedKeysCheckboxes.items():
+ parameter = self.toLongKey(key) + '.enabled'
+ newValue = str(False)
+ currentValue = self.parameterNode.GetParameter(parameter)
+ if not currentValue or currentValue != newValue:
+ self.parameterNode.SetParameter(parameter, newValue)
+
+ def requestDefault(self):
+ if not self.parameterNode:
+ return
+ self.setDefaultParameters(self.parameterNode, overwriteExisting=True)
diff --git a/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py b/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py
index 444b45b310f..4c4cbc7c0ea 100644
--- a/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py
+++ b/Modules/Scripted/SegmentStatistics/SubjectHierarchyPlugins/SegmentStatisticsSubjectHierarchyPlugin.py
@@ -8,124 +8,124 @@
class SegmentStatisticsSubjectHierarchyPlugin(AbstractScriptedSubjectHierarchyPlugin):
- """ Scripted subject hierarchy plugin for the Segment Statistics module.
-
- This is also an example for scripted plugins, so includes all possible methods.
- The methods that are not needed (i.e. the default implementation in
- qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
- omitted in plugins created based on this one.
- """
-
- # Necessary static member to be able to set python source to scripted subject hierarchy plugin
- filePath = __file__
-
- def __init__(self, scriptedPlugin):
- scriptedPlugin.name = 'SegmentStatistics'
- AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
-
- self.segmentStatisticsAction = qt.QAction("Calculate statistics...", scriptedPlugin)
- self.segmentStatisticsAction.connect("triggered()", self.onCalculateStatistics)
-
- def canAddNodeToSubjectHierarchy(self, node, parentItemID):
- # This plugin cannot own any items (it's not a role but a function plugin),
- # but the it can be decided the following way:
- # if node is not None and node.IsA("vtkMRMLMyNode"):
- # return 1.0
- return 0.0
-
- def canOwnSubjectHierarchyItem(self, itemID):
- # This plugin cannot own any items (it's not a role but a function plugin),
- # but the it can be decided the following way:
- # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- # shNode = pluginHandlerSingleton.subjectHierarchyNode()
- # associatedNode = shNode.GetItemDataNode(itemID)
- # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode")
- # return 1.0
- return 0.0
-
- def roleForPlugin(self):
- # As this plugin cannot own any items, it doesn't have a role either
- return "N/A"
-
- def helpText(self):
- # return (""
- # ""
- # "SegmentStatistics module subject hierarchy help text"
- # ""
- # "
"
- # ""
- # ""
- # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
- # ""
- # "
\n")
- return ""
-
- def icon(self, itemID):
- # As this plugin cannot own any items, it doesn't have an icon eitherimport os
- # import os
- # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png')
- # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath):
- # return qt.QIcon(iconPath)
- # Item unknown by plugin
- return qt.QIcon()
-
- def visibilityIcon(self, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
-
- def editProperties(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
-
- def itemContextMenuActions(self):
- return [self.segmentStatisticsAction]
-
- def onCalculateStatistics(self):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- currentItemID = pluginHandlerSingleton.currentItem()
- if not currentItemID:
- logging.error("Invalid current item")
-
- shNode = pluginHandlerSingleton.subjectHierarchyNode()
- segmentationNode = shNode.GetItemDataNode(currentItemID)
-
- # Select segmentation node in segment statistics
- pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentStatistics')
- statisticsWidget = slicer.modules.segmentstatistics.widgetRepresentation().self()
- statisticsWidget.segmentationSelector.setCurrentNode(segmentationNode)
-
- # Get master volume from segmentation
- masterVolume = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole())
- if masterVolume is not None:
- statisticsWidget.scalarSelector.setCurrentNode(masterVolume)
-
- def sceneContextMenuActions(self):
- return []
-
- def showContextMenuActionsForItem(self, itemID):
- # Scene
- if not itemID:
- # No scene context menu actions in this plugin
- return
-
- # Volume but not LabelMap
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- if pluginHandlerSingleton.pluginByName('Segmentations').canOwnSubjectHierarchyItem(itemID):
- # Get current item
- currentItemID = pluginHandlerSingleton.currentItem()
- if not currentItemID:
- logging.error("Invalid current item")
- return
- self.segmentStatisticsAction.visible = True
-
- def tooltip(self, itemID):
- # As this plugin cannot own any items, it doesn't provide tooltip either
- return ""
-
- def setDisplayVisibility(self, itemID, visible):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
-
- def getDisplayVisibility(self, itemID):
- pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
- return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
+ """ Scripted subject hierarchy plugin for the Segment Statistics module.
+
+ This is also an example for scripted plugins, so includes all possible methods.
+ The methods that are not needed (i.e. the default implementation in
+ qSlicerSubjectHierarchyAbstractPlugin is satisfactory) can simply be
+ omitted in plugins created based on this one.
+ """
+
+ # Necessary static member to be able to set python source to scripted subject hierarchy plugin
+ filePath = __file__
+
+ def __init__(self, scriptedPlugin):
+ scriptedPlugin.name = 'SegmentStatistics'
+ AbstractScriptedSubjectHierarchyPlugin.__init__(self, scriptedPlugin)
+
+ self.segmentStatisticsAction = qt.QAction("Calculate statistics...", scriptedPlugin)
+ self.segmentStatisticsAction.connect("triggered()", self.onCalculateStatistics)
+
+ def canAddNodeToSubjectHierarchy(self, node, parentItemID):
+ # This plugin cannot own any items (it's not a role but a function plugin),
+ # but the it can be decided the following way:
+ # if node is not None and node.IsA("vtkMRMLMyNode"):
+ # return 1.0
+ return 0.0
+
+ def canOwnSubjectHierarchyItem(self, itemID):
+ # This plugin cannot own any items (it's not a role but a function plugin),
+ # but the it can be decided the following way:
+ # pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ # shNode = pluginHandlerSingleton.subjectHierarchyNode()
+ # associatedNode = shNode.GetItemDataNode(itemID)
+ # if associatedNode is not None and associatedNode.IsA("vtkMRMLMyNode")
+ # return 1.0
+ return 0.0
+
+ def roleForPlugin(self):
+ # As this plugin cannot own any items, it doesn't have a role either
+ return "N/A"
+
+ def helpText(self):
+ # return (""
+ # ""
+ # "SegmentStatistics module subject hierarchy help text"
+ # ""
+ # "
"
+ # ""
+ # ""
+ # "This is how you can add help text to the subject hierarchy module help box via a python scripted plugin."
+ # ""
+ # "
\n")
+ return ""
+
+ def icon(self, itemID):
+ # As this plugin cannot own any items, it doesn't have an icon eitherimport os
+ # import os
+ # iconPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons/MyIcon.png')
+ # if self.canOwnSubjectHierarchyItem(itemID) > 0.0 and os.path.exists(iconPath):
+ # return qt.QIcon(iconPath)
+ # Item unknown by plugin
+ return qt.QIcon()
+
+ def visibilityIcon(self, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').visibilityIcon(visible)
+
+ def editProperties(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').editProperties(itemID)
+
+ def itemContextMenuActions(self):
+ return [self.segmentStatisticsAction]
+
+ def onCalculateStatistics(self):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ currentItemID = pluginHandlerSingleton.currentItem()
+ if not currentItemID:
+ logging.error("Invalid current item")
+
+ shNode = pluginHandlerSingleton.subjectHierarchyNode()
+ segmentationNode = shNode.GetItemDataNode(currentItemID)
+
+ # Select segmentation node in segment statistics
+ pluginHandlerSingleton.pluginByName('Default').switchToModule('SegmentStatistics')
+ statisticsWidget = slicer.modules.segmentstatistics.widgetRepresentation().self()
+ statisticsWidget.segmentationSelector.setCurrentNode(segmentationNode)
+
+ # Get master volume from segmentation
+ masterVolume = segmentationNode.GetNodeReference(slicer.vtkMRMLSegmentationNode.GetReferenceImageGeometryReferenceRole())
+ if masterVolume is not None:
+ statisticsWidget.scalarSelector.setCurrentNode(masterVolume)
+
+ def sceneContextMenuActions(self):
+ return []
+
+ def showContextMenuActionsForItem(self, itemID):
+ # Scene
+ if not itemID:
+ # No scene context menu actions in this plugin
+ return
+
+ # Volume but not LabelMap
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ if pluginHandlerSingleton.pluginByName('Segmentations').canOwnSubjectHierarchyItem(itemID):
+ # Get current item
+ currentItemID = pluginHandlerSingleton.currentItem()
+ if not currentItemID:
+ logging.error("Invalid current item")
+ return
+ self.segmentStatisticsAction.visible = True
+
+ def tooltip(self, itemID):
+ # As this plugin cannot own any items, it doesn't provide tooltip either
+ return ""
+
+ def setDisplayVisibility(self, itemID, visible):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ pluginHandlerSingleton.pluginByName('Default').setDisplayVisibility(itemID, visible)
+
+ def getDisplayVisibility(self, itemID):
+ pluginHandlerSingleton = slicer.qSlicerSubjectHierarchyPluginHandler.instance()
+ return pluginHandlerSingleton.pluginByName('Default').getDisplayVisibility(itemID)
diff --git a/Modules/Scripted/SelfTests/SelfTests.py b/Modules/Scripted/SelfTests/SelfTests.py
index a1b3deb07b7..90cc97ea0ed 100644
--- a/Modules/Scripted/SelfTests/SelfTests.py
+++ b/Modules/Scripted/SelfTests/SelfTests.py
@@ -17,44 +17,44 @@
class ExampleSelfTests:
- @staticmethod
- def closeScene():
- """Close the scene"""
- slicer.mrmlScene.Clear(0)
+ @staticmethod
+ def closeScene():
+ """Close the scene"""
+ slicer.mrmlScene.Clear(0)
class SelfTests(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "SelfTests"
- self.parent.categories = ["Testing"]
- self.parent.contributors = ["Steve Pieper (Isomics)"]
- self.parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "SelfTests"
+ self.parent.categories = ["Testing"]
+ self.parent.contributors = ["Steve Pieper (Isomics)"]
+ self.parent.helpText = """
The SelfTests module allows developers to provide built-in self-tests (BIST) for slicer so that users can tell
if their installed version of slicer are running as designed.
"""
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = """
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = """
This work is part of SparKit project, funded by Cancer Care Ontario (CCO)'s ACRU program
and Ontario Consortium for Adaptive Interventions in Radiation Oncology (OCAIRO).
"""
- #
- # slicer.selfTests is a dictionary of tests that are registered
- # here or in other parts of the code. The key is the name of the test
- # and the value is a python callable that runs the test and returns
- # if the test passed or raises and exception if it fails.
- # the __doc__ attribute of the test is used as a tooltip for the test
- # button.
- #
- try:
- slicer.selfTests
- except AttributeError:
- slicer.selfTests = {}
-
- # register the example tests
- slicer.selfTests['MRMLSceneExists'] = lambda: slicer.app.mrmlScene
- slicer.selfTests['CloseScene'] = ExampleSelfTests.closeScene
+ #
+ # slicer.selfTests is a dictionary of tests that are registered
+ # here or in other parts of the code. The key is the name of the test
+ # and the value is a python callable that runs the test and returns
+ # if the test passed or raises and exception if it fails.
+ # the __doc__ attribute of the test is used as a tooltip for the test
+ # button.
+ #
+ try:
+ slicer.selfTests
+ except AttributeError:
+ slicer.selfTests = {}
+
+ # register the example tests
+ slicer.selfTests['MRMLSceneExists'] = lambda: slicer.app.mrmlScene
+ slicer.selfTests['CloseScene'] = ExampleSelfTests.closeScene
#
# SelfTests widget
@@ -62,126 +62,126 @@ def __init__(self, parent):
class SelfTestsWidget(ScriptedLoadableModuleWidget):
- """Slicer module that creates the Qt GUI for interacting with SelfTests
- Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
+ """Slicer module that creates the Qt GUI for interacting with SelfTests
+ Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
- # sets up the widget
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
+ # sets up the widget
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
- # This module is often used in developer mode, therefore
- # collapse reload & test section by default.
- if hasattr(self, "reloadCollapsibleButton"):
- self.reloadCollapsibleButton.collapsed = True
+ # This module is often used in developer mode, therefore
+ # collapse reload & test section by default.
+ if hasattr(self, "reloadCollapsibleButton"):
+ self.reloadCollapsibleButton.collapsed = True
- self.logic = SelfTestsLogic(slicer.selfTests)
+ self.logic = SelfTestsLogic(slicer.selfTests)
- globals()['selfTests'] = self
+ globals()['selfTests'] = self
- #
- # test list
- #
+ #
+ # test list
+ #
- self.testList = ctk.ctkCollapsibleButton(self.parent)
- self.testList.setLayout(qt.QVBoxLayout())
- self.testList.setText("Self Tests")
- self.layout.addWidget(self.testList)
- self.testList.collapsed = False
+ self.testList = ctk.ctkCollapsibleButton(self.parent)
+ self.testList.setLayout(qt.QVBoxLayout())
+ self.testList.setText("Self Tests")
+ self.layout.addWidget(self.testList)
+ self.testList.collapsed = False
- self.runAll = qt.QPushButton("Run All")
- self.testList.layout().addWidget(self.runAll)
- self.runAll.connect('clicked()', self.onRunAll)
+ self.runAll = qt.QPushButton("Run All")
+ self.testList.layout().addWidget(self.runAll)
+ self.runAll.connect('clicked()', self.onRunAll)
- self.testButtons = {}
- self.testMapper = qt.QSignalMapper()
- self.testMapper.connect('mapped(const QString&)', self.onRun)
- testKeys = sorted(slicer.selfTests.keys())
- for test in testKeys:
- self.testButtons[test] = qt.QPushButton(test)
- self.testButtons[test].setToolTip(slicer.selfTests[test].__doc__)
- self.testList.layout().addWidget(self.testButtons[test])
- self.testMapper.setMapping(self.testButtons[test], test)
- self.testButtons[test].connect('clicked()', self.testMapper, 'map()')
+ self.testButtons = {}
+ self.testMapper = qt.QSignalMapper()
+ self.testMapper.connect('mapped(const QString&)', self.onRun)
+ testKeys = sorted(slicer.selfTests.keys())
+ for test in testKeys:
+ self.testButtons[test] = qt.QPushButton(test)
+ self.testButtons[test].setToolTip(slicer.selfTests[test].__doc__)
+ self.testList.layout().addWidget(self.testButtons[test])
+ self.testMapper.setMapping(self.testButtons[test], test)
+ self.testButtons[test].connect('clicked()', self.testMapper, 'map()')
- # Add spacer to layout
- self.layout.addStretch(1)
+ # Add spacer to layout
+ self.layout.addStretch(1)
- def onRunAll(self):
- self.logic.run(continueCheck=self.continueCheck)
- slicer.util.infoDisplay(self.logic, windowTitle='SelfTests')
+ def onRunAll(self):
+ self.logic.run(continueCheck=self.continueCheck)
+ slicer.util.infoDisplay(self.logic, windowTitle='SelfTests')
- def onRun(self, test):
- self.logic.run([test, ], continueCheck=self.continueCheck)
- slicer.util.infoDisplay(self.logic, windowTitle='SelfTests')
+ def onRun(self, test):
+ self.logic.run([test, ], continueCheck=self.continueCheck)
+ slicer.util.infoDisplay(self.logic, windowTitle='SelfTests')
- def continueCheck(self, logic):
- slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
- return True
+ def continueCheck(self, logic):
+ slicer.app.processEvents(qt.QEventLoop.ExcludeUserInputEvents)
+ return True
class SelfTestsLogic:
- """Logic to handle invoking the tests and reporting the results"""
-
- def __init__(self, selfTests):
- self.selfTests = selfTests
- self.results = {}
- self.passed = []
- self.failed = []
-
- def __str__(self):
- testsRun = len(list(self.results.keys()))
- if testsRun == 0:
- return "No tests run"
- s = "%.0f%% passed (%d of %d)" % (
- (100. * len(self.passed) / testsRun),
- len(self.passed), testsRun)
- s += "\n---\n"
- for test in self.results:
- s += f"{test}\t{self.results[test]}\n"
- return s
-
- def run(self, tests=None, continueCheck=None):
- if not tests:
- tests = list(self.selfTests.keys())
-
- for test in tests:
- try:
- result = self.selfTests[test]()
- self.passed.append(test)
- except Exception as e:
- traceback.print_exc()
- result = "Failed with: %s" % e
- self.failed.append(test)
- self.results[test] = result
- if continueCheck:
- if not continueCheck(self):
- return
+ """Logic to handle invoking the tests and reporting the results"""
+
+ def __init__(self, selfTests):
+ self.selfTests = selfTests
+ self.results = {}
+ self.passed = []
+ self.failed = []
+
+ def __str__(self):
+ testsRun = len(list(self.results.keys()))
+ if testsRun == 0:
+ return "No tests run"
+ s = "%.0f%% passed (%d of %d)" % (
+ (100. * len(self.passed) / testsRun),
+ len(self.passed), testsRun)
+ s += "\n---\n"
+ for test in self.results:
+ s += f"{test}\t{self.results[test]}\n"
+ return s
+
+ def run(self, tests=None, continueCheck=None):
+ if not tests:
+ tests = list(self.selfTests.keys())
+
+ for test in tests:
+ try:
+ result = self.selfTests[test]()
+ self.passed.append(test)
+ except Exception as e:
+ traceback.print_exc()
+ result = "Failed with: %s" % e
+ self.failed.append(test)
+ self.results[test] = result
+ if continueCheck:
+ if not continueCheck(self):
+ return
def SelfTestsTest():
- if hasattr(slicer, 'selfTests'):
- logic = SelfTestsLogic(list(slicer.selfTests.keys()))
- logic.run()
- print(logic.results)
- print("SelfTestsTest Passed!")
- return logic.failed == []
+ if hasattr(slicer, 'selfTests'):
+ logic = SelfTestsLogic(list(slicer.selfTests.keys()))
+ logic.run()
+ print(logic.results)
+ print("SelfTestsTest Passed!")
+ return logic.failed == []
def SelfTestsDemo():
- pass
+ pass
if __name__ == "__main__":
- import sys
- if '--test' in sys.argv:
- if SelfTestsTest():
- exit(0)
- exit(1)
- if '--demo' in sys.argv:
- SelfTestsDemo()
- exit()
- # TODO - 'exit()' returns so this code gets run
- # even if the argument matches one of the cases above
- # print ("usage: SelfTests.py [--test | --demo]")
+ import sys
+ if '--test' in sys.argv:
+ if SelfTestsTest():
+ exit(0)
+ exit(1)
+ if '--demo' in sys.argv:
+ SelfTestsDemo()
+ exit()
+ # TODO - 'exit()' returns so this code gets run
+ # even if the argument matches one of the cases above
+ # print ("usage: SelfTests.py [--test | --demo]")
diff --git a/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py b/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py
index f40997b156c..a755f5c9768 100644
--- a/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py
+++ b/Modules/Scripted/VectorToScalarVolume/VectorToScalarVolume.py
@@ -12,45 +12,45 @@
@contextmanager
def MyScopedQtPropertySetter(qobject, properties):
- """ Context manager to set/reset properties"""
- # TODO: Move it to slicer.utils and delete it here.
- previousValues = {}
- for propertyName, propertyValue in properties.items():
- previousValues[propertyName] = getattr(qobject, propertyName)
- setattr(qobject, propertyName, propertyValue)
- yield
- for propertyName in properties.keys():
- setattr(qobject, propertyName, previousValues[propertyName])
+ """ Context manager to set/reset properties"""
+ # TODO: Move it to slicer.utils and delete it here.
+ previousValues = {}
+ for propertyName, propertyValue in properties.items():
+ previousValues[propertyName] = getattr(qobject, propertyName)
+ setattr(qobject, propertyName, propertyValue)
+ yield
+ for propertyName in properties.keys():
+ setattr(qobject, propertyName, previousValues[propertyName])
@contextmanager
def MyObjectsBlockSignals(*qobjects):
- """
- Context manager to block/reset signals of any number of input qobjects.
- Usage:
- with MyObjectsBlockSignals(self.aComboBox, self.otherComboBox):
- """
- # TODO: Move it to slicer.utils and delete it here.
- previousValues = list()
- for qobject in qobjects:
- # blockedSignal returns the previous value of signalsBlocked()
- previousValues.append(qobject.blockSignals(True))
- yield
- for (qobject, previousValue) in zip(qobjects, previousValues):
- qobject.blockSignals(previousValue)
+ """
+ Context manager to block/reset signals of any number of input qobjects.
+ Usage:
+ with MyObjectsBlockSignals(self.aComboBox, self.otherComboBox):
+ """
+ # TODO: Move it to slicer.utils and delete it here.
+ previousValues = list()
+ for qobject in qobjects:
+ # blockedSignal returns the previous value of signalsBlocked()
+ previousValues.append(qobject.blockSignals(True))
+ yield
+ for (qobject, previousValue) in zip(qobjects, previousValues):
+ qobject.blockSignals(previousValue)
def getNode(nodeID):
- if nodeID is None:
- return None
- return slicer.mrmlScene.GetNodeByID(nodeID)
+ if nodeID is None:
+ return None
+ return slicer.mrmlScene.GetNodeByID(nodeID)
def getNodeID(node):
- if node is None:
- return ""
- else:
- return node.GetID()
+ if node is None:
+ return ""
+ else:
+ return node.GetID()
#
@@ -58,15 +58,15 @@ def getNodeID(node):
#
class VectorToScalarVolume(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "Vector to Scalar Volume"
- self.parent.categories = ["Converters"]
- self.parent.dependencies = []
- self.parent.contributors = ["Steve Pieper (Isomics)",
- "Pablo Hernandez-Cerdan (Kitware)",
- "Jean-Christophe Fillion-Robin (Kitware)", ]
- self.parent.helpText = """
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "Vector to Scalar Volume"
+ self.parent.categories = ["Converters"]
+ self.parent.dependencies = []
+ self.parent.contributors = ["Steve Pieper (Isomics)",
+ "Pablo Hernandez-Cerdan (Kitware)",
+ "Jean-Christophe Fillion-Robin (Kitware)", ]
+ self.parent.helpText = """
Make a scalar (1 component) volume from a vector volume.
It provides multiple conversion modes:
@@ -77,7 +77,7 @@ def __init__(self, parent):
- computes the mean of all the components.
"""
- self.parent.acknowledgementText = """
+ self.parent.acknowledgementText = """
Developed by Steve Pieper, Isomics, Inc.,
partially funded by NIH grant 3P41RR013218-12S1 (NAC) and is part of the National Alliance
for Medical Image Computing (NA-MIC), funded by the National Institutes of Health through the
@@ -89,205 +89,205 @@ def __init__(self, parent):
#
class VectorToScalarVolumeWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
- """
- The user selected parameters are stored in a parameterNode.
- """
-
- def __init__(self, parent=None):
- ScriptedLoadableModuleWidget.__init__(self, parent)
- VTKObservationMixin.__init__(self)
- self.logic = None
- self._parameterNode = None
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- self.logic = VectorToScalarVolumeLogic()
- # This will use createParameterNode with the provided default options
- self.setParameterNode(self.logic.getParameterNode())
-
- self.parameterSetSelectionCollapsibleButton = ctk.ctkCollapsibleButton()
- self.parameterSetSelectionCollapsibleButton.text = "Parameter set"
- self.layout.addWidget(self.parameterSetSelectionCollapsibleButton)
-
- # Layout within the "Selection" collapsible button
- parameterSetSelectionFormLayout = qt.QFormLayout(self.parameterSetSelectionCollapsibleButton)
-
- # Parameter set selector (inspired by SegmentStatistics.py)
- self.parameterNodeSelector = slicer.qMRMLNodeComboBox()
- self.parameterNodeSelector.nodeTypes = (("vtkMRMLScriptedModuleNode"), "")
- self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "VectorToScalarVolume")
- self.parameterNodeSelector.selectNodeUponCreation = True
- self.parameterNodeSelector.addEnabled = True
- self.parameterNodeSelector.renameEnabled = True
- self.parameterNodeSelector.removeEnabled = True
- self.parameterNodeSelector.noneEnabled = False
- self.parameterNodeSelector.showHidden = True
- self.parameterNodeSelector.showChildNodeTypes = False
- self.parameterNodeSelector.baseName = "VectorToScalarVolume"
- self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene)
- self.parameterNodeSelector.toolTip = "Pick parameter set"
- parameterSetSelectionFormLayout.addRow("Parameter set: ", self.parameterNodeSelector)
-
- # Parameters
- self.selectionCollapsibleButton = ctk.ctkCollapsibleButton()
- self.selectionCollapsibleButton.text = "Conversion settings"
- self.layout.addWidget(self.selectionCollapsibleButton)
-
- # Layout within the "Selection" collapsible button
- parametersFormLayout = qt.QFormLayout(self.selectionCollapsibleButton)
-
- #
- # the volume selectors
- #
- self.inputSelector = slicer.qMRMLNodeComboBox()
- self.inputSelector.nodeTypes = ["vtkMRMLVectorVolumeNode"]
- self.inputSelector.addEnabled = False
- self.inputSelector.removeEnabled = False
- self.inputSelector.setMRMLScene(slicer.mrmlScene)
- parametersFormLayout.addRow("Input Vector Volume: ", self.inputSelector)
-
- self.outputSelector = slicer.qMRMLNodeComboBox()
- self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
- self.outputSelector.hideChildNodeTypes = ["vtkMRMLVectorVolumeNode"]
- self.outputSelector.setMRMLScene(slicer.mrmlScene)
- self.outputSelector.addEnabled = True
- self.outputSelector.renameEnabled = True
- self.outputSelector.baseName = "Scalar Volume"
- parametersFormLayout.addRow("Output Scalar Volume: ", self.outputSelector)
-
- #
- # Options to extract single components
- #
- self.conversionMethodWidget = VectorToScalarVolumeConversionMethodWidget()
- parametersFormLayout.addRow("Conversion Method: ", self.conversionMethodWidget)
- # Apply button
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.toolTip = "Run Convert the vector to scalar."
- parametersFormLayout.addRow(self.applyButton)
-
- # Add vertical spacer
- self.layout.addStretch(1)
-
- # Connections
- self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode)
- self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.updateGuiFromMRML)
-
- # updateParameterNodeFromGui
- self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui)
- self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui)
-
- self.applyButton.connect('clicked(bool)', self.onApply)
-
- # conversion widget
- self.conversionMethodWidget.methodSelectorComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui)
- self.conversionMethodWidget.componentsComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui)
-
- # The parameter node had defaults at creation, propagate them to the GUI.
- self.updateGuiFromMRML()
-
- def cleanup(self):
- self.removeObservers()
-
- def parameterNode(self):
- return self._parameterNode
-
- def setParameterNode(self, inputParameterNode):
- if inputParameterNode == self._parameterNode:
- return
- if self._parameterNode is not None:
- self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML)
- if inputParameterNode is not None:
- self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML)
- self._parameterNode = inputParameterNode
-
- def inputVolumeNode(self):
- return self.inputSelector.currentNode()
-
- def setInputVolumeNode(self, node):
- if isinstance(node, str):
- node = getNode(node)
- self.inputSelector.setCurrentNode(node)
-
- def outputVolumeNode(self):
- return self.outputSelector.currentNode()
-
- def setOutputVolumeNode(self, node):
- if isinstance(node, str):
- node = getNode(node)
- self.outputSelector.setCurrentNode(node)
-
- def updateButtonStates(self):
-
- isMethodSingleComponent = self._parameterNode.GetParameter("ConversionMethod") == VectorToScalarVolumeLogic.SINGLE_COMPONENT
-
- # Update apply button state and tooltip
- applyErrorMessage = ""
- if not self.inputVolumeNode():
- applyErrorMessage = "Please select Input Vector Volume"
- elif not self.outputVolumeNode():
- applyErrorMessage = "Please select Output Scalar Volume"
- elif not self.parameterNode():
- applyErrorMessage = "Please select Parameter set"
- elif isMethodSingleComponent and (int(self._parameterNode.GetParameter("ComponentToExtract")) < 0):
- applyErrorMessage = "Please select a component to extract"
-
- self.applyButton.enabled = (not applyErrorMessage)
- self.applyButton.toolTip = applyErrorMessage
-
- self.conversionMethodWidget.componentsComboBox.visible = isMethodSingleComponent
-
- if (self.inputVolumeNode() is not None) and isMethodSingleComponent:
- imageComponents = self.inputVolumeNode().GetImageData().GetNumberOfScalarComponents()
- wasBlocked = self.conversionMethodWidget.componentsComboBox.blockSignals(True)
- if self.conversionMethodWidget.componentsComboBox.count != imageComponents:
- self.conversionMethodWidget.componentsComboBox.clear()
- for comp in range(imageComponents):
- self.conversionMethodWidget.componentsComboBox.insertItem(comp, str(comp))
- self.conversionMethodWidget.componentsComboBox.blockSignals(wasBlocked)
-
- def updateGuiFromMRML(self, caller=None, event=None):
"""
- Query all the parameters in the parameterNode,
- and update the GUI state accordingly if something has changed.
+ The user selected parameters are stored in a parameterNode.
"""
- self.updateButtonStates()
-
- if not self.parameterNode():
- return
+ def __init__(self, parent=None):
+ ScriptedLoadableModuleWidget.__init__(self, parent)
+ VTKObservationMixin.__init__(self)
+ self.logic = None
+ self._parameterNode = None
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ self.logic = VectorToScalarVolumeLogic()
+ # This will use createParameterNode with the provided default options
+ self.setParameterNode(self.logic.getParameterNode())
+
+ self.parameterSetSelectionCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.parameterSetSelectionCollapsibleButton.text = "Parameter set"
+ self.layout.addWidget(self.parameterSetSelectionCollapsibleButton)
+
+ # Layout within the "Selection" collapsible button
+ parameterSetSelectionFormLayout = qt.QFormLayout(self.parameterSetSelectionCollapsibleButton)
+
+ # Parameter set selector (inspired by SegmentStatistics.py)
+ self.parameterNodeSelector = slicer.qMRMLNodeComboBox()
+ self.parameterNodeSelector.nodeTypes = (("vtkMRMLScriptedModuleNode"), "")
+ self.parameterNodeSelector.addAttribute("vtkMRMLScriptedModuleNode", "ModuleName", "VectorToScalarVolume")
+ self.parameterNodeSelector.selectNodeUponCreation = True
+ self.parameterNodeSelector.addEnabled = True
+ self.parameterNodeSelector.renameEnabled = True
+ self.parameterNodeSelector.removeEnabled = True
+ self.parameterNodeSelector.noneEnabled = False
+ self.parameterNodeSelector.showHidden = True
+ self.parameterNodeSelector.showChildNodeTypes = False
+ self.parameterNodeSelector.baseName = "VectorToScalarVolume"
+ self.parameterNodeSelector.setMRMLScene(slicer.mrmlScene)
+ self.parameterNodeSelector.toolTip = "Pick parameter set"
+ parameterSetSelectionFormLayout.addRow("Parameter set: ", self.parameterNodeSelector)
+
+ # Parameters
+ self.selectionCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.selectionCollapsibleButton.text = "Conversion settings"
+ self.layout.addWidget(self.selectionCollapsibleButton)
+
+ # Layout within the "Selection" collapsible button
+ parametersFormLayout = qt.QFormLayout(self.selectionCollapsibleButton)
+
+ #
+ # the volume selectors
+ #
+ self.inputSelector = slicer.qMRMLNodeComboBox()
+ self.inputSelector.nodeTypes = ["vtkMRMLVectorVolumeNode"]
+ self.inputSelector.addEnabled = False
+ self.inputSelector.removeEnabled = False
+ self.inputSelector.setMRMLScene(slicer.mrmlScene)
+ parametersFormLayout.addRow("Input Vector Volume: ", self.inputSelector)
+
+ self.outputSelector = slicer.qMRMLNodeComboBox()
+ self.outputSelector.nodeTypes = ["vtkMRMLScalarVolumeNode"]
+ self.outputSelector.hideChildNodeTypes = ["vtkMRMLVectorVolumeNode"]
+ self.outputSelector.setMRMLScene(slicer.mrmlScene)
+ self.outputSelector.addEnabled = True
+ self.outputSelector.renameEnabled = True
+ self.outputSelector.baseName = "Scalar Volume"
+ parametersFormLayout.addRow("Output Scalar Volume: ", self.outputSelector)
+
+ #
+ # Options to extract single components
+ #
+ self.conversionMethodWidget = VectorToScalarVolumeConversionMethodWidget()
+ parametersFormLayout.addRow("Conversion Method: ", self.conversionMethodWidget)
+ # Apply button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.toolTip = "Run Convert the vector to scalar."
+ parametersFormLayout.addRow(self.applyButton)
+
+ # Add vertical spacer
+ self.layout.addStretch(1)
+
+ # Connections
+ self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.setParameterNode)
+ self.parameterNodeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.updateGuiFromMRML)
+
+ # updateParameterNodeFromGui
+ self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui)
+ self.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGui)
+
+ self.applyButton.connect('clicked(bool)', self.onApply)
+
+ # conversion widget
+ self.conversionMethodWidget.methodSelectorComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui)
+ self.conversionMethodWidget.componentsComboBox.connect('currentIndexChanged(int)', self.updateParameterNodeFromGui)
+
+ # The parameter node had defaults at creation, propagate them to the GUI.
+ self.updateGuiFromMRML()
+
+ def cleanup(self):
+ self.removeObservers()
+
+ def parameterNode(self):
+ return self._parameterNode
+
+ def setParameterNode(self, inputParameterNode):
+ if inputParameterNode == self._parameterNode:
+ return
+ if self._parameterNode is not None:
+ self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML)
+ if inputParameterNode is not None:
+ self.addObserver(inputParameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGuiFromMRML)
+ self._parameterNode = inputParameterNode
+
+ def inputVolumeNode(self):
+ return self.inputSelector.currentNode()
+
+ def setInputVolumeNode(self, node):
+ if isinstance(node, str):
+ node = getNode(node)
+ self.inputSelector.setCurrentNode(node)
+
+ def outputVolumeNode(self):
+ return self.outputSelector.currentNode()
+
+ def setOutputVolumeNode(self, node):
+ if isinstance(node, str):
+ node = getNode(node)
+ self.outputSelector.setCurrentNode(node)
+
+ def updateButtonStates(self):
+
+ isMethodSingleComponent = self._parameterNode.GetParameter("ConversionMethod") == VectorToScalarVolumeLogic.SINGLE_COMPONENT
+
+ # Update apply button state and tooltip
+ applyErrorMessage = ""
+ if not self.inputVolumeNode():
+ applyErrorMessage = "Please select Input Vector Volume"
+ elif not self.outputVolumeNode():
+ applyErrorMessage = "Please select Output Scalar Volume"
+ elif not self.parameterNode():
+ applyErrorMessage = "Please select Parameter set"
+ elif isMethodSingleComponent and (int(self._parameterNode.GetParameter("ComponentToExtract")) < 0):
+ applyErrorMessage = "Please select a component to extract"
+
+ self.applyButton.enabled = (not applyErrorMessage)
+ self.applyButton.toolTip = applyErrorMessage
+
+ self.conversionMethodWidget.componentsComboBox.visible = isMethodSingleComponent
+
+ if (self.inputVolumeNode() is not None) and isMethodSingleComponent:
+ imageComponents = self.inputVolumeNode().GetImageData().GetNumberOfScalarComponents()
+ wasBlocked = self.conversionMethodWidget.componentsComboBox.blockSignals(True)
+ if self.conversionMethodWidget.componentsComboBox.count != imageComponents:
+ self.conversionMethodWidget.componentsComboBox.clear()
+ for comp in range(imageComponents):
+ self.conversionMethodWidget.componentsComboBox.insertItem(comp, str(comp))
+ self.conversionMethodWidget.componentsComboBox.blockSignals(wasBlocked)
+
+ def updateGuiFromMRML(self, caller=None, event=None):
+ """
+ Query all the parameters in the parameterNode,
+ and update the GUI state accordingly if something has changed.
+ """
+
+ self.updateButtonStates()
+
+ if not self.parameterNode():
+ return
+
+ self.setInputVolumeNode(self._parameterNode.GetParameter("InputVectorVolume"))
+ self.setOutputVolumeNode(self._parameterNode.GetParameter("OutputScalarVolume"))
+ self.conversionMethodWidget.methodSelectorComboBox.setCurrentIndex(
+ self.conversionMethodWidget.methodSelectorComboBox.findData(
+ self._parameterNode.GetParameter("ConversionMethod")))
+ self.conversionMethodWidget.componentsComboBox.setCurrentIndex(
+ int(self._parameterNode.GetParameter("ComponentToExtract")))
- self.setInputVolumeNode(self._parameterNode.GetParameter("InputVectorVolume"))
- self.setOutputVolumeNode(self._parameterNode.GetParameter("OutputScalarVolume"))
- self.conversionMethodWidget.methodSelectorComboBox.setCurrentIndex(
- self.conversionMethodWidget.methodSelectorComboBox.findData(
- self._parameterNode.GetParameter("ConversionMethod")))
- self.conversionMethodWidget.componentsComboBox.setCurrentIndex(
- int(self._parameterNode.GetParameter("ComponentToExtract")))
+ def updateParameterNodeFromGui(self):
- def updateParameterNodeFromGui(self):
+ self.updateButtonStates()
- self.updateButtonStates()
+ if self._parameterNode is None:
+ return
- if self._parameterNode is None:
- return
+ with NodeModify(self._parameterNode):
+ self._parameterNode.SetParameter("InputVectorVolume", getNodeID(self.inputVolumeNode()))
+ self._parameterNode.SetParameter("OutputScalarVolume", getNodeID(self.outputVolumeNode()))
+ self._parameterNode.SetParameter("ConversionMethod", self.conversionMethodWidget.conversionMethod())
+ self._parameterNode.SetParameter("ComponentToExtract", str(self.conversionMethodWidget.componentToExtract()))
- with NodeModify(self._parameterNode):
- self._parameterNode.SetParameter("InputVectorVolume", getNodeID(self.inputVolumeNode()))
- self._parameterNode.SetParameter("OutputScalarVolume", getNodeID(self.outputVolumeNode()))
- self._parameterNode.SetParameter("ConversionMethod", self.conversionMethodWidget.conversionMethod())
- self._parameterNode.SetParameter("ComponentToExtract", str(self.conversionMethodWidget.componentToExtract()))
+ def onApply(self):
- def onApply(self):
+ with MyScopedQtPropertySetter(self.applyButton, {"enabled": False, "text": "Working..."}):
+ success = self.logic.run(self._parameterNode)
- with MyScopedQtPropertySetter(self.applyButton, {"enabled": False, "text": "Working..."}):
- success = self.logic.run(self._parameterNode)
-
- # make the output volume appear in all the slice views
- if success:
- selectionNode = slicer.app.applicationLogic().GetSelectionNode()
- selectionNode.SetActiveVolumeID(self.outputVolumeNode().GetID())
- slicer.app.applicationLogic().PropagateVolumeSelection(0)
+ # make the output volume appear in all the slice views
+ if success:
+ selectionNode = slicer.app.applicationLogic().GetSelectionNode()
+ selectionNode.SetActiveVolumeID(self.outputVolumeNode().GetID())
+ slicer.app.applicationLogic().PropagateVolumeSelection(0)
#
@@ -295,42 +295,42 @@ def onApply(self):
#
class VectorToScalarVolumeConversionMethodWidget(qt.QWidget):
- """
- Widget to interact with conversion parameters only.
- It is separated from VectorToScalarVolumeWidget to enable GUI reusability in other modules.
- """
+ """
+ Widget to interact with conversion parameters only.
+ It is separated from VectorToScalarVolumeWidget to enable GUI reusability in other modules.
+ """
- def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent)
- self.setup()
+ def __init__(self, parent=None):
+ qt.QWidget.__init__(self, parent)
+ self.setup()
- def setup(self):
- self.methodLayout = qt.QHBoxLayout(self)
- self.methodLayout.setContentsMargins(0, 0, 0, 0)
- self.methodSelectorComboBox = qt.QComboBox()
+ def setup(self):
+ self.methodLayout = qt.QHBoxLayout(self)
+ self.methodLayout.setContentsMargins(0, 0, 0, 0)
+ self.methodSelectorComboBox = qt.QComboBox()
- self.methodSelectorComboBox.addItem("Luminance", VectorToScalarVolumeLogic.LUMINANCE)
- self.methodSelectorComboBox.setItemData(0, '(RGB,RGBA) Luminance from first three components: 0.30*R + 0.59*G + 0.11*B + 0.0*A)', qt.Qt.ToolTipRole)
- self.methodSelectorComboBox.addItem("Average", VectorToScalarVolumeLogic.AVERAGE)
- self.methodSelectorComboBox.setItemData(1, 'Average all the components.', qt.Qt.ToolTipRole)
- self.methodSelectorComboBox.addItem("Single Component Extraction", VectorToScalarVolumeLogic.SINGLE_COMPONENT)
- self.methodSelectorComboBox.setItemData(2, 'Extract single component', qt.Qt.ToolTipRole)
+ self.methodSelectorComboBox.addItem("Luminance", VectorToScalarVolumeLogic.LUMINANCE)
+ self.methodSelectorComboBox.setItemData(0, '(RGB,RGBA) Luminance from first three components: 0.30*R + 0.59*G + 0.11*B + 0.0*A)', qt.Qt.ToolTipRole)
+ self.methodSelectorComboBox.addItem("Average", VectorToScalarVolumeLogic.AVERAGE)
+ self.methodSelectorComboBox.setItemData(1, 'Average all the components.', qt.Qt.ToolTipRole)
+ self.methodSelectorComboBox.addItem("Single Component Extraction", VectorToScalarVolumeLogic.SINGLE_COMPONENT)
+ self.methodSelectorComboBox.setItemData(2, 'Extract single component', qt.Qt.ToolTipRole)
- self.methodLayout.addWidget(self.methodSelectorComboBox)
+ self.methodLayout.addWidget(self.methodSelectorComboBox)
- # ComponentToExtract
- singleComponentLayout = qt.QHBoxLayout()
- self.componentsComboBox = qt.QComboBox()
- singleComponentLayout.addWidget(self.componentsComboBox)
- self.methodLayout.addLayout(singleComponentLayout)
+ # ComponentToExtract
+ singleComponentLayout = qt.QHBoxLayout()
+ self.componentsComboBox = qt.QComboBox()
+ singleComponentLayout.addWidget(self.componentsComboBox)
+ self.methodLayout.addLayout(singleComponentLayout)
- def componentToExtract(self):
- " returns current index. -1 is invalid or disabled combo box"
- return self.componentsComboBox.currentIndex
+ def componentToExtract(self):
+ " returns current index. -1 is invalid or disabled combo box"
+ return self.componentsComboBox.currentIndex
- def conversionMethod(self):
- " returns data (str)"
- return self.methodSelectorComboBox.currentData
+ def conversionMethod(self):
+ " returns data (str)"
+ return self.methodSelectorComboBox.currentData
#
@@ -338,184 +338,184 @@ def conversionMethod(self):
#
class VectorToScalarVolumeLogic(ScriptedLoadableModuleLogic):
- """
- Implement the logic to compute the transform from vector to scalar.
- It is stateless, with the run function getting inputs and setting outputs.
- """
-
- LUMINANCE = 'LUMINANCE'
- AVERAGE = 'AVERAGE'
- SINGLE_COMPONENT = 'SINGLE_COMPONENT'
- EXTRACT_COMPONENT_NONE = -1
-
- CONVERSION_METHODS = (LUMINANCE, AVERAGE, SINGLE_COMPONENT)
-
- def __init__(self, parent=None):
- ScriptedLoadableModuleLogic.__init__(self, parent)
-
- def createParameterNode(self):
- """ Override base class method to provide default parameters. """
- node = ScriptedLoadableModuleLogic.createParameterNode(self)
- node.SetParameter("ConversionMethod", self.LUMINANCE)
- node.SetParameter("ComponentToExtract", str(self.EXTRACT_COMPONENT_NONE))
- return node
-
- @staticmethod
- def isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract):
- """
- Validate parameters using the parameterNode.
- Returns: (bool:isValid, string:errorMessage)
"""
- #
- # Checking input/output consistency.
- #
- if not inputVolumeNode:
- msg = 'no input volume node defined'
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
- if not outputVolumeNode:
- msg = 'no output volume node defined'
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
- if inputVolumeNode.GetID() == outputVolumeNode.GetID():
- msg = 'input and output volume is the same. ' \
- 'Create a new volume for output to avoid this error.'
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
-
- #
- # Checking based on method selected
- #
- if conversionMethod not in (VectorToScalarVolumeLogic.SINGLE_COMPONENT,
- VectorToScalarVolumeLogic.LUMINANCE,
- VectorToScalarVolumeLogic.AVERAGE):
- msg = 'conversionMethod %s unrecognized.' % conversionMethod
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
-
- inputImage = inputVolumeNode.GetImageData()
- numberOfComponents = inputImage.GetNumberOfScalarComponents()
-
- # SINGLE_COMPONENT: Check that input has enough components for the given componentToExtract
- if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT:
- # componentToExtract is an index with valid values in the range: [0, numberOfComponents-1]
- if not(0 <= componentToExtract < numberOfComponents):
- msg = 'componentToExtract %d is invalid. Image has only %d components.' % (componentToExtract, numberOfComponents)
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
-
- # LUMINANCE: Check that input vector has at least three components.
- if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE:
- if numberOfComponents < 3:
- msg = 'input has only %d components but requires ' \
- 'at least 3 components for luminance conversion.' % numberOfComponents
- logging.debug("isValidInputOutputData failed: %s" % msg)
- return False, msg
-
- return True, None
-
- def run(self, parameterNode):
+ Implement the logic to compute the transform from vector to scalar.
+ It is stateless, with the run function getting inputs and setting outputs.
"""
- Run the conversion with given parameterNode.
- """
- if parameterNode is None:
- slicer.util.errorDisplay('Invalid Parameter Node: None')
- return False
-
- inputVolumeNode = getNode(parameterNode.GetParameter("InputVectorVolume"))
- outputVolumeNode = getNode(parameterNode.GetParameter("OutputScalarVolume"))
- conversionMethod = parameterNode.GetParameter("ConversionMethod")
- componentToExtract = parameterNode.GetParameter("ComponentToExtract")
- if componentToExtract == '':
- componentToExtract = str(self.EXTRACT_COMPONENT_NONE)
- componentToExtract = int(componentToExtract)
-
- valid, msg = self.isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract)
- if not valid:
- slicer.util.errorDisplay(msg)
- return False
-
- logging.debug('Conversion mode is %s' % conversionMethod)
- logging.debug('ComponentToExtract is %s' % componentToExtract)
-
- if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT:
- self.runConversionMethodSingleComponent(inputVolumeNode, outputVolumeNode, componentToExtract)
-
- if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE:
- self.runConversionMethodLuminance(inputVolumeNode, outputVolumeNode)
-
- if conversionMethod == VectorToScalarVolumeLogic.AVERAGE:
- self.runConversionMethodAverage(inputVolumeNode, outputVolumeNode)
-
- return True
-
- def runWithVariables(self, inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract):
- """ Convenience method to run with variables, it creates a new parameterNode with these values. """
-
- parameterNode = self.getParameterNode()
- parameterNode.SetParameter("InputVectorVolume", getNodeID(inputVolumeNode))
- parameterNode.SetParameter("OutputScalarVolume", getNodeID(outputVolumeNode))
- parameterNode.SetParameter("ConversionMethod", conversionMethod)
- parameterNode.SetParameter("ComponentToExtract", str(componentToExtract))
- return self.run(parameterNode)
-
- def runConversionMethodSingleComponent(self, inputVolumeNode, outputVolumeNode, componentToExtract):
- ijkToRAS = vtk.vtkMatrix4x4()
- inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
- outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
-
- extract = vtk.vtkImageExtractComponents()
- extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
- extract.SetComponents(componentToExtract)
- extract.Update()
- outputVolumeNode.SetImageDataConnection(extract.GetOutputPort())
-
- def runConversionMethodLuminance(self, inputVolumeNode, outputVolumeNode):
- ijkToRAS = vtk.vtkMatrix4x4()
- inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
- outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
-
- extract = vtk.vtkImageExtractComponents()
- extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
- extract.SetComponents(0, 1, 2)
- luminance = vtk.vtkImageLuminance()
- luminance.SetInputConnection(extract.GetOutputPort())
- luminance.Update()
- outputVolumeNode.SetImageDataConnection(luminance.GetOutputPort())
-
- def runConversionMethodAverage(self, inputVolumeNode, outputVolumeNode):
- ijkToRAS = vtk.vtkMatrix4x4()
- inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
- outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
-
- numberOfComponents = inputVolumeNode.GetImageData().GetNumberOfScalarComponents()
- weightedSum = vtk.vtkImageWeightedSum()
- weights = vtk.vtkDoubleArray()
- weights.SetNumberOfValues(numberOfComponents)
- # TODO: Average could be extended to let the user choose the weights of the components.
- evenWeight = 1.0 / numberOfComponents
- logging.debug("ImageWeightedSum: weight value for all components: %s" % evenWeight)
- for comp in range(numberOfComponents):
- weights.SetValue(comp, evenWeight)
- weightedSum.SetWeights(weights)
-
- for comp in range(numberOfComponents):
- extract = vtk.vtkImageExtractComponents()
- extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
- extract.SetComponents(comp)
- extract.Update()
- # Cast component to Double
- compToDouble = vtk.vtkImageCast()
- compToDouble.SetInputConnection(0, extract.GetOutputPort())
- compToDouble.SetOutputScalarTypeToDouble()
- # Add to the weighted sum
- weightedSum.AddInputConnection(compToDouble.GetOutputPort())
-
- logging.debug("TotalInputConnections in weightedSum: %s" % weightedSum.GetTotalNumberOfInputConnections())
- weightedSum.SetNormalizeByWeight(False) # It is already normalized in the evenWeight case.
- weightedSum.Update()
- # Cast back to the type of the InputVolume, for consistency with other ConversionMethods
- castBack = vtk.vtkImageCast()
- castBack.SetInputConnection(0, weightedSum.GetOutputPort())
- castBack.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType())
- outputVolumeNode.SetImageDataConnection(castBack.GetOutputPort())
+
+ LUMINANCE = 'LUMINANCE'
+ AVERAGE = 'AVERAGE'
+ SINGLE_COMPONENT = 'SINGLE_COMPONENT'
+ EXTRACT_COMPONENT_NONE = -1
+
+ CONVERSION_METHODS = (LUMINANCE, AVERAGE, SINGLE_COMPONENT)
+
+ def __init__(self, parent=None):
+ ScriptedLoadableModuleLogic.__init__(self, parent)
+
+ def createParameterNode(self):
+ """ Override base class method to provide default parameters. """
+ node = ScriptedLoadableModuleLogic.createParameterNode(self)
+ node.SetParameter("ConversionMethod", self.LUMINANCE)
+ node.SetParameter("ComponentToExtract", str(self.EXTRACT_COMPONENT_NONE))
+ return node
+
+ @staticmethod
+ def isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract):
+ """
+ Validate parameters using the parameterNode.
+ Returns: (bool:isValid, string:errorMessage)
+ """
+ #
+ # Checking input/output consistency.
+ #
+ if not inputVolumeNode:
+ msg = 'no input volume node defined'
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+ if not outputVolumeNode:
+ msg = 'no output volume node defined'
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+ if inputVolumeNode.GetID() == outputVolumeNode.GetID():
+ msg = 'input and output volume is the same. ' \
+ 'Create a new volume for output to avoid this error.'
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+
+ #
+ # Checking based on method selected
+ #
+ if conversionMethod not in (VectorToScalarVolumeLogic.SINGLE_COMPONENT,
+ VectorToScalarVolumeLogic.LUMINANCE,
+ VectorToScalarVolumeLogic.AVERAGE):
+ msg = 'conversionMethod %s unrecognized.' % conversionMethod
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+
+ inputImage = inputVolumeNode.GetImageData()
+ numberOfComponents = inputImage.GetNumberOfScalarComponents()
+
+ # SINGLE_COMPONENT: Check that input has enough components for the given componentToExtract
+ if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT:
+ # componentToExtract is an index with valid values in the range: [0, numberOfComponents-1]
+ if not(0 <= componentToExtract < numberOfComponents):
+ msg = 'componentToExtract %d is invalid. Image has only %d components.' % (componentToExtract, numberOfComponents)
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+
+ # LUMINANCE: Check that input vector has at least three components.
+ if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE:
+ if numberOfComponents < 3:
+ msg = 'input has only %d components but requires ' \
+ 'at least 3 components for luminance conversion.' % numberOfComponents
+ logging.debug("isValidInputOutputData failed: %s" % msg)
+ return False, msg
+
+ return True, None
+
+ def run(self, parameterNode):
+ """
+ Run the conversion with given parameterNode.
+ """
+ if parameterNode is None:
+ slicer.util.errorDisplay('Invalid Parameter Node: None')
+ return False
+
+ inputVolumeNode = getNode(parameterNode.GetParameter("InputVectorVolume"))
+ outputVolumeNode = getNode(parameterNode.GetParameter("OutputScalarVolume"))
+ conversionMethod = parameterNode.GetParameter("ConversionMethod")
+ componentToExtract = parameterNode.GetParameter("ComponentToExtract")
+ if componentToExtract == '':
+ componentToExtract = str(self.EXTRACT_COMPONENT_NONE)
+ componentToExtract = int(componentToExtract)
+
+ valid, msg = self.isValidInputOutputData(inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract)
+ if not valid:
+ slicer.util.errorDisplay(msg)
+ return False
+
+ logging.debug('Conversion mode is %s' % conversionMethod)
+ logging.debug('ComponentToExtract is %s' % componentToExtract)
+
+ if conversionMethod == VectorToScalarVolumeLogic.SINGLE_COMPONENT:
+ self.runConversionMethodSingleComponent(inputVolumeNode, outputVolumeNode, componentToExtract)
+
+ if conversionMethod == VectorToScalarVolumeLogic.LUMINANCE:
+ self.runConversionMethodLuminance(inputVolumeNode, outputVolumeNode)
+
+ if conversionMethod == VectorToScalarVolumeLogic.AVERAGE:
+ self.runConversionMethodAverage(inputVolumeNode, outputVolumeNode)
+
+ return True
+
+ def runWithVariables(self, inputVolumeNode, outputVolumeNode, conversionMethod, componentToExtract):
+ """ Convenience method to run with variables, it creates a new parameterNode with these values. """
+
+ parameterNode = self.getParameterNode()
+ parameterNode.SetParameter("InputVectorVolume", getNodeID(inputVolumeNode))
+ parameterNode.SetParameter("OutputScalarVolume", getNodeID(outputVolumeNode))
+ parameterNode.SetParameter("ConversionMethod", conversionMethod)
+ parameterNode.SetParameter("ComponentToExtract", str(componentToExtract))
+ return self.run(parameterNode)
+
+ def runConversionMethodSingleComponent(self, inputVolumeNode, outputVolumeNode, componentToExtract):
+ ijkToRAS = vtk.vtkMatrix4x4()
+ inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
+ outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
+
+ extract = vtk.vtkImageExtractComponents()
+ extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
+ extract.SetComponents(componentToExtract)
+ extract.Update()
+ outputVolumeNode.SetImageDataConnection(extract.GetOutputPort())
+
+ def runConversionMethodLuminance(self, inputVolumeNode, outputVolumeNode):
+ ijkToRAS = vtk.vtkMatrix4x4()
+ inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
+ outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
+
+ extract = vtk.vtkImageExtractComponents()
+ extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
+ extract.SetComponents(0, 1, 2)
+ luminance = vtk.vtkImageLuminance()
+ luminance.SetInputConnection(extract.GetOutputPort())
+ luminance.Update()
+ outputVolumeNode.SetImageDataConnection(luminance.GetOutputPort())
+
+ def runConversionMethodAverage(self, inputVolumeNode, outputVolumeNode):
+ ijkToRAS = vtk.vtkMatrix4x4()
+ inputVolumeNode.GetIJKToRASMatrix(ijkToRAS)
+ outputVolumeNode.SetIJKToRASMatrix(ijkToRAS)
+
+ numberOfComponents = inputVolumeNode.GetImageData().GetNumberOfScalarComponents()
+ weightedSum = vtk.vtkImageWeightedSum()
+ weights = vtk.vtkDoubleArray()
+ weights.SetNumberOfValues(numberOfComponents)
+ # TODO: Average could be extended to let the user choose the weights of the components.
+ evenWeight = 1.0 / numberOfComponents
+ logging.debug("ImageWeightedSum: weight value for all components: %s" % evenWeight)
+ for comp in range(numberOfComponents):
+ weights.SetValue(comp, evenWeight)
+ weightedSum.SetWeights(weights)
+
+ for comp in range(numberOfComponents):
+ extract = vtk.vtkImageExtractComponents()
+ extract.SetInputConnection(inputVolumeNode.GetImageDataConnection())
+ extract.SetComponents(comp)
+ extract.Update()
+ # Cast component to Double
+ compToDouble = vtk.vtkImageCast()
+ compToDouble.SetInputConnection(0, extract.GetOutputPort())
+ compToDouble.SetOutputScalarTypeToDouble()
+ # Add to the weighted sum
+ weightedSum.AddInputConnection(compToDouble.GetOutputPort())
+
+ logging.debug("TotalInputConnections in weightedSum: %s" % weightedSum.GetTotalNumberOfInputConnections())
+ weightedSum.SetNormalizeByWeight(False) # It is already normalized in the evenWeight case.
+ weightedSum.Update()
+ # Cast back to the type of the InputVolume, for consistency with other ConversionMethods
+ castBack = vtk.vtkImageCast()
+ castBack.SetInputConnection(0, weightedSum.GetOutputPort())
+ castBack.SetOutputScalarType(inputVolumeNode.GetImageData().GetScalarType())
+ outputVolumeNode.SetImageDataConnection(castBack.GetOutputPort())
diff --git a/Modules/Scripted/WebServer/WebServer.py b/Modules/Scripted/WebServer/WebServer.py
index c333db4ca55..597d975418d 100644
--- a/Modules/Scripted/WebServer/WebServer.py
+++ b/Modules/Scripted/WebServer/WebServer.py
@@ -17,15 +17,15 @@
class WebServer(ScriptedLoadableModule):
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- parent.title = "Web Server"
- parent.categories = ["Servers"]
- parent.dependencies = []
- parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab Queen's University)"]
- parent.helpText = """Provides an embedded web server for slicer that provides a web services API for interacting with slicer.
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ parent.title = "Web Server"
+ parent.categories = ["Servers"]
+ parent.dependencies = []
+ parent.contributors = ["Steve Pieper (Isomics)", "Andras Lasso (PerkLab Queen's University)"]
+ parent.helpText = """Provides an embedded web server for slicer that provides a web services API for interacting with slicer.
"""
- parent.acknowledgementText = """
+ parent.acknowledgementText = """
This work was partially funded by NIH grant 3P41RR013218.
"""
@@ -35,212 +35,212 @@ def __init__(self, parent):
class WebServerWidget(ScriptedLoadableModuleWidget):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent=None):
- ScriptedLoadableModuleWidget.__init__(self, parent)
- self.guiMessages = True
- self.consoleMessages = False
- # By default, no request handlers are created, we will add them in startServer
- self.logic = WebServerLogic(logMessage=self.logMessage, requestHandlers=[])
-
- def enter(self):
- pass
-
- def exit(self):
- pass
-
- def setup(self):
- ScriptedLoadableModuleWidget.setup(self)
-
- # start button
- self.startServerButton = qt.QPushButton("Start server")
- self.startServerButton.name = "StartWebServer"
- self.startServerButton.toolTip = "Start web server with the selected options."
- self.layout.addWidget(self.startServerButton)
-
- # stop button
- self.stopServerButton = qt.QPushButton("Stop server")
- self.stopServerButton.name = "StopWebServer"
- self.stopServerButton.toolTip = "Start web server with the selected options."
- self.layout.addWidget(self.stopServerButton)
-
- # open browser page
- self.localConnectionButton = qt.QPushButton("Open static pages in external browser")
- self.localConnectionButton.toolTip = "Open a connection to the server on the local machine with your system browser."
- self.layout.addWidget(self.localConnectionButton)
-
- # open slicer widget
- self.localQtConnectionButton = qt.QPushButton("Open static pages in internal browser")
- self.localQtConnectionButton.toolTip = "Open a connection with Qt to the server on the local machine."
- self.layout.addWidget(self.localQtConnectionButton)
-
- # log window
- self.log = qt.QTextEdit()
- self.log.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
- self.log.readOnly = True
- self.layout.addWidget(self.log)
- self.logMessage('Status: Idle\n')
-
- # clear log button
- self.clearLogButton = qt.QPushButton("Clear Log")
- self.clearLogButton.toolTip = "Clear the log window."
- self.layout.addWidget(self.clearLogButton)
-
- # TODO: warning dialog on first connect
- # TODO: config option for port
- # TODO: config option for optional plugins
- # TODO: config option for certfile (https)
-
- self.advancedCollapsibleButton = ctk.ctkCollapsibleButton()
- self.advancedCollapsibleButton.text = "Advanced"
- self.layout.addWidget(self.advancedCollapsibleButton)
- advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton)
- self.advancedCollapsibleButton.collapsed = True
-
- # handlers
-
- self.enableSlicerHandler = qt.QCheckBox()
- self.enableSlicerHandler.toolTip = "Enable remote control of Slicer application (stop server to change option)"
- advancedFormLayout.addRow('Slicer API: ', self.enableSlicerHandler)
-
- self.enableSlicerHandlerExec = qt.QCheckBox()
- self.enableSlicerHandlerExec.toolTip = "Enable execution of arbitrary Python command using Slicer API. It only has effect if Slicer API is enabled, too (stop server to change option)."
- advancedFormLayout.addRow('Slicer API exec: ', self.enableSlicerHandlerExec)
-
- self.enableDICOMHandler = qt.QCheckBox()
- self.enableDICOMHandler.toolTip = "Enable serving Slicer DICOM database content via DICOMweb (stop server to change option)"
- if hasattr(slicer.modules, "dicom"):
- advancedFormLayout.addRow('DICOMweb API: ', self.enableDICOMHandler)
-
- self.enableStaticPagesHandler = qt.QCheckBox()
- self.enableStaticPagesHandler.toolTip = "Enable serving static pages (stop server to change option)"
- advancedFormLayout.addRow('Static pages: ', self.enableStaticPagesHandler)
-
- # log to console
- self.logToConsole = qt.QCheckBox()
- self.logToConsole.toolTip = "Copy log messages to the python console and parent terminal (disable to improve performance)"
- advancedFormLayout.addRow('Log to Console: ', self.logToConsole)
-
- # log to GUI
- self.logToGUI = qt.QCheckBox()
- self.logToGUI.toolTip = "Copy log messages to the log widget (disable to improve performance)"
- advancedFormLayout.addRow('Log to GUI: ', self.logToGUI)
-
- # Initialize GUI
- self.updateGUIFromSettings()
- self.updateGUIFromLogic()
-
- # Connections
- self.startServerButton.connect('clicked(bool)', self.startServer)
- self.stopServerButton.connect('clicked(bool)', self.stopServer)
- self.enableSlicerHandler.connect('clicked()', self.updateHandlersFromGUI)
- self.enableSlicerHandlerExec.connect('clicked()', self.updateHandlersFromGUI)
- self.enableDICOMHandler.connect('clicked()', self.updateHandlersFromGUI)
- self.enableStaticPagesHandler.connect('clicked()', self.updateHandlersFromGUI)
- self.localConnectionButton.connect('clicked()', self.openLocalConnection)
- self.localQtConnectionButton.connect('clicked()', self.openQtLocalConnection)
- self.clearLogButton.connect('clicked()', self.log.clear)
- self.logToConsole.connect('clicked()', self.updateLoggingFromGUI)
- self.logToGUI.connect('clicked()', self.updateLoggingFromGUI)
-
- self.updateLoggingFromGUI()
-
- def startServer(self):
- self.logic.requestHandlers = []
- self.logic.addDefaultRequestHandlers(
- enableSlicer=self.enableSlicerHandler.checked,
- enableExec=self.enableSlicerHandlerExec.checked,
- enableDICOM=self.enableDICOMHandler.checked,
- enableStaticPages=self.enableStaticPagesHandler.checked)
- self.logic.start()
- self.updateGUIFromLogic()
-
- def stopServer(self):
- self.logic.stop()
- self.updateGUIFromLogic()
-
- def updateGUIFromSettings(self):
- self.logToConsole.checked = slicer.app.userSettings().value("WebServer/logToConsole", False)
- self.logToGUI.checked = slicer.app.userSettings().value("WebServer/logToGUI", True)
- self.enableSlicerHandler.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandler", True)
- self.enableSlicerHandlerExec.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandlerExec", False)
- if hasattr(slicer.modules, "dicom"):
- self.enableDICOMHandler.checked = slicer.app.userSettings().value("WebServer/enableDICOMHandler", True)
- else:
- self.enableDICOMHandler.checked = False
- self.enableStaticPagesHandler.checked = slicer.app.userSettings().value("WebServer/enableStaticPagesHandler", True)
-
- def updateGUIFromLogic(self):
- self.startServerButton.setEnabled(not self.logic.serverStarted)
- self.stopServerButton.setEnabled(self.logic.serverStarted)
-
- self.enableSlicerHandler.setEnabled(not self.logic.serverStarted)
- self.enableSlicerHandlerExec.setEnabled(not self.logic.serverStarted)
- self.enableDICOMHandler.setEnabled(not self.logic.serverStarted)
- self.enableStaticPagesHandler.setEnabled(not self.logic.serverStarted)
-
- def updateLoggingFromGUI(self):
- self.consoleMessages = self.logToConsole.checked
- self.guiMessages = self.logToGUI.checked
- slicer.app.userSettings().setValue("WebServer/logToConsole", self.logToConsole.checked)
- slicer.app.userSettings().setValue("WebServer/logToGUI", self.logToGUI.checked)
-
- def updateHandlersFromGUI(self):
- slicer.app.userSettings().setValue("WebServer/enableSlicerHandler", self.enableSlicerHandler.checked)
- slicer.app.userSettings().setValue("WebServer/enableSlicerHandlerExec", self.enableSlicerHandlerExec.checked)
- slicer.app.userSettings().setValue("WebServer/enableDICOMHandler", self.enableDICOMHandler.checked)
- slicer.app.userSettings().setValue("WebServer/enableStaticPagesHandler", self.enableStaticPagesHandler.checked)
-
- def openLocalConnection(self):
- qt.QDesktopServices.openUrl(qt.QUrl(f'http://localhost:{self.logic.port}'))
-
- def openQtLocalConnection(self):
- self.webWidget = slicer.qSlicerWebWidget()
- self.webWidget.url = f'http://localhost:{self.logic.port}'
- self.webWidget.show()
-
- def onReload(self):
- logging.debug("Reloading WebServer")
- slicer._webServerStarted = self.logic.serverStarted
- self.stopServer()
-
- packageName = 'WebServerLib'
- submoduleNames = ['SlicerRequestHandler', 'StaticPagesRequestHandler']
- if hasattr(slicer.modules, "dicom"):
- submoduleNames.append('DICOMRequestHandler')
- import imp
- f, filename, description = imp.find_module(packageName)
- package = imp.load_module(packageName, f, filename, description)
- for submoduleName in submoduleNames:
- f, filename, description = imp.find_module(submoduleName, package.__path__)
- try:
- imp.load_module(packageName + '.' + submoduleName, f, filename, description)
- finally:
- f.close()
-
- ScriptedLoadableModuleWidget.onReload(self)
-
- # Restart web server if it was running
- if slicer._webServerStarted:
- slicer.modules.WebServerWidget.startServer()
- del slicer._webServerStarted
-
- def logMessage(self, *args):
- if self.consoleMessages:
- for arg in args:
- print(arg)
- if self.guiMessages:
- if len(self.log.html) > 1024 * 256:
- self.log.clear()
- self.log.insertHtml("Log cleared\n")
- for arg in args:
- self.log.insertHtml(arg)
- self.log.insertPlainText('\n')
- self.log.ensureCursorVisible()
- self.log.repaint()
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent=None):
+ ScriptedLoadableModuleWidget.__init__(self, parent)
+ self.guiMessages = True
+ self.consoleMessages = False
+ # By default, no request handlers are created, we will add them in startServer
+ self.logic = WebServerLogic(logMessage=self.logMessage, requestHandlers=[])
+
+ def enter(self):
+ pass
+
+ def exit(self):
+ pass
+
+ def setup(self):
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # start button
+ self.startServerButton = qt.QPushButton("Start server")
+ self.startServerButton.name = "StartWebServer"
+ self.startServerButton.toolTip = "Start web server with the selected options."
+ self.layout.addWidget(self.startServerButton)
+
+ # stop button
+ self.stopServerButton = qt.QPushButton("Stop server")
+ self.stopServerButton.name = "StopWebServer"
+ self.stopServerButton.toolTip = "Start web server with the selected options."
+ self.layout.addWidget(self.stopServerButton)
+
+ # open browser page
+ self.localConnectionButton = qt.QPushButton("Open static pages in external browser")
+ self.localConnectionButton.toolTip = "Open a connection to the server on the local machine with your system browser."
+ self.layout.addWidget(self.localConnectionButton)
+
+ # open slicer widget
+ self.localQtConnectionButton = qt.QPushButton("Open static pages in internal browser")
+ self.localQtConnectionButton.toolTip = "Open a connection with Qt to the server on the local machine."
+ self.layout.addWidget(self.localQtConnectionButton)
+
+ # log window
+ self.log = qt.QTextEdit()
+ self.log.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ self.log.readOnly = True
+ self.layout.addWidget(self.log)
+ self.logMessage('
Status: Idle\n')
+
+ # clear log button
+ self.clearLogButton = qt.QPushButton("Clear Log")
+ self.clearLogButton.toolTip = "Clear the log window."
+ self.layout.addWidget(self.clearLogButton)
+
+ # TODO: warning dialog on first connect
+ # TODO: config option for port
+ # TODO: config option for optional plugins
+ # TODO: config option for certfile (https)
+
+ self.advancedCollapsibleButton = ctk.ctkCollapsibleButton()
+ self.advancedCollapsibleButton.text = "Advanced"
+ self.layout.addWidget(self.advancedCollapsibleButton)
+ advancedFormLayout = qt.QFormLayout(self.advancedCollapsibleButton)
+ self.advancedCollapsibleButton.collapsed = True
+
+ # handlers
+
+ self.enableSlicerHandler = qt.QCheckBox()
+ self.enableSlicerHandler.toolTip = "Enable remote control of Slicer application (stop server to change option)"
+ advancedFormLayout.addRow('Slicer API: ', self.enableSlicerHandler)
+
+ self.enableSlicerHandlerExec = qt.QCheckBox()
+ self.enableSlicerHandlerExec.toolTip = "Enable execution of arbitrary Python command using Slicer API. It only has effect if Slicer API is enabled, too (stop server to change option)."
+ advancedFormLayout.addRow('Slicer API exec: ', self.enableSlicerHandlerExec)
+
+ self.enableDICOMHandler = qt.QCheckBox()
+ self.enableDICOMHandler.toolTip = "Enable serving Slicer DICOM database content via DICOMweb (stop server to change option)"
+ if hasattr(slicer.modules, "dicom"):
+ advancedFormLayout.addRow('DICOMweb API: ', self.enableDICOMHandler)
+
+ self.enableStaticPagesHandler = qt.QCheckBox()
+ self.enableStaticPagesHandler.toolTip = "Enable serving static pages (stop server to change option)"
+ advancedFormLayout.addRow('Static pages: ', self.enableStaticPagesHandler)
+
+ # log to console
+ self.logToConsole = qt.QCheckBox()
+ self.logToConsole.toolTip = "Copy log messages to the python console and parent terminal (disable to improve performance)"
+ advancedFormLayout.addRow('Log to Console: ', self.logToConsole)
+
+ # log to GUI
+ self.logToGUI = qt.QCheckBox()
+ self.logToGUI.toolTip = "Copy log messages to the log widget (disable to improve performance)"
+ advancedFormLayout.addRow('Log to GUI: ', self.logToGUI)
+
+ # Initialize GUI
+ self.updateGUIFromSettings()
+ self.updateGUIFromLogic()
+
+ # Connections
+ self.startServerButton.connect('clicked(bool)', self.startServer)
+ self.stopServerButton.connect('clicked(bool)', self.stopServer)
+ self.enableSlicerHandler.connect('clicked()', self.updateHandlersFromGUI)
+ self.enableSlicerHandlerExec.connect('clicked()', self.updateHandlersFromGUI)
+ self.enableDICOMHandler.connect('clicked()', self.updateHandlersFromGUI)
+ self.enableStaticPagesHandler.connect('clicked()', self.updateHandlersFromGUI)
+ self.localConnectionButton.connect('clicked()', self.openLocalConnection)
+ self.localQtConnectionButton.connect('clicked()', self.openQtLocalConnection)
+ self.clearLogButton.connect('clicked()', self.log.clear)
+ self.logToConsole.connect('clicked()', self.updateLoggingFromGUI)
+ self.logToGUI.connect('clicked()', self.updateLoggingFromGUI)
+
+ self.updateLoggingFromGUI()
+
+ def startServer(self):
+ self.logic.requestHandlers = []
+ self.logic.addDefaultRequestHandlers(
+ enableSlicer=self.enableSlicerHandler.checked,
+ enableExec=self.enableSlicerHandlerExec.checked,
+ enableDICOM=self.enableDICOMHandler.checked,
+ enableStaticPages=self.enableStaticPagesHandler.checked)
+ self.logic.start()
+ self.updateGUIFromLogic()
+
+ def stopServer(self):
+ self.logic.stop()
+ self.updateGUIFromLogic()
+
+ def updateGUIFromSettings(self):
+ self.logToConsole.checked = slicer.app.userSettings().value("WebServer/logToConsole", False)
+ self.logToGUI.checked = slicer.app.userSettings().value("WebServer/logToGUI", True)
+ self.enableSlicerHandler.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandler", True)
+ self.enableSlicerHandlerExec.checked = slicer.app.userSettings().value("WebServer/enableSlicerHandlerExec", False)
+ if hasattr(slicer.modules, "dicom"):
+ self.enableDICOMHandler.checked = slicer.app.userSettings().value("WebServer/enableDICOMHandler", True)
+ else:
+ self.enableDICOMHandler.checked = False
+ self.enableStaticPagesHandler.checked = slicer.app.userSettings().value("WebServer/enableStaticPagesHandler", True)
+
+ def updateGUIFromLogic(self):
+ self.startServerButton.setEnabled(not self.logic.serverStarted)
+ self.stopServerButton.setEnabled(self.logic.serverStarted)
+
+ self.enableSlicerHandler.setEnabled(not self.logic.serverStarted)
+ self.enableSlicerHandlerExec.setEnabled(not self.logic.serverStarted)
+ self.enableDICOMHandler.setEnabled(not self.logic.serverStarted)
+ self.enableStaticPagesHandler.setEnabled(not self.logic.serverStarted)
+
+ def updateLoggingFromGUI(self):
+ self.consoleMessages = self.logToConsole.checked
+ self.guiMessages = self.logToGUI.checked
+ slicer.app.userSettings().setValue("WebServer/logToConsole", self.logToConsole.checked)
+ slicer.app.userSettings().setValue("WebServer/logToGUI", self.logToGUI.checked)
+
+ def updateHandlersFromGUI(self):
+ slicer.app.userSettings().setValue("WebServer/enableSlicerHandler", self.enableSlicerHandler.checked)
+ slicer.app.userSettings().setValue("WebServer/enableSlicerHandlerExec", self.enableSlicerHandlerExec.checked)
+ slicer.app.userSettings().setValue("WebServer/enableDICOMHandler", self.enableDICOMHandler.checked)
+ slicer.app.userSettings().setValue("WebServer/enableStaticPagesHandler", self.enableStaticPagesHandler.checked)
+
+ def openLocalConnection(self):
+ qt.QDesktopServices.openUrl(qt.QUrl(f'http://localhost:{self.logic.port}'))
+
+ def openQtLocalConnection(self):
+ self.webWidget = slicer.qSlicerWebWidget()
+ self.webWidget.url = f'http://localhost:{self.logic.port}'
+ self.webWidget.show()
+
+ def onReload(self):
+ logging.debug("Reloading WebServer")
+ slicer._webServerStarted = self.logic.serverStarted
+ self.stopServer()
+
+ packageName = 'WebServerLib'
+ submoduleNames = ['SlicerRequestHandler', 'StaticPagesRequestHandler']
+ if hasattr(slicer.modules, "dicom"):
+ submoduleNames.append('DICOMRequestHandler')
+ import imp
+ f, filename, description = imp.find_module(packageName)
+ package = imp.load_module(packageName, f, filename, description)
+ for submoduleName in submoduleNames:
+ f, filename, description = imp.find_module(submoduleName, package.__path__)
+ try:
+ imp.load_module(packageName + '.' + submoduleName, f, filename, description)
+ finally:
+ f.close()
+
+ ScriptedLoadableModuleWidget.onReload(self)
+
+ # Restart web server if it was running
+ if slicer._webServerStarted:
+ slicer.modules.WebServerWidget.startServer()
+ del slicer._webServerStarted
+
+ def logMessage(self, *args):
+ if self.consoleMessages:
+ for arg in args:
+ print(arg)
+ if self.guiMessages:
+ if len(self.log.html) > 1024 * 256:
+ self.log.clear()
+ self.log.insertHtml("Log cleared\n")
+ for arg in args:
+ self.log.insertHtml(arg)
+ self.log.insertPlainText('\n')
+ self.log.ensureCursorVisible()
+ self.log.repaint()
#
# SlicerHTTPServer
@@ -248,284 +248,284 @@ def logMessage(self, *args):
class SlicerHTTPServer(HTTPServer):
- """
- This web server is configured to integrate with the Qt main loop
- by listenting activity on the fileno of the servers socket.
- """
- # TODO: set header so client knows that image refreshes are needed (avoid
- # using the &time=xxx trick)
- def __init__(self, server_address=("", 2016), requestHandlers=None, docroot='.', logMessage=None, certfile=None):
- """
- :param server_address: passed to parent class (default ("", 8070))
- :param requestHandlers: request handler objects; if not specified then Slicer, DICOM, and StaticPages handlers are registered
- :param docroot: used to serve static pages content
- :param logMessage: a callable for messages
- :param certfile: path to a file with an ssl certificate (.pem file)
- """
- HTTPServer.__init__(self, server_address, SlicerHTTPServer.DummyRequestHandler)
-
- self.requestHandlers = []
-
- if requestHandlers is not None:
- for requestHandler in requestHandlers:
- self.requestHandlers.append(requestHandler)
-
- self.docroot = docroot
- self.timeout = 1.
- if certfile:
- # https://stackoverflow.com/questions/19705785/python-3-simple-https-server
- import ssl
- self.socket = ssl.wrap_socket(self.socket,
- server_side=True,
- certfile=certfile,
- ssl_version=ssl.PROTOCOL_TLS)
- self.socket.settimeout(5.)
- if logMessage:
- self.logMessage = logMessage
- self.notifiers = {}
- self.connections = {}
- self.requestCommunicators = {}
-
- class DummyRequestHandler(object):
- pass
-
- class SlicerRequestCommunicator(object):
"""
- Encapsulate elements for handling event driven read of request.
- An instance is created for each client connection to our web server.
- This class handles event driven chunking of the communication.
- .. note:: this is an internal class of the web server
+ This web server is configured to integrate with the Qt main loop
+ by listenting activity on the fileno of the servers socket.
"""
- def __init__(self, connectionSocket, requestHandlers, docroot, logMessage):
- """
- :param connectionSocket: socket for this request
- :param docroot: for handling static pages content
- :param logMessage: callable
- """
- self.connectionSocket = connectionSocket
- self.docroot = docroot
- self.logMessage = logMessage
- self.bufferSize = 1024 * 1024
- self.requestHandlers = []
- for requestHandler in requestHandlers:
- self.registerRequestHandler(requestHandler)
- self.expectedRequestSize = -1
- self.requestSoFar = b""
- fileno = self.connectionSocket.fileno()
- self.readNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Read)
- self.readNotifier.connect('activated(int)', self.onReadable)
- self.logMessage('Waiting on %d...' % fileno)
-
- def registerRequestHandler(self, handler):
- self.requestHandlers.append(handler)
- handler.logMessage = self.logMessage
-
- def onReadableComplete(self):
- self.logMessage("reading complete, freeing notifier")
- self.readNotifier = None
-
- def onReadable(self, fileno):
- self.logMessage('Reading...')
- requestHeader = b""
- requestBody = b""
- requestComplete = False
- requestPart = ""
- try:
- requestPart = self.connectionSocket.recv(self.bufferSize)
- self.logMessage('Just received... %d bytes in this part' % len(requestPart))
- self.requestSoFar += requestPart
- endOfHeader = self.requestSoFar.find(b'\r\n\r\n')
- if self.expectedRequestSize > 0:
- self.logMessage('received... %d of %d expected' % (len(self.requestSoFar), self.expectedRequestSize))
- if len(self.requestSoFar) >= self.expectedRequestSize:
- requestHeader = self.requestSoFar[:endOfHeader + 2]
- requestBody = self.requestSoFar[4 + endOfHeader:]
- requestComplete = True
- else:
- if endOfHeader != -1:
- self.logMessage('Looking for content in header...')
- contentLengthTag = self.requestSoFar.find(b'Content-Length:')
- if contentLengthTag != -1:
- tag = self.requestSoFar[contentLengthTag:]
- numberStartIndex = tag.find(b' ')
- numberEndIndex = tag.find(b'\r\n')
- contentLength = int(tag[numberStartIndex:numberEndIndex])
- self.expectedRequestSize = 4 + endOfHeader + contentLength
- self.logMessage('Expecting a body of %d, total size %d' % (contentLength, self.expectedRequestSize))
- if len(requestPart) == self.expectedRequestSize:
- requestHeader = requestPart[:endOfHeader + 2]
- requestBody = requestPart[4 + endOfHeader:]
+ # TODO: set header so client knows that image refreshes are needed (avoid
+ # using the &time=xxx trick)
+ def __init__(self, server_address=("", 2016), requestHandlers=None, docroot='.', logMessage=None, certfile=None):
+ """
+ :param server_address: passed to parent class (default ("", 8070))
+ :param requestHandlers: request handler objects; if not specified then Slicer, DICOM, and StaticPages handlers are registered
+ :param docroot: used to serve static pages content
+ :param logMessage: a callable for messages
+ :param certfile: path to a file with an ssl certificate (.pem file)
+ """
+ HTTPServer.__init__(self, server_address, SlicerHTTPServer.DummyRequestHandler)
+
+ self.requestHandlers = []
+
+ if requestHandlers is not None:
+ for requestHandler in requestHandlers:
+ self.requestHandlers.append(requestHandler)
+
+ self.docroot = docroot
+ self.timeout = 1.
+ if certfile:
+ # https://stackoverflow.com/questions/19705785/python-3-simple-https-server
+ import ssl
+ self.socket = ssl.wrap_socket(self.socket,
+ server_side=True,
+ certfile=certfile,
+ ssl_version=ssl.PROTOCOL_TLS)
+ self.socket.settimeout(5.)
+ if logMessage:
+ self.logMessage = logMessage
+ self.notifiers = {}
+ self.connections = {}
+ self.requestCommunicators = {}
+
+ class DummyRequestHandler(object):
+ pass
+
+ class SlicerRequestCommunicator(object):
+ """
+ Encapsulate elements for handling event driven read of request.
+ An instance is created for each client connection to our web server.
+ This class handles event driven chunking of the communication.
+ .. note:: this is an internal class of the web server
+ """
+ def __init__(self, connectionSocket, requestHandlers, docroot, logMessage):
+ """
+ :param connectionSocket: socket for this request
+ :param docroot: for handling static pages content
+ :param logMessage: callable
+ """
+ self.connectionSocket = connectionSocket
+ self.docroot = docroot
+ self.logMessage = logMessage
+ self.bufferSize = 1024 * 1024
+ self.requestHandlers = []
+ for requestHandler in requestHandlers:
+ self.registerRequestHandler(requestHandler)
+ self.expectedRequestSize = -1
+ self.requestSoFar = b""
+ fileno = self.connectionSocket.fileno()
+ self.readNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Read)
+ self.readNotifier.connect('activated(int)', self.onReadable)
+ self.logMessage('Waiting on %d...' % fileno)
+
+ def registerRequestHandler(self, handler):
+ self.requestHandlers.append(handler)
+ handler.logMessage = self.logMessage
+
+ def onReadableComplete(self):
+ self.logMessage("reading complete, freeing notifier")
+ self.readNotifier = None
+
+ def onReadable(self, fileno):
+ self.logMessage('Reading...')
+ requestHeader = b""
+ requestBody = b""
+ requestComplete = False
+ requestPart = ""
+ try:
+ requestPart = self.connectionSocket.recv(self.bufferSize)
+ self.logMessage('Just received... %d bytes in this part' % len(requestPart))
+ self.requestSoFar += requestPart
+ endOfHeader = self.requestSoFar.find(b'\r\n\r\n')
+ if self.expectedRequestSize > 0:
+ self.logMessage('received... %d of %d expected' % (len(self.requestSoFar), self.expectedRequestSize))
+ if len(self.requestSoFar) >= self.expectedRequestSize:
+ requestHeader = self.requestSoFar[:endOfHeader + 2]
+ requestBody = self.requestSoFar[4 + endOfHeader:]
+ requestComplete = True
+ else:
+ if endOfHeader != -1:
+ self.logMessage('Looking for content in header...')
+ contentLengthTag = self.requestSoFar.find(b'Content-Length:')
+ if contentLengthTag != -1:
+ tag = self.requestSoFar[contentLengthTag:]
+ numberStartIndex = tag.find(b' ')
+ numberEndIndex = tag.find(b'\r\n')
+ contentLength = int(tag[numberStartIndex:numberEndIndex])
+ self.expectedRequestSize = 4 + endOfHeader + contentLength
+ self.logMessage('Expecting a body of %d, total size %d' % (contentLength, self.expectedRequestSize))
+ if len(requestPart) == self.expectedRequestSize:
+ requestHeader = requestPart[:endOfHeader + 2]
+ requestBody = requestPart[4 + endOfHeader:]
+ requestComplete = True
+ else:
+ self.logMessage('Found end of header with no content, so body is empty')
+ requestHeader = self.requestSoFar[:-2]
+ requestComplete = True
+ except socket.error as e:
+ print('Socket error: ', e)
+ print('So far:\n', self.requestSoFar)
requestComplete = True
- else:
- self.logMessage('Found end of header with no content, so body is empty')
- requestHeader = self.requestSoFar[:-2]
- requestComplete = True
- except socket.error as e:
- print('Socket error: ', e)
- print('So far:\n', self.requestSoFar)
- requestComplete = True
-
- if len(requestPart) == 0 or requestComplete:
- self.logMessage('Got complete message of header size %d, body size %d' % (len(requestHeader), len(requestBody)))
- self.readNotifier.disconnect('activated(int)', self.onReadable)
- self.readNotifier.setEnabled(False)
- qt.QTimer.singleShot(0, self.onReadableComplete)
-
- if len(self.requestSoFar) == 0:
- self.logMessage("Ignoring empty request")
- return
-
- method, uri, version = [b'GET', b'/', b'HTTP/1.1'] # defaults
- requestLines = requestHeader.split(b'\r\n')
- self.logMessage(requestLines[0])
+
+ if len(requestPart) == 0 or requestComplete:
+ self.logMessage('Got complete message of header size %d, body size %d' % (len(requestHeader), len(requestBody)))
+ self.readNotifier.disconnect('activated(int)', self.onReadable)
+ self.readNotifier.setEnabled(False)
+ qt.QTimer.singleShot(0, self.onReadableComplete)
+
+ if len(self.requestSoFar) == 0:
+ self.logMessage("Ignoring empty request")
+ return
+
+ method, uri, version = [b'GET', b'/', b'HTTP/1.1'] # defaults
+ requestLines = requestHeader.split(b'\r\n')
+ self.logMessage(requestLines[0])
+ try:
+ method, uri, version = requestLines[0].split(b' ')
+ except ValueError as e:
+ self.logMessage("Could not interpret first request lines: ", requestLines)
+
+ if requestLines == "":
+ self.logMessage("Assuming empty string is HTTP/1.1 GET of /.")
+
+ if version != b"HTTP/1.1":
+ self.logMessage("Warning, we don't speak %s", version)
+ return
+
+ # TODO: methods = ["GET", "POST", "PUT", "DELETE"]
+ methods = [b"GET", b"POST", b"PUT"]
+ if not method in methods:
+ self.logMessage("Warning, we only handle %s" % methods)
+ return
+
+ parsedURL = urllib.parse.urlparse(uri)
+ request = parsedURL.path
+ if parsedURL.query != b"":
+ request += b'?' + parsedURL.query
+ self.logMessage('Parsing url request: ', parsedURL)
+ self.logMessage(' request is: %s' % request)
+
+ highestConfidenceHandler = None
+ highestConfidence = 0.0
+ for handler in self.requestHandlers:
+ confidence = handler.canHandleRequest(uri, requestBody)
+ if confidence > highestConfidence:
+ highestConfidenceHandler = handler
+ highestConfidence = confidence
+
+ if highestConfidenceHandler is not None and highestConfidence > 0.0:
+ try:
+ contentType, responseBody = highestConfidenceHandler.handleRequest(uri, requestBody)
+ except:
+ etype, value, tb = sys.exc_info()
+ import traceback
+ for frame in traceback.format_tb(tb):
+ self.logMessage(frame)
+ self.logMessage(etype, value)
+ contentType = b'text/plain'
+ responseBody = b'Server error' # TODO: send correct error code in response
+ else:
+ contentType = b'text/plain'
+ responseBody = b''
+
+ if responseBody:
+ self.response = b"HTTP/1.1 200 OK\r\n"
+ self.response += b"Access-Control-Allow-Origin: *\r\n"
+ self.response += b"Content-Type: %s\r\n" % contentType
+ self.response += b"Content-Length: %d\r\n" % len(responseBody)
+ self.response += b"Cache-Control: no-cache\r\n"
+ self.response += b"\r\n"
+ self.response += responseBody
+ else:
+ self.response = b"HTTP/1.1 404 Not Found\r\n"
+ self.response += b"\r\n"
+
+ self.toSend = len(self.response)
+ self.sentSoFar = 0
+ fileno = self.connectionSocket.fileno()
+ self.writeNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Write)
+ self.writeNotifier.connect('activated(int)', self.onWritable)
+
+ def onWriteableComplete(self):
+ self.logMessage("writing complete, freeing notifier")
+ self.writeNotifier = None
+ self.connectionSocket = None
+
+ def onWritable(self, fileno):
+ self.logMessage('Sending on %d...' % (fileno))
+ sendError = False
+ try:
+ sent = self.connectionSocket.send(self.response[:500 * self.bufferSize])
+ self.response = self.response[sent:]
+ self.sentSoFar += sent
+ self.logMessage('sent: %d (%d of %d, %f%%)' % (sent, self.sentSoFar, self.toSend, 100. * self.sentSoFar / self.toSend))
+ except socket.error as e:
+ self.logMessage('Socket error while sending: %s' % e)
+ sendError = True
+
+ if self.sentSoFar >= self.toSend or sendError:
+ self.writeNotifier.disconnect('activated(int)', self.onWritable)
+ self.writeNotifier.setEnabled(False)
+ qt.QTimer.singleShot(0, self.onWriteableComplete)
+ self.connectionSocket.close()
+ self.logMessage('closed fileno %d' % (fileno))
+
+ def onServerSocketNotify(self, fileno):
+ self.logMessage('got request on %d' % fileno)
try:
- method, uri, version = requestLines[0].split(b' ')
- except ValueError as e:
- self.logMessage("Could not interpret first request lines: ", requestLines)
-
- if requestLines == "":
- self.logMessage("Assuming empty string is HTTP/1.1 GET of /.")
-
- if version != b"HTTP/1.1":
- self.logMessage("Warning, we don't speak %s", version)
- return
-
- # TODO: methods = ["GET", "POST", "PUT", "DELETE"]
- methods = [b"GET", b"POST", b"PUT"]
- if not method in methods:
- self.logMessage("Warning, we only handle %s" % methods)
- return
-
- parsedURL = urllib.parse.urlparse(uri)
- request = parsedURL.path
- if parsedURL.query != b"":
- request += b'?' + parsedURL.query
- self.logMessage('Parsing url request: ', parsedURL)
- self.logMessage(' request is: %s' % request)
-
- highestConfidenceHandler = None
- highestConfidence = 0.0
- for handler in self.requestHandlers:
- confidence = handler.canHandleRequest(uri, requestBody)
- if confidence > highestConfidence:
- highestConfidenceHandler = handler
- highestConfidence = confidence
-
- if highestConfidenceHandler is not None and highestConfidence > 0.0:
- try:
- contentType, responseBody = highestConfidenceHandler.handleRequest(uri, requestBody)
- except:
- etype, value, tb = sys.exc_info()
- import traceback
- for frame in traceback.format_tb(tb):
- self.logMessage(frame)
- self.logMessage(etype, value)
- contentType = b'text/plain'
- responseBody = b'Server error' # TODO: send correct error code in response
- else:
- contentType = b'text/plain'
- responseBody = b''
-
- if responseBody:
- self.response = b"HTTP/1.1 200 OK\r\n"
- self.response += b"Access-Control-Allow-Origin: *\r\n"
- self.response += b"Content-Type: %s\r\n" % contentType
- self.response += b"Content-Length: %d\r\n" % len(responseBody)
- self.response += b"Cache-Control: no-cache\r\n"
- self.response += b"\r\n"
- self.response += responseBody
- else:
- self.response = b"HTTP/1.1 404 Not Found\r\n"
- self.response += b"\r\n"
-
- self.toSend = len(self.response)
- self.sentSoFar = 0
- fileno = self.connectionSocket.fileno()
- self.writeNotifier = qt.QSocketNotifier(fileno, qt.QSocketNotifier.Write)
- self.writeNotifier.connect('activated(int)', self.onWritable)
-
- def onWriteableComplete(self):
- self.logMessage("writing complete, freeing notifier")
- self.writeNotifier = None
- self.connectionSocket = None
-
- def onWritable(self, fileno):
- self.logMessage('Sending on %d...' % (fileno))
- sendError = False
- try:
- sent = self.connectionSocket.send(self.response[:500 * self.bufferSize])
- self.response = self.response[sent:]
- self.sentSoFar += sent
- self.logMessage('sent: %d (%d of %d, %f%%)' % (sent, self.sentSoFar, self.toSend, 100. * self.sentSoFar / self.toSend))
- except socket.error as e:
- self.logMessage('Socket error while sending: %s' % e)
- sendError = True
-
- if self.sentSoFar >= self.toSend or sendError:
- self.writeNotifier.disconnect('activated(int)', self.onWritable)
- self.writeNotifier.setEnabled(False)
- qt.QTimer.singleShot(0, self.onWriteableComplete)
- self.connectionSocket.close()
- self.logMessage('closed fileno %d' % (fileno))
-
- def onServerSocketNotify(self, fileno):
- self.logMessage('got request on %d' % fileno)
- try:
- (connectionSocket, clientAddress) = self.socket.accept()
- fileno = connectionSocket.fileno()
- self.requestCommunicators[fileno] = self.SlicerRequestCommunicator(connectionSocket, self.requestHandlers, self.docroot, self.logMessage)
- self.logMessage('Connected on %s fileno %d' % (connectionSocket, connectionSocket.fileno()))
- except socket.error as e:
- self.logMessage('Socket Error', socket.error, e)
-
- def start(self):
- """start the server
- Uses one thread since we are event driven
- """
- try:
- self.logMessage('started httpserver...')
- self.notifier = qt.QSocketNotifier(self.socket.fileno(), qt.QSocketNotifier.Read)
- self.logMessage('listening on %d...' % self.socket.fileno())
- self.notifier.connect('activated(int)', self.onServerSocketNotify)
-
- except KeyboardInterrupt:
- self.logMessage('KeyboardInterrupt - stopping')
- self.stop()
-
- def stop(self):
- self.socket.close()
- if self.notifier:
- self.notifier.disconnect('activated(int)', self.onServerSocketNotify)
- self.notifier = None
-
- def handle_error(self, request, client_address):
- """Handle an error gracefully. May be overridden.
-
- The default is to print a traceback and continue.
- """
- print('-' * 40)
- print('Exception happened during processing of request', request)
- print('From', client_address)
- import traceback
- traceback.print_exc() # XXX But this goes to stderr!
- print('-' * 40)
-
- @classmethod
- def findFreePort(self, port=2016):
- """returns a port that is not apparently in use"""
- portFree = False
- while not portFree:
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- s.bind(("", port))
- except socket.error as e:
+ (connectionSocket, clientAddress) = self.socket.accept()
+ fileno = connectionSocket.fileno()
+ self.requestCommunicators[fileno] = self.SlicerRequestCommunicator(connectionSocket, self.requestHandlers, self.docroot, self.logMessage)
+ self.logMessage('Connected on %s fileno %d' % (connectionSocket, connectionSocket.fileno()))
+ except socket.error as e:
+ self.logMessage('Socket Error', socket.error, e)
+
+ def start(self):
+ """start the server
+ Uses one thread since we are event driven
+ """
+ try:
+ self.logMessage('started httpserver...')
+ self.notifier = qt.QSocketNotifier(self.socket.fileno(), qt.QSocketNotifier.Read)
+ self.logMessage('listening on %d...' % self.socket.fileno())
+ self.notifier.connect('activated(int)', self.onServerSocketNotify)
+
+ except KeyboardInterrupt:
+ self.logMessage('KeyboardInterrupt - stopping')
+ self.stop()
+
+ def stop(self):
+ self.socket.close()
+ if self.notifier:
+ self.notifier.disconnect('activated(int)', self.onServerSocketNotify)
+ self.notifier = None
+
+ def handle_error(self, request, client_address):
+ """Handle an error gracefully. May be overridden.
+
+ The default is to print a traceback and continue.
+ """
+ print('-' * 40)
+ print('Exception happened during processing of request', request)
+ print('From', client_address)
+ import traceback
+ traceback.print_exc() # XXX But this goes to stderr!
+ print('-' * 40)
+
+ @classmethod
+ def findFreePort(self, port=2016):
+ """returns a port that is not apparently in use"""
portFree = False
- port += 1
- finally:
- s.close()
- portFree = True
- return port
+ while not portFree:
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(("", port))
+ except socket.error as e:
+ portFree = False
+ port += 1
+ finally:
+ s.close()
+ portFree = True
+ return port
#
@@ -533,69 +533,69 @@ def findFreePort(self, port=2016):
#
class WebServerLogic:
- """Include a concrete subclass of SimpleHTTPServer
- that speaks slicer.
- If requestHandlers is not specified then default request handlers are added,
- controlled by enableSlicer, enableDICOM, enableStaticPages arguments (all True by default).
- Exec interface is enabled it enableExec and enableSlicer are both set to True
- (enableExec is set to False by default for improved security).
- """
- def __init__(self, port=None, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True, requestHandlers=None, logMessage=None):
- if logMessage:
- self.logMessage = logMessage
-
- if port:
- self.port = port
- else:
- self.port = 2016
-
- self.server = None
- self.serverStarted = False
-
- moduleDirectory = os.path.dirname(slicer.modules.webserver.path.encode())
- self.docroot = moduleDirectory + b"/Resources/docroot"
-
- self.requestHandlers = []
- if requestHandlers is None:
- # No custom request handlers are specified, use the defaults
- self.addDefaultRequestHandlers(enableSlicer, enableExec, enableDICOM, enableStaticPages)
- else:
- # Use the specified custom request handlers
- for requestHandler in requestHandlers:
- self.requestHandlers.append(requestHandler)
-
- def addDefaultRequestHandlers(self, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True):
- if enableSlicer:
- from WebServerLib import SlicerRequestHandler
- self.requestHandlers.append(SlicerRequestHandler(enableExec))
- if enableDICOM:
- from WebServerLib import DICOMRequestHandler
- self.requestHandlers.append(DICOMRequestHandler())
- if enableStaticPages:
- from WebServerLib import StaticPagesRequestHandler
- self.requestHandlers.append(StaticPagesRequestHandler(self.docroot))
-
- def logMessage(self, *args):
- logging.debug(args)
-
- def start(self):
- """Set up the server"""
- self.stop()
- self.port = SlicerHTTPServer.findFreePort(self.port)
- self.logMessage("Starting server on port %d" % self.port)
- self.logMessage('docroot: %s' % self.docroot)
- # example: certfile = '/Users/pieper/slicer/latest/SlicerWeb/localhost.pem'
- certfile = None
- self.server = SlicerHTTPServer(requestHandlers=self.requestHandlers,
- docroot=self.docroot,
- server_address=("", self.port),
- logMessage=self.logMessage,
- certfile=certfile)
- self.server.start()
- self.serverStarted = True
-
- def stop(self):
- if self.server:
- self.server.stop()
- self.serverStarted = False
- self.logMessage("Server stopped.")
+ """Include a concrete subclass of SimpleHTTPServer
+ that speaks slicer.
+ If requestHandlers is not specified then default request handlers are added,
+ controlled by enableSlicer, enableDICOM, enableStaticPages arguments (all True by default).
+ Exec interface is enabled it enableExec and enableSlicer are both set to True
+ (enableExec is set to False by default for improved security).
+ """
+ def __init__(self, port=None, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True, requestHandlers=None, logMessage=None):
+ if logMessage:
+ self.logMessage = logMessage
+
+ if port:
+ self.port = port
+ else:
+ self.port = 2016
+
+ self.server = None
+ self.serverStarted = False
+
+ moduleDirectory = os.path.dirname(slicer.modules.webserver.path.encode())
+ self.docroot = moduleDirectory + b"/Resources/docroot"
+
+ self.requestHandlers = []
+ if requestHandlers is None:
+ # No custom request handlers are specified, use the defaults
+ self.addDefaultRequestHandlers(enableSlicer, enableExec, enableDICOM, enableStaticPages)
+ else:
+ # Use the specified custom request handlers
+ for requestHandler in requestHandlers:
+ self.requestHandlers.append(requestHandler)
+
+ def addDefaultRequestHandlers(self, enableSlicer=True, enableExec=False, enableDICOM=True, enableStaticPages=True):
+ if enableSlicer:
+ from WebServerLib import SlicerRequestHandler
+ self.requestHandlers.append(SlicerRequestHandler(enableExec))
+ if enableDICOM:
+ from WebServerLib import DICOMRequestHandler
+ self.requestHandlers.append(DICOMRequestHandler())
+ if enableStaticPages:
+ from WebServerLib import StaticPagesRequestHandler
+ self.requestHandlers.append(StaticPagesRequestHandler(self.docroot))
+
+ def logMessage(self, *args):
+ logging.debug(args)
+
+ def start(self):
+ """Set up the server"""
+ self.stop()
+ self.port = SlicerHTTPServer.findFreePort(self.port)
+ self.logMessage("Starting server on port %d" % self.port)
+ self.logMessage('docroot: %s' % self.docroot)
+ # example: certfile = '/Users/pieper/slicer/latest/SlicerWeb/localhost.pem'
+ certfile = None
+ self.server = SlicerHTTPServer(requestHandlers=self.requestHandlers,
+ docroot=self.docroot,
+ server_address=("", self.port),
+ logMessage=self.logMessage,
+ certfile=certfile)
+ self.server.start()
+ self.serverStarted = True
+
+ def stop(self):
+ if self.server:
+ self.server.stop()
+ self.serverStarted = False
+ self.logMessage("Server stopped.")
diff --git a/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py
index 61922be1fbd..82c3a3d9747 100644
--- a/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py
+++ b/Modules/Scripted/WebServer/WebServerLib/DICOMRequestHandler.py
@@ -6,207 +6,207 @@
class DICOMRequestHandler(object):
- """
- Implements the mapping between DICOMweb endpoints
- and ctkDICOMDatabase api calls.
- TODO: only a subset of api calls supported, but enough to server a viewer app (ohif)
- """
-
- def __init__(self):
"""
- :param logMessage: callable to log messages
+ Implements the mapping between DICOMweb endpoints
+ and ctkDICOMDatabase api calls.
+ TODO: only a subset of api calls supported, but enough to server a viewer app (ohif)
"""
- self.retrieveURLTag = pydicom.tag.Tag(0x00080190)
- self.numberOfStudyRelatedSeriesTag = pydicom.tag.Tag(0x00200206)
- self.numberOfStudyRelatedInstancesTag = pydicom.tag.Tag(0x00200208)
- def logMessage(self, *args):
- logging.debug(args)
+ def __init__(self):
+ """
+ :param logMessage: callable to log messages
+ """
+ self.retrieveURLTag = pydicom.tag.Tag(0x00080190)
+ self.numberOfStudyRelatedSeriesTag = pydicom.tag.Tag(0x00200206)
+ self.numberOfStudyRelatedInstancesTag = pydicom.tag.Tag(0x00200208)
- def canHandleRequest(self, uri, requestBody):
- parsedURL = urllib.parse.urlparse(uri)
- return 0.5 if parsedURL.path.startswith(b'/dicom') else 0.0
+ def logMessage(self, *args):
+ logging.debug(args)
- def handleRequest(self, uri, requestBody):
- """
- Dispatches various dicom requests
- :param parsedURL: the REST path and arguments
- :param requestBody: the binary that came with the request
- """
- parsedURL = urllib.parse.urlparse(uri)
- contentType = b'text/plain'
- responseBody = None
- splitPath = parsedURL.path.split(b'/')
- if len(splitPath) > 4 and splitPath[4].startswith(b"series"):
- self.logMessage("handling series")
- contentType, responseBody = self.handleSeries(parsedURL, requestBody)
- elif len(splitPath) > 2 and splitPath[2].startswith(b"studies"):
- self.logMessage('handling studies')
- contentType, responseBody = self.handleStudies(parsedURL, requestBody)
- else:
- self.logMessage('Looks like wadouri %s' % parsedURL.query)
- contentType, responseBody = self.handleWADOURI(parsedURL, requestBody)
- return contentType, responseBody
+ def canHandleRequest(self, uri, requestBody):
+ parsedURL = urllib.parse.urlparse(uri)
+ return 0.5 if parsedURL.path.startswith(b'/dicom') else 0.0
- def handleStudies(self, parsedURL, requestBody):
- """
- Handle study requests by returning json
- :param parsedURL: the REST path and arguments
- :param requestBody: the binary that came with the request
- """
- contentType = b'application/json'
- splitPath = parsedURL.path.split(b'/')
- offset = 0
- limit = 100
- params = parsedURL.query.split(b"&")
- for param in params:
- if param.split(b"=")[0] == b"offset":
- offset = int(param.split(b"=")[1])
- if param.split(b"=")[0] == b"limit":
- limit = int(param.split(b"=")[1])
- studyCount = 0
- responseBody = b"[{}]"
- if len(splitPath) == 3:
- # studies qido search
- representativeSeries = None
- studyResponseString = b"["
- for patient in slicer.dicomDatabase.patients():
- if studyCount > offset + limit:
- break
- for study in slicer.dicomDatabase.studiesForPatient(patient):
- studyCount += 1
- if studyCount < offset:
- continue
- if studyCount > offset + limit:
- break
- series = slicer.dicomDatabase.seriesForStudy(study)
- numberOfStudyRelatedSeries = len(series)
- numberOfStudyRelatedInstances = 0
- modalitiesInStudy = set()
- for serie in series:
- seriesInstances = slicer.dicomDatabase.instancesForSeries(serie)
- numberOfStudyRelatedInstances += len(seriesInstances)
- if len(seriesInstances) > 0:
- representativeSeries = serie
- try:
- dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(seriesInstances[0]), stop_before_pixels=True)
- modalitiesInStudy.add(dataset.Modality)
- except AttributeError as e:
- print('Could not get instance information for %s' % seriesInstances[0])
- print(e)
- if representativeSeries is None:
- print('Could not find any instances for study %s' % study)
- continue
- instances = slicer.dicomDatabase.instancesForSeries(representativeSeries)
- firstInstance = instances[0]
- dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True)
- studyDataset = pydicom.dataset.Dataset()
- studyDataset.SpecificCharacterSet = [u'ISO_IR 100']
- studyDataset.StudyDate = dataset.StudyDate
- studyDataset.StudyTime = dataset.StudyTime
- studyDataset.StudyDescription = dataset.StudyDescription
- studyDataset.StudyInstanceUID = dataset.StudyInstanceUID
- studyDataset.AccessionNumber = dataset.AccessionNumber
- studyDataset.InstanceAvailability = u'ONLINE'
- studyDataset.ModalitiesInStudy = list(modalitiesInStudy)
- studyDataset.ReferringPhysicianName = dataset.ReferringPhysicianName
- studyDataset[self.retrieveURLTag] = pydicom.dataelem.DataElement(
- 0x00080190, "UR", "http://example.com") # TODO: provide WADO-RS RetrieveURL
- studyDataset.PatientName = dataset.PatientName
- studyDataset.PatientID = dataset.PatientID
- studyDataset.PatientBirthDate = dataset.PatientBirthDate
- studyDataset.PatientSex = dataset.PatientSex
- studyDataset.StudyID = dataset.StudyID
- studyDataset[self.numberOfStudyRelatedSeriesTag] = pydicom.dataelem.DataElement(
- self.numberOfStudyRelatedSeriesTag, "IS", str(numberOfStudyRelatedSeries))
- studyDataset[self.numberOfStudyRelatedInstancesTag] = pydicom.dataelem.DataElement(
- self.numberOfStudyRelatedInstancesTag, "IS", str(numberOfStudyRelatedInstances))
- jsonDataset = studyDataset.to_json(studyDataset)
- studyResponseString += jsonDataset.encode() + b","
- if studyResponseString.endswith(b','):
- studyResponseString = studyResponseString[:-1]
- studyResponseString += b']'
- responseBody = studyResponseString
- elif splitPath[4] == b'metadata':
- self.logMessage('returning metadata')
- contentType = b'application/json'
- responseBody = b"["
- studyUID = splitPath[3].decode()
- series = slicer.dicomDatabase.seriesForStudy(studyUID)
- for serie in series:
- seriesInstances = slicer.dicomDatabase.instancesForSeries(serie)
- for instance in seriesInstances:
- dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True)
- jsonDataset = dataset.to_json()
- responseBody += jsonDataset.encode() + b","
- if responseBody.endswith(b','):
- responseBody = responseBody[:-1]
- responseBody += b']'
- return contentType, responseBody
+ def handleRequest(self, uri, requestBody):
+ """
+ Dispatches various dicom requests
+ :param parsedURL: the REST path and arguments
+ :param requestBody: the binary that came with the request
+ """
+ parsedURL = urllib.parse.urlparse(uri)
+ contentType = b'text/plain'
+ responseBody = None
+ splitPath = parsedURL.path.split(b'/')
+ if len(splitPath) > 4 and splitPath[4].startswith(b"series"):
+ self.logMessage("handling series")
+ contentType, responseBody = self.handleSeries(parsedURL, requestBody)
+ elif len(splitPath) > 2 and splitPath[2].startswith(b"studies"):
+ self.logMessage('handling studies')
+ contentType, responseBody = self.handleStudies(parsedURL, requestBody)
+ else:
+ self.logMessage('Looks like wadouri %s' % parsedURL.query)
+ contentType, responseBody = self.handleWADOURI(parsedURL, requestBody)
+ return contentType, responseBody
- def handleSeries(self, parsedURL, requestBody):
- """
- Handle series requests by returning json
- :param parsedURL: the REST path and arguments
- :param requestBody: the binary that came with the request
- """
- contentType = b'application/json'
- splitPath = parsedURL.path.split(b'/')
- responseBody = b"[{}]"
- if len(splitPath) == 5:
- # series qido search
- studyUID = splitPath[-2].decode()
- seriesResponseString = b"["
- series = slicer.dicomDatabase.seriesForStudy(studyUID)
- for serie in series:
- instances = slicer.dicomDatabase.instancesForSeries(serie, 1)
- firstInstance = instances[0]
- dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True)
- seriesDataset = pydicom.dataset.Dataset()
- seriesDataset.SpecificCharacterSet = [u'ISO_IR 100']
- seriesDataset.Modality = dataset.Modality
- seriesDataset.SeriesInstanceUID = dataset.SeriesInstanceUID
- seriesDataset.SeriesNumber = dataset.SeriesNumber
- if hasattr(dataset, "PerformedProcedureStepStartDate"):
- seriesDataset.PerformedProcedureStepStartDate = dataset.PerformedProcedureStepStartDate
- if hasattr(dataset, "PerformedProcedureStepStartTime"):
- seriesDataset.PerformedProcedureStepStartTime = dataset.PerformedProcedureStepStartTime
- jsonDataset = seriesDataset.to_json(seriesDataset)
- seriesResponseString += jsonDataset.encode() + b","
- if seriesResponseString.endswith(b','):
- seriesResponseString = seriesResponseString[:-1]
- seriesResponseString += b']'
- responseBody = seriesResponseString
- elif len(splitPath) == 7 and splitPath[6] == b'metadata':
- self.logMessage('returning series metadata')
- contentType = b'application/json'
- responseBody = b"["
- seriesUID = splitPath[5].decode()
- seriesInstances = slicer.dicomDatabase.instancesForSeries(seriesUID)
- for instance in seriesInstances:
- dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True)
- jsonDataset = dataset.to_json()
- responseBody += jsonDataset.encode() + b","
- if responseBody.endswith(b','):
- responseBody = responseBody[:-1]
- responseBody += b']'
- return contentType, responseBody
+ def handleStudies(self, parsedURL, requestBody):
+ """
+ Handle study requests by returning json
+ :param parsedURL: the REST path and arguments
+ :param requestBody: the binary that came with the request
+ """
+ contentType = b'application/json'
+ splitPath = parsedURL.path.split(b'/')
+ offset = 0
+ limit = 100
+ params = parsedURL.query.split(b"&")
+ for param in params:
+ if param.split(b"=")[0] == b"offset":
+ offset = int(param.split(b"=")[1])
+ if param.split(b"=")[0] == b"limit":
+ limit = int(param.split(b"=")[1])
+ studyCount = 0
+ responseBody = b"[{}]"
+ if len(splitPath) == 3:
+ # studies qido search
+ representativeSeries = None
+ studyResponseString = b"["
+ for patient in slicer.dicomDatabase.patients():
+ if studyCount > offset + limit:
+ break
+ for study in slicer.dicomDatabase.studiesForPatient(patient):
+ studyCount += 1
+ if studyCount < offset:
+ continue
+ if studyCount > offset + limit:
+ break
+ series = slicer.dicomDatabase.seriesForStudy(study)
+ numberOfStudyRelatedSeries = len(series)
+ numberOfStudyRelatedInstances = 0
+ modalitiesInStudy = set()
+ for serie in series:
+ seriesInstances = slicer.dicomDatabase.instancesForSeries(serie)
+ numberOfStudyRelatedInstances += len(seriesInstances)
+ if len(seriesInstances) > 0:
+ representativeSeries = serie
+ try:
+ dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(seriesInstances[0]), stop_before_pixels=True)
+ modalitiesInStudy.add(dataset.Modality)
+ except AttributeError as e:
+ print('Could not get instance information for %s' % seriesInstances[0])
+ print(e)
+ if representativeSeries is None:
+ print('Could not find any instances for study %s' % study)
+ continue
+ instances = slicer.dicomDatabase.instancesForSeries(representativeSeries)
+ firstInstance = instances[0]
+ dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True)
+ studyDataset = pydicom.dataset.Dataset()
+ studyDataset.SpecificCharacterSet = [u'ISO_IR 100']
+ studyDataset.StudyDate = dataset.StudyDate
+ studyDataset.StudyTime = dataset.StudyTime
+ studyDataset.StudyDescription = dataset.StudyDescription
+ studyDataset.StudyInstanceUID = dataset.StudyInstanceUID
+ studyDataset.AccessionNumber = dataset.AccessionNumber
+ studyDataset.InstanceAvailability = u'ONLINE'
+ studyDataset.ModalitiesInStudy = list(modalitiesInStudy)
+ studyDataset.ReferringPhysicianName = dataset.ReferringPhysicianName
+ studyDataset[self.retrieveURLTag] = pydicom.dataelem.DataElement(
+ 0x00080190, "UR", "http://example.com") # TODO: provide WADO-RS RetrieveURL
+ studyDataset.PatientName = dataset.PatientName
+ studyDataset.PatientID = dataset.PatientID
+ studyDataset.PatientBirthDate = dataset.PatientBirthDate
+ studyDataset.PatientSex = dataset.PatientSex
+ studyDataset.StudyID = dataset.StudyID
+ studyDataset[self.numberOfStudyRelatedSeriesTag] = pydicom.dataelem.DataElement(
+ self.numberOfStudyRelatedSeriesTag, "IS", str(numberOfStudyRelatedSeries))
+ studyDataset[self.numberOfStudyRelatedInstancesTag] = pydicom.dataelem.DataElement(
+ self.numberOfStudyRelatedInstancesTag, "IS", str(numberOfStudyRelatedInstances))
+ jsonDataset = studyDataset.to_json(studyDataset)
+ studyResponseString += jsonDataset.encode() + b","
+ if studyResponseString.endswith(b','):
+ studyResponseString = studyResponseString[:-1]
+ studyResponseString += b']'
+ responseBody = studyResponseString
+ elif splitPath[4] == b'metadata':
+ self.logMessage('returning metadata')
+ contentType = b'application/json'
+ responseBody = b"["
+ studyUID = splitPath[3].decode()
+ series = slicer.dicomDatabase.seriesForStudy(studyUID)
+ for serie in series:
+ seriesInstances = slicer.dicomDatabase.instancesForSeries(serie)
+ for instance in seriesInstances:
+ dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True)
+ jsonDataset = dataset.to_json()
+ responseBody += jsonDataset.encode() + b","
+ if responseBody.endswith(b','):
+ responseBody = responseBody[:-1]
+ responseBody += b']'
+ return contentType, responseBody
- def handleWADOURI(self, parsedURL, requestBody):
- """
- Handle wado uri by returning the binary part10 contents of the dicom file
- :param parsedURL: the REST path and arguments
- :param requestBody: the binary that came with the request
- """
- q = urllib.parse.parse_qs(parsedURL.query)
- try:
- instanceUID = q[b'objectUID'][0].decode().strip()
- except KeyError:
- return None, None
- self.logMessage('found uid %s' % instanceUID)
- contentType = b'application/dicom'
- path = slicer.dicomDatabase.fileForInstance(instanceUID)
- fp = open(path, 'rb')
- responseBody = fp.read()
- fp.close()
- return contentType, responseBody
+ def handleSeries(self, parsedURL, requestBody):
+ """
+ Handle series requests by returning json
+ :param parsedURL: the REST path and arguments
+ :param requestBody: the binary that came with the request
+ """
+ contentType = b'application/json'
+ splitPath = parsedURL.path.split(b'/')
+ responseBody = b"[{}]"
+ if len(splitPath) == 5:
+ # series qido search
+ studyUID = splitPath[-2].decode()
+ seriesResponseString = b"["
+ series = slicer.dicomDatabase.seriesForStudy(studyUID)
+ for serie in series:
+ instances = slicer.dicomDatabase.instancesForSeries(serie, 1)
+ firstInstance = instances[0]
+ dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(firstInstance), stop_before_pixels=True)
+ seriesDataset = pydicom.dataset.Dataset()
+ seriesDataset.SpecificCharacterSet = [u'ISO_IR 100']
+ seriesDataset.Modality = dataset.Modality
+ seriesDataset.SeriesInstanceUID = dataset.SeriesInstanceUID
+ seriesDataset.SeriesNumber = dataset.SeriesNumber
+ if hasattr(dataset, "PerformedProcedureStepStartDate"):
+ seriesDataset.PerformedProcedureStepStartDate = dataset.PerformedProcedureStepStartDate
+ if hasattr(dataset, "PerformedProcedureStepStartTime"):
+ seriesDataset.PerformedProcedureStepStartTime = dataset.PerformedProcedureStepStartTime
+ jsonDataset = seriesDataset.to_json(seriesDataset)
+ seriesResponseString += jsonDataset.encode() + b","
+ if seriesResponseString.endswith(b','):
+ seriesResponseString = seriesResponseString[:-1]
+ seriesResponseString += b']'
+ responseBody = seriesResponseString
+ elif len(splitPath) == 7 and splitPath[6] == b'metadata':
+ self.logMessage('returning series metadata')
+ contentType = b'application/json'
+ responseBody = b"["
+ seriesUID = splitPath[5].decode()
+ seriesInstances = slicer.dicomDatabase.instancesForSeries(seriesUID)
+ for instance in seriesInstances:
+ dataset = pydicom.dcmread(slicer.dicomDatabase.fileForInstance(instance), stop_before_pixels=True)
+ jsonDataset = dataset.to_json()
+ responseBody += jsonDataset.encode() + b","
+ if responseBody.endswith(b','):
+ responseBody = responseBody[:-1]
+ responseBody += b']'
+ return contentType, responseBody
+
+ def handleWADOURI(self, parsedURL, requestBody):
+ """
+ Handle wado uri by returning the binary part10 contents of the dicom file
+ :param parsedURL: the REST path and arguments
+ :param requestBody: the binary that came with the request
+ """
+ q = urllib.parse.parse_qs(parsedURL.query)
+ try:
+ instanceUID = q[b'objectUID'][0].decode().strip()
+ except KeyError:
+ return None, None
+ self.logMessage('found uid %s' % instanceUID)
+ contentType = b'application/dicom'
+ path = slicer.dicomDatabase.fileForInstance(instanceUID)
+ fp = open(path, 'rb')
+ responseBody = fp.read()
+ fp.close()
+ return contentType, responseBody
diff --git a/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py
index f890cc81c9a..2dda9632c0e 100644
--- a/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py
+++ b/Modules/Scripted/WebServer/WebServerLib/SlicerRequestHandler.py
@@ -13,465 +13,465 @@
class SlicerRequestHandler(object):
- """Implements the Slicer REST api"""
-
- def __init__(self, enableExec=False):
- self.enableExec = enableExec
-
- def logMessage(self, *args):
- logging.debug(args)
-
- def canHandleRequest(self, uri, requestBody):
- parsedURL = urllib.parse.urlparse(uri)
- pathParts = os.path.split(parsedURL.path) # path is like /slicer/timeimage
- route = pathParts[0]
- return 0.5 if route.startswith(b'/slicer') else 0.0
-
- def handleRequest(self, uri, requestBody):
- """Handle a slicer api request.
- TODO: better routing (add routing plugins)
- :param request: request portion of the URL
- :param requestBody: binary data that came with request
- :return: tuple of (mime) type and responseBody (binary)
- """
- parsedURL = urllib.parse.urlparse(uri)
- request = parsedURL.path
- request = request[len(b'/slicer'):]
- if parsedURL.query != b"":
- request += b'?' + parsedURL.query
- self.logMessage(' request is: %s' % request)
-
- responseBody = None
- contentType = b'text/plain'
- try:
- if self.enableExec and request.find(b'/exec') == 0:
- responseBody, contentType = self.exec(request, requestBody)
- elif request.find(b'/timeimage') == 0:
- responseBody, contentType = self.timeimage(request)
- elif request.find(b'/gui') == 0:
- responseBody, contentType = self.gui(request)
- elif request.find(b'/screenshot') == 0:
- responseBody, contentType = self.screenshot(request)
- elif request.find(b'/slice') == 0:
- responseBody, contentType = self.slice(request)
- elif request.find(b'/threeD') == 0:
- responseBody, contentType = self.threeD(request)
- elif request.find(b'/mrml') == 0:
- responseBody, contentType = self.mrml(request)
- elif request.find(b'/tracking') == 0:
- responseBody, contentType = self.tracking(request)
- elif request.find(b'/sampledata') == 0:
- responseBody, contentType = self.sampleData(request)
- elif request.find(b'/volumeSelection') == 0:
- responseBody, contentType = self.volumeSelection(request)
- elif request.find(b'/volumes') == 0:
- responseBody, contentType = self.volumes(request, requestBody)
- elif request.find(b'/volume') == 0:
- responseBody, contentType = self.volume(request, requestBody)
- elif request.find(b'/gridTransforms') == 0:
- responseBody, contentType = self.gridTransforms(request, requestBody)
- elif request.find(b'/gridTransform') == 0:
- responseBody, contentType = self.gridTransform(request, requestBody)
- print("responseBody", len(responseBody))
- elif request.find(b'/fiducials') == 0:
- responseBody, contentType = self.fiducials(request, requestBody)
- elif request.find(b'/fiducial') == 0:
- responseBody, contentType = self.fiducial(request, requestBody)
- elif request.find(b'/accessDICOMwebStudy') == 0:
- responseBody, contentType = self.accessDICOMwebStudy(request, requestBody)
- else:
- responseBody = b"unknown command \"" + request + b"\""
- except:
- self.logMessage("Could not handle slicer command: %s" % request)
- etype, value, tb = sys.exc_info()
- import traceback
- self.logMessage(etype, value)
- self.logMessage(traceback.format_tb(tb))
- print(etype, value)
- print(traceback.format_tb(tb))
- for frame in traceback.format_tb(tb):
- print(frame)
- return contentType, responseBody
-
- def exec(self, request, requestBody):
- """
- Implements the Read Eval Print Loop for python code.
- :param source: python code to run
- :return: result of code running as json string (from the content of the
- `dict` object set into the `__execResult` variable)
- example:
-curl -X POST localhost:2016/slicer/exec --data "slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)"
- """
- self.logMessage('exec with body %s' % requestBody)
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- if requestBody:
- source = requestBody
- else:
- try:
- source = urllib.parse.unquote(q['source'][0])
- except KeyError:
- self.logMessage('need to supply source code to run')
- return "", b'text/plain'
- self.logMessage('will run %s' % source)
- exec("__execResult = {}", globals())
- exec(source, globals())
- result = json.dumps(eval("__execResult", globals())).encode()
- self.logMessage('result: %s' % result)
- return result, b'application/json'
-
- def setupMRMLTracking(self):
- """
- For the tracking endpoint this creates a kind of 'cursor' in the scene.
- Adds "trackingDevice" (model node) to self.
- """
- if not hasattr(self, "trackingDevice"):
- """ set up the mrml parts or use existing """
- nodes = slicer.mrmlScene.GetNodesByName('trackingDevice')
- if nodes.GetNumberOfItems() > 0:
- self.trackingDevice = nodes.GetItemAsObject(0)
- nodes = slicer.mrmlScene.GetNodesByName('tracker')
- self.tracker = nodes.GetItemAsObject(0)
- else:
- # trackingDevice cursor
- self.cube = vtk.vtkCubeSource()
- self.cube.SetXLength(30)
- self.cube.SetYLength(70)
- self.cube.SetZLength(5)
- self.cube.Update()
- # display node
- self.modelDisplay = slicer.vtkMRMLModelDisplayNode()
- self.modelDisplay.SetColor(1, 1, 0) # yellow
- slicer.mrmlScene.AddNode(self.modelDisplay)
- # self.modelDisplay.SetPolyData(self.cube.GetOutputPort())
- # Create model node
- self.trackingDevice = slicer.vtkMRMLModelNode()
- self.trackingDevice.SetScene(slicer.mrmlScene)
- self.trackingDevice.SetName("trackingDevice")
- self.trackingDevice.SetAndObservePolyData(self.cube.GetOutputDataObject(0))
- self.trackingDevice.SetAndObserveDisplayNodeID(self.modelDisplay.GetID())
- slicer.mrmlScene.AddNode(self.trackingDevice)
- # tracker
- self.tracker = slicer.vtkMRMLLinearTransformNode()
- self.tracker.SetName('tracker')
- slicer.mrmlScene.AddNode(self.tracker)
- self.trackingDevice.SetAndObserveTransformNodeID(self.tracker.GetID())
-
- def tracking(self, request):
- """
- Send the matrix for a tracked object in the scene
- :param m: 4x4 tracker matrix in column major order (position is last row)
- :param q: quaternion in WXYZ order
- :param p: position (last column of transform)
- Matrix is overwritten if position or quaternion are provided
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- self.logMessage(q)
- try:
- transformMatrix = list(map(float, q['m'][0].split(',')))
- except KeyError:
- transformMatrix = None
- try:
- quaternion = list(map(float, q['q'][0].split(',')))
- except KeyError:
- quaternion = None
- try:
- position = list(map(float, q['p'][0].split(',')))
- except KeyError:
- position = None
-
- self.setupMRMLTracking()
- m = vtk.vtkMatrix4x4()
- self.tracker.GetMatrixTransformToParent(m)
-
- if transformMatrix:
- for row in range(3):
- for column in range(3):
- m.SetElement(row, column, transformMatrix[3 * row + column])
- m.SetElement(row, column, transformMatrix[3 * row + column])
- m.SetElement(row, column, transformMatrix[3 * row + column])
- m.SetElement(row, column, transformMatrix[3 * row + column])
-
- if position:
- for row in range(3):
- m.SetElement(row, 3, position[row])
-
- if quaternion:
- qu = vtk.vtkQuaternion['float64']()
- qu.SetW(quaternion[0])
- qu.SetX(quaternion[1])
- qu.SetY(quaternion[2])
- qu.SetZ(quaternion[3])
- m3 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
- qu.ToMatrix3x3(m3)
- for row in range(3):
- for column in range(3):
- m.SetElement(row, column, m3[row][column])
-
- self.tracker.SetMatrixTransformToParent(m)
-
- return (f"Set matrix".encode()), b'text/plain'
-
- def sampleData(self, request):
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- self.logMessage(f"SampleData request: {repr(request)}")
- try:
- name = q['name'][0].strip()
- except KeyError:
- name = None
- if not name:
- return (b"sampledata name was not specifiedXYZ"), b'text/plain'
- import SampleData
- try:
- SampleData.downloadSample(name)
- except IndexError:
- return (f"sampledata {name} was not found".encode()), b'text/plain'
- return (f"Sample data {name} loaded".encode()), b'text/plain'
-
- def volumeSelection(self, request):
- """
- Cycles through loaded volumes in the scene
- :param cmd: either "next" or "previous" to indicate direction
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- cmd = q['cmd'][0].strip().lower()
- except KeyError:
- cmd = 'next'
- options = ['next', 'previous']
- if not cmd in options:
- cmd = 'next'
-
- applicationLogic = slicer.app.applicationLogic()
- selectionNode = applicationLogic.GetSelectionNode()
- currentNodeID = selectionNode.GetActiveVolumeID()
- currentIndex = 0
- if currentNodeID:
- nodes = slicer.util.getNodes('vtkMRML*VolumeNode*')
- for nodeName in nodes:
- if nodes[nodeName].GetID() == currentNodeID:
- break
- currentIndex += 1
- if currentIndex >= len(nodes):
- currentIndex = 0
- if cmd == 'next':
- newIndex = currentIndex + 1
- elif cmd == 'previous':
- newIndex = currentIndex - 1
- if newIndex >= len(nodes):
- newIndex = 0
- if newIndex < 0:
- newIndex = len(nodes) - 1
- volumeNode = nodes[nodes.keys()[newIndex]]
- selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID())
- applicationLogic.PropagateVolumeSelection(0)
- return (f"Volume selected".encode()), b'text/plain'
-
- def volumes(self, request, requestBody):
- """
- Returns a json list of mrml volume names and ids
- """
- volumes = []
- mrmlVolumes = slicer.util.getNodes('vtkMRMLScalarVolumeNode*')
- mrmlVolumes.update(slicer.util.getNodes('vtkMRMLLabelMapVolumeNode*'))
- for id_ in mrmlVolumes.keys():
- volumeNode = mrmlVolumes[id_]
- volumes.append({"name": volumeNode.GetName(), "id": volumeNode.GetID()})
- return (json.dumps(volumes).encode()), b'application/json'
-
- def volume(self, request, requestBody):
- """
- If there is a request body, this tries to parse the binary as nrrd
- and put it in the scene, either in an existing node or a new one.
- If there is no request body then the binary of the nrrd is returned for the given id.
- :param id: is the mrml id of the volume to get or put
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- volumeID = q['id'][0].strip()
- except KeyError:
- volumeID = 'vtkMRMLScalarVolumeNode*'
-
- if requestBody:
- return self.postNRRD(volumeID, requestBody), b'application/octet-stream'
- else:
- return self.getNRRD(volumeID), b'application/octet-stream'
-
- def gridTransforms(self, request, requestBody):
- """
- Returns a list of names and ids of grid transforms in the scene
- """
- gridTransforms = []
- mrmlGridTransforms = slicer.util.getNodes('vtkMRMLGridTransformNode*')
- for id_ in mrmlGridTransforms.keys():
- gridTransform = mrmlGridTransforms[id_]
- gridTransforms.append({"name": gridTransform.GetName(), "id": gridTransform.GetID()})
- return (json.dumps(gridTransforms).encode()), b'application/json'
-
- def gridTransform(self, request, requestBody):
- """
- If there is a request body, this tries to parse the binary as nrrd grid transform
- and put it in the scene, either in an existing node or a new one.
- If there is no request body then the binary of the nrrd is returned for the given id.
- :param id: is the mrml id of the volume to get or put
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- transformID = q['id'][0].strip()
- except KeyError:
- transformID = 'vtkMRMLGridTransformNode*'
-
- if requestBody:
- return self.postTransformNRRD(transformID, requestBody), b'application/octet-stream'
- else:
- return self.getTransformNRRD(transformID), b'application/octet-stream'
-
- def postNRRD(self, volumeID, requestBody):
- """Convert a binary blob of nrrd data into a node in the scene.
- Overwrite volumeID if it exists, otherwise create new
- :param volumeID: mrml id of the volume to update (new is created if id is invalid)
- :param requestBody: the binary of the nrrd.
- .. note:: only a subset of valid nrrds are supported (just scalar volumes and grid transforms)
- """
-
- if requestBody[:4] != b"NRRD":
- self.logMessage('Cannot load non-nrrd file (magic is %s)' % requestBody[:4])
- return
-
- fields = {}
- endOfHeader = requestBody.find(b'\n\n') # TODO: could be \r\n
- header = requestBody[:endOfHeader]
- self.logMessage(header)
- for line in header.split(b'\n'):
- colonIndex = line.find(b':')
- if line[0] != '#' and colonIndex != -1:
- key = line[:colonIndex]
- value = line[colonIndex + 2:]
- fields[key] = value
-
- if fields[b'type'] != b'short':
- self.logMessage('Can only read short volumes')
- return b"{'status': 'failed'}"
- if fields[b'dimension'] != b'3':
- self.logMessage('Can only read 3D, 1 component volumes')
- return b"{'status': 'failed'}"
- if fields[b'endian'] != b'little':
- self.logMessage('Can only read little endian')
- return b"{'status': 'failed'}"
- if fields[b'encoding'] != b'raw':
- self.logMessage('Can only read raw encoding')
- return b"{'status': 'failed'}"
- if fields[b'space'] != b'left-posterior-superior':
- self.logMessage('Can only read space in LPS')
- return b"{'status': 'failed'}"
-
- imageData = vtk.vtkImageData()
- imageData.SetDimensions(list(map(int, fields[b'sizes'].split(b' '))))
- imageData.AllocateScalars(vtk.VTK_SHORT, 1)
-
- origin = list(map(float, fields[b'space origin'].replace(b'(', b'').replace(b')', b'').split(b',')))
- origin[0] *= -1
- origin[1] *= -1
-
- directions = []
- directionParts = fields[b'space directions'].split(b')')[:3]
- for directionPart in directionParts:
- part = directionPart.replace(b'(', b'').replace(b')', b'').split(b',')
- directions.append(list(map(float, part)))
-
- ijkToRAS = vtk.vtkMatrix4x4()
- ijkToRAS.Identity()
- for row in range(3):
- ijkToRAS.SetElement(row, 3, origin[row])
- for column in range(3):
- element = directions[column][row]
- if row < 2:
- element *= -1
- ijkToRAS.SetElement(row, column, element)
-
- try:
- node = slicer.util.getNode(volumeID)
- except slicer.util.MRMLNodeNotFoundException:
- node = None
- if not node:
- node = slicer.vtkMRMLScalarVolumeNode()
- node.SetName(volumeID)
- slicer.mrmlScene.AddNode(node)
- node.CreateDefaultDisplayNodes()
- node.SetAndObserveImageData(imageData)
- node.SetIJKToRASMatrix(ijkToRAS)
-
- pixels = numpy.frombuffer(requestBody[endOfHeader + 2:], dtype=numpy.dtype('int16'))
- array = slicer.util.array(node.GetID())
- array[:] = pixels.reshape(array.shape)
- imageData.GetPointData().GetScalars().Modified()
-
- displayNode = node.GetDisplayNode()
- displayNode.ProcessMRMLEvents(displayNode, vtk.vtkCommand.ModifiedEvent, "")
- # TODO: this could be optional
- slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveVolumeID(node.GetID())
- slicer.app.applicationLogic().PropagateVolumeSelection()
-
- return b"{'status': 'success'}"
-
- def getNRRD(self, volumeID):
- """Return a nrrd binary blob with contents of the volume node
- :param volumeID: must be a valid mrml id
- """
- volumeNode = slicer.util.getNode(volumeID)
- volumeArray = slicer.util.array(volumeID)
-
- if volumeNode is None or volumeArray is None:
- self.logMessage('Could not find requested volume')
- return None
- supportedNodes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"]
- if not volumeNode.GetClassName() in supportedNodes:
- self.logMessage('Can only get scalar volumes')
- return None
-
- imageData = volumeNode.GetImageData()
-
- supportedScalarTypes = ["short", "double"]
- scalarType = imageData.GetScalarTypeAsString()
- if scalarType not in supportedScalarTypes:
- self.logMessage('Can only get volumes of types %s, not %s' % (str(supportedScalarTypes), scalarType))
- self.logMessage('Converting to short, but may cause data loss.')
- volumeArray = numpy.array(volumeArray, dtype='int16')
- scalarType = 'short'
-
- sizes = imageData.GetDimensions()
- sizes = " ".join(list(map(str, sizes)))
-
- originList = [0, ] * 3
- directionLists = [[0, ] * 3, [0, ] * 3, [0, ] * 3]
- ijkToRAS = vtk.vtkMatrix4x4()
- volumeNode.GetIJKToRASMatrix(ijkToRAS)
- for row in range(3):
- originList[row] = ijkToRAS.GetElement(row, 3)
- for column in range(3):
- element = ijkToRAS.GetElement(row, column)
- if row < 2:
- element *= -1
- directionLists[column][row] = element
- originList[0] *= -1
- originList[1] *= -1
- origin = '(' + ','.join(list(map(str, originList))) + ')'
- directions = ""
- for directionList in directionLists:
- direction = '(' + ','.join(list(map(str, directionList))) + ')'
- directions += direction + " "
- directions = directions[:-1]
-
- # should look like:
- # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0)
- # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172)
-
- nrrdHeader = """NRRD0004
+ """Implements the Slicer REST api"""
+
+ def __init__(self, enableExec=False):
+ self.enableExec = enableExec
+
+ def logMessage(self, *args):
+ logging.debug(args)
+
+ def canHandleRequest(self, uri, requestBody):
+ parsedURL = urllib.parse.urlparse(uri)
+ pathParts = os.path.split(parsedURL.path) # path is like /slicer/timeimage
+ route = pathParts[0]
+ return 0.5 if route.startswith(b'/slicer') else 0.0
+
+ def handleRequest(self, uri, requestBody):
+ """Handle a slicer api request.
+ TODO: better routing (add routing plugins)
+ :param request: request portion of the URL
+ :param requestBody: binary data that came with request
+ :return: tuple of (mime) type and responseBody (binary)
+ """
+ parsedURL = urllib.parse.urlparse(uri)
+ request = parsedURL.path
+ request = request[len(b'/slicer'):]
+ if parsedURL.query != b"":
+ request += b'?' + parsedURL.query
+ self.logMessage(' request is: %s' % request)
+
+ responseBody = None
+ contentType = b'text/plain'
+ try:
+ if self.enableExec and request.find(b'/exec') == 0:
+ responseBody, contentType = self.exec(request, requestBody)
+ elif request.find(b'/timeimage') == 0:
+ responseBody, contentType = self.timeimage(request)
+ elif request.find(b'/gui') == 0:
+ responseBody, contentType = self.gui(request)
+ elif request.find(b'/screenshot') == 0:
+ responseBody, contentType = self.screenshot(request)
+ elif request.find(b'/slice') == 0:
+ responseBody, contentType = self.slice(request)
+ elif request.find(b'/threeD') == 0:
+ responseBody, contentType = self.threeD(request)
+ elif request.find(b'/mrml') == 0:
+ responseBody, contentType = self.mrml(request)
+ elif request.find(b'/tracking') == 0:
+ responseBody, contentType = self.tracking(request)
+ elif request.find(b'/sampledata') == 0:
+ responseBody, contentType = self.sampleData(request)
+ elif request.find(b'/volumeSelection') == 0:
+ responseBody, contentType = self.volumeSelection(request)
+ elif request.find(b'/volumes') == 0:
+ responseBody, contentType = self.volumes(request, requestBody)
+ elif request.find(b'/volume') == 0:
+ responseBody, contentType = self.volume(request, requestBody)
+ elif request.find(b'/gridTransforms') == 0:
+ responseBody, contentType = self.gridTransforms(request, requestBody)
+ elif request.find(b'/gridTransform') == 0:
+ responseBody, contentType = self.gridTransform(request, requestBody)
+ print("responseBody", len(responseBody))
+ elif request.find(b'/fiducials') == 0:
+ responseBody, contentType = self.fiducials(request, requestBody)
+ elif request.find(b'/fiducial') == 0:
+ responseBody, contentType = self.fiducial(request, requestBody)
+ elif request.find(b'/accessDICOMwebStudy') == 0:
+ responseBody, contentType = self.accessDICOMwebStudy(request, requestBody)
+ else:
+ responseBody = b"unknown command \"" + request + b"\""
+ except:
+ self.logMessage("Could not handle slicer command: %s" % request)
+ etype, value, tb = sys.exc_info()
+ import traceback
+ self.logMessage(etype, value)
+ self.logMessage(traceback.format_tb(tb))
+ print(etype, value)
+ print(traceback.format_tb(tb))
+ for frame in traceback.format_tb(tb):
+ print(frame)
+ return contentType, responseBody
+
+ def exec(self, request, requestBody):
+ """
+ Implements the Read Eval Print Loop for python code.
+ :param source: python code to run
+ :return: result of code running as json string (from the content of the
+ `dict` object set into the `__execResult` variable)
+ example:
+ curl -X POST localhost:2016/slicer/exec --data "slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)"
+ """
+ self.logMessage('exec with body %s' % requestBody)
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ if requestBody:
+ source = requestBody
+ else:
+ try:
+ source = urllib.parse.unquote(q['source'][0])
+ except KeyError:
+ self.logMessage('need to supply source code to run')
+ return "", b'text/plain'
+ self.logMessage('will run %s' % source)
+ exec("__execResult = {}", globals())
+ exec(source, globals())
+ result = json.dumps(eval("__execResult", globals())).encode()
+ self.logMessage('result: %s' % result)
+ return result, b'application/json'
+
+ def setupMRMLTracking(self):
+ """
+ For the tracking endpoint this creates a kind of 'cursor' in the scene.
+ Adds "trackingDevice" (model node) to self.
+ """
+ if not hasattr(self, "trackingDevice"):
+ """ set up the mrml parts or use existing """
+ nodes = slicer.mrmlScene.GetNodesByName('trackingDevice')
+ if nodes.GetNumberOfItems() > 0:
+ self.trackingDevice = nodes.GetItemAsObject(0)
+ nodes = slicer.mrmlScene.GetNodesByName('tracker')
+ self.tracker = nodes.GetItemAsObject(0)
+ else:
+ # trackingDevice cursor
+ self.cube = vtk.vtkCubeSource()
+ self.cube.SetXLength(30)
+ self.cube.SetYLength(70)
+ self.cube.SetZLength(5)
+ self.cube.Update()
+ # display node
+ self.modelDisplay = slicer.vtkMRMLModelDisplayNode()
+ self.modelDisplay.SetColor(1, 1, 0) # yellow
+ slicer.mrmlScene.AddNode(self.modelDisplay)
+ # self.modelDisplay.SetPolyData(self.cube.GetOutputPort())
+ # Create model node
+ self.trackingDevice = slicer.vtkMRMLModelNode()
+ self.trackingDevice.SetScene(slicer.mrmlScene)
+ self.trackingDevice.SetName("trackingDevice")
+ self.trackingDevice.SetAndObservePolyData(self.cube.GetOutputDataObject(0))
+ self.trackingDevice.SetAndObserveDisplayNodeID(self.modelDisplay.GetID())
+ slicer.mrmlScene.AddNode(self.trackingDevice)
+ # tracker
+ self.tracker = slicer.vtkMRMLLinearTransformNode()
+ self.tracker.SetName('tracker')
+ slicer.mrmlScene.AddNode(self.tracker)
+ self.trackingDevice.SetAndObserveTransformNodeID(self.tracker.GetID())
+
+ def tracking(self, request):
+ """
+ Send the matrix for a tracked object in the scene
+ :param m: 4x4 tracker matrix in column major order (position is last row)
+ :param q: quaternion in WXYZ order
+ :param p: position (last column of transform)
+ Matrix is overwritten if position or quaternion are provided
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ self.logMessage(q)
+ try:
+ transformMatrix = list(map(float, q['m'][0].split(',')))
+ except KeyError:
+ transformMatrix = None
+ try:
+ quaternion = list(map(float, q['q'][0].split(',')))
+ except KeyError:
+ quaternion = None
+ try:
+ position = list(map(float, q['p'][0].split(',')))
+ except KeyError:
+ position = None
+
+ self.setupMRMLTracking()
+ m = vtk.vtkMatrix4x4()
+ self.tracker.GetMatrixTransformToParent(m)
+
+ if transformMatrix:
+ for row in range(3):
+ for column in range(3):
+ m.SetElement(row, column, transformMatrix[3 * row + column])
+ m.SetElement(row, column, transformMatrix[3 * row + column])
+ m.SetElement(row, column, transformMatrix[3 * row + column])
+ m.SetElement(row, column, transformMatrix[3 * row + column])
+
+ if position:
+ for row in range(3):
+ m.SetElement(row, 3, position[row])
+
+ if quaternion:
+ qu = vtk.vtkQuaternion['float64']()
+ qu.SetW(quaternion[0])
+ qu.SetX(quaternion[1])
+ qu.SetY(quaternion[2])
+ qu.SetZ(quaternion[3])
+ m3 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
+ qu.ToMatrix3x3(m3)
+ for row in range(3):
+ for column in range(3):
+ m.SetElement(row, column, m3[row][column])
+
+ self.tracker.SetMatrixTransformToParent(m)
+
+ return (f"Set matrix".encode()), b'text/plain'
+
+ def sampleData(self, request):
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ self.logMessage(f"SampleData request: {repr(request)}")
+ try:
+ name = q['name'][0].strip()
+ except KeyError:
+ name = None
+ if not name:
+ return (b"sampledata name was not specifiedXYZ"), b'text/plain'
+ import SampleData
+ try:
+ SampleData.downloadSample(name)
+ except IndexError:
+ return (f"sampledata {name} was not found".encode()), b'text/plain'
+ return (f"Sample data {name} loaded".encode()), b'text/plain'
+
+ def volumeSelection(self, request):
+ """
+ Cycles through loaded volumes in the scene
+ :param cmd: either "next" or "previous" to indicate direction
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ cmd = q['cmd'][0].strip().lower()
+ except KeyError:
+ cmd = 'next'
+ options = ['next', 'previous']
+ if not cmd in options:
+ cmd = 'next'
+
+ applicationLogic = slicer.app.applicationLogic()
+ selectionNode = applicationLogic.GetSelectionNode()
+ currentNodeID = selectionNode.GetActiveVolumeID()
+ currentIndex = 0
+ if currentNodeID:
+ nodes = slicer.util.getNodes('vtkMRML*VolumeNode*')
+ for nodeName in nodes:
+ if nodes[nodeName].GetID() == currentNodeID:
+ break
+ currentIndex += 1
+ if currentIndex >= len(nodes):
+ currentIndex = 0
+ if cmd == 'next':
+ newIndex = currentIndex + 1
+ elif cmd == 'previous':
+ newIndex = currentIndex - 1
+ if newIndex >= len(nodes):
+ newIndex = 0
+ if newIndex < 0:
+ newIndex = len(nodes) - 1
+ volumeNode = nodes[nodes.keys()[newIndex]]
+ selectionNode.SetReferenceActiveVolumeID(volumeNode.GetID())
+ applicationLogic.PropagateVolumeSelection(0)
+ return (f"Volume selected".encode()), b'text/plain'
+
+ def volumes(self, request, requestBody):
+ """
+ Returns a json list of mrml volume names and ids
+ """
+ volumes = []
+ mrmlVolumes = slicer.util.getNodes('vtkMRMLScalarVolumeNode*')
+ mrmlVolumes.update(slicer.util.getNodes('vtkMRMLLabelMapVolumeNode*'))
+ for id_ in mrmlVolumes.keys():
+ volumeNode = mrmlVolumes[id_]
+ volumes.append({"name": volumeNode.GetName(), "id": volumeNode.GetID()})
+ return (json.dumps(volumes).encode()), b'application/json'
+
+ def volume(self, request, requestBody):
+ """
+ If there is a request body, this tries to parse the binary as nrrd
+ and put it in the scene, either in an existing node or a new one.
+ If there is no request body then the binary of the nrrd is returned for the given id.
+ :param id: is the mrml id of the volume to get or put
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ volumeID = q['id'][0].strip()
+ except KeyError:
+ volumeID = 'vtkMRMLScalarVolumeNode*'
+
+ if requestBody:
+ return self.postNRRD(volumeID, requestBody), b'application/octet-stream'
+ else:
+ return self.getNRRD(volumeID), b'application/octet-stream'
+
+ def gridTransforms(self, request, requestBody):
+ """
+ Returns a list of names and ids of grid transforms in the scene
+ """
+ gridTransforms = []
+ mrmlGridTransforms = slicer.util.getNodes('vtkMRMLGridTransformNode*')
+ for id_ in mrmlGridTransforms.keys():
+ gridTransform = mrmlGridTransforms[id_]
+ gridTransforms.append({"name": gridTransform.GetName(), "id": gridTransform.GetID()})
+ return (json.dumps(gridTransforms).encode()), b'application/json'
+
+ def gridTransform(self, request, requestBody):
+ """
+ If there is a request body, this tries to parse the binary as nrrd grid transform
+ and put it in the scene, either in an existing node or a new one.
+ If there is no request body then the binary of the nrrd is returned for the given id.
+ :param id: is the mrml id of the volume to get or put
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ transformID = q['id'][0].strip()
+ except KeyError:
+ transformID = 'vtkMRMLGridTransformNode*'
+
+ if requestBody:
+ return self.postTransformNRRD(transformID, requestBody), b'application/octet-stream'
+ else:
+ return self.getTransformNRRD(transformID), b'application/octet-stream'
+
+ def postNRRD(self, volumeID, requestBody):
+ """Convert a binary blob of nrrd data into a node in the scene.
+ Overwrite volumeID if it exists, otherwise create new
+ :param volumeID: mrml id of the volume to update (new is created if id is invalid)
+ :param requestBody: the binary of the nrrd.
+ .. note:: only a subset of valid nrrds are supported (just scalar volumes and grid transforms)
+ """
+
+ if requestBody[:4] != b"NRRD":
+ self.logMessage('Cannot load non-nrrd file (magic is %s)' % requestBody[:4])
+ return
+
+ fields = {}
+ endOfHeader = requestBody.find(b'\n\n') # TODO: could be \r\n
+ header = requestBody[:endOfHeader]
+ self.logMessage(header)
+ for line in header.split(b'\n'):
+ colonIndex = line.find(b':')
+ if line[0] != '#' and colonIndex != -1:
+ key = line[:colonIndex]
+ value = line[colonIndex + 2:]
+ fields[key] = value
+
+ if fields[b'type'] != b'short':
+ self.logMessage('Can only read short volumes')
+ return b"{'status': 'failed'}"
+ if fields[b'dimension'] != b'3':
+ self.logMessage('Can only read 3D, 1 component volumes')
+ return b"{'status': 'failed'}"
+ if fields[b'endian'] != b'little':
+ self.logMessage('Can only read little endian')
+ return b"{'status': 'failed'}"
+ if fields[b'encoding'] != b'raw':
+ self.logMessage('Can only read raw encoding')
+ return b"{'status': 'failed'}"
+ if fields[b'space'] != b'left-posterior-superior':
+ self.logMessage('Can only read space in LPS')
+ return b"{'status': 'failed'}"
+
+ imageData = vtk.vtkImageData()
+ imageData.SetDimensions(list(map(int, fields[b'sizes'].split(b' '))))
+ imageData.AllocateScalars(vtk.VTK_SHORT, 1)
+
+ origin = list(map(float, fields[b'space origin'].replace(b'(', b'').replace(b')', b'').split(b',')))
+ origin[0] *= -1
+ origin[1] *= -1
+
+ directions = []
+ directionParts = fields[b'space directions'].split(b')')[:3]
+ for directionPart in directionParts:
+ part = directionPart.replace(b'(', b'').replace(b')', b'').split(b',')
+ directions.append(list(map(float, part)))
+
+ ijkToRAS = vtk.vtkMatrix4x4()
+ ijkToRAS.Identity()
+ for row in range(3):
+ ijkToRAS.SetElement(row, 3, origin[row])
+ for column in range(3):
+ element = directions[column][row]
+ if row < 2:
+ element *= -1
+ ijkToRAS.SetElement(row, column, element)
+
+ try:
+ node = slicer.util.getNode(volumeID)
+ except slicer.util.MRMLNodeNotFoundException:
+ node = None
+ if not node:
+ node = slicer.vtkMRMLScalarVolumeNode()
+ node.SetName(volumeID)
+ slicer.mrmlScene.AddNode(node)
+ node.CreateDefaultDisplayNodes()
+ node.SetAndObserveImageData(imageData)
+ node.SetIJKToRASMatrix(ijkToRAS)
+
+ pixels = numpy.frombuffer(requestBody[endOfHeader + 2:], dtype=numpy.dtype('int16'))
+ array = slicer.util.array(node.GetID())
+ array[:] = pixels.reshape(array.shape)
+ imageData.GetPointData().GetScalars().Modified()
+
+ displayNode = node.GetDisplayNode()
+ displayNode.ProcessMRMLEvents(displayNode, vtk.vtkCommand.ModifiedEvent, "")
+ # TODO: this could be optional
+ slicer.app.applicationLogic().GetSelectionNode().SetReferenceActiveVolumeID(node.GetID())
+ slicer.app.applicationLogic().PropagateVolumeSelection()
+
+ return b"{'status': 'success'}"
+
+ def getNRRD(self, volumeID):
+ """Return a nrrd binary blob with contents of the volume node
+ :param volumeID: must be a valid mrml id
+ """
+ volumeNode = slicer.util.getNode(volumeID)
+ volumeArray = slicer.util.array(volumeID)
+
+ if volumeNode is None or volumeArray is None:
+ self.logMessage('Could not find requested volume')
+ return None
+ supportedNodes = ["vtkMRMLScalarVolumeNode", "vtkMRMLLabelMapVolumeNode"]
+ if not volumeNode.GetClassName() in supportedNodes:
+ self.logMessage('Can only get scalar volumes')
+ return None
+
+ imageData = volumeNode.GetImageData()
+
+ supportedScalarTypes = ["short", "double"]
+ scalarType = imageData.GetScalarTypeAsString()
+ if scalarType not in supportedScalarTypes:
+ self.logMessage('Can only get volumes of types %s, not %s' % (str(supportedScalarTypes), scalarType))
+ self.logMessage('Converting to short, but may cause data loss.')
+ volumeArray = numpy.array(volumeArray, dtype='int16')
+ scalarType = 'short'
+
+ sizes = imageData.GetDimensions()
+ sizes = " ".join(list(map(str, sizes)))
+
+ originList = [0, ] * 3
+ directionLists = [[0, ] * 3, [0, ] * 3, [0, ] * 3]
+ ijkToRAS = vtk.vtkMatrix4x4()
+ volumeNode.GetIJKToRASMatrix(ijkToRAS)
+ for row in range(3):
+ originList[row] = ijkToRAS.GetElement(row, 3)
+ for column in range(3):
+ element = ijkToRAS.GetElement(row, column)
+ if row < 2:
+ element *= -1
+ directionLists[column][row] = element
+ originList[0] *= -1
+ originList[1] *= -1
+ origin = '(' + ','.join(list(map(str, originList))) + ')'
+ directions = ""
+ for directionList in directionLists:
+ direction = '(' + ','.join(list(map(str, directionList))) + ')'
+ directions += direction + " "
+ directions = directions[:-1]
+
+ # should look like:
+ # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0)
+ # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172)
+
+ nrrdHeader = """NRRD0004
# Complete NRRD file format specification at:
# http://teem.sourceforge.net/nrrd/format.html
type: %%scalarType%%
@@ -486,53 +486,53 @@ def getNRRD(self, volumeID):
""".replace("%%scalarType%%", scalarType).replace("%%sizes%%", sizes).replace("%%directions%%", directions).replace("%%origin%%", origin)
- nrrdData = nrrdHeader.encode() + volumeArray.tobytes()
- return nrrdData
-
- def getTransformNRRD(self, transformID):
- """Return a nrrd binary blob with contents of the transform node
- """
- transformNode = slicer.util.getNode(transformID)
- transformArray = slicer.util.array(transformID)
-
- if transformNode is None or transformArray is None:
- self.logMessage('Could not find requested transform')
- return None
- supportedNodes = ["vtkMRMLGridTransformNode", ]
- if not transformNode.GetClassName() in supportedNodes:
- self.logMessage('Can only get grid transforms')
- return None
-
- # map the vectors to be in the LPS measurement frame
- # (need to make a copy so as not to change the slicer transform)
- lpsArray = numpy.array(transformArray)
- lpsArray *= numpy.array([-1, -1, 1])
-
- imageData = transformNode.GetTransformFromParent().GetDisplacementGrid()
-
- # for now, only handle non-oriented grid transform as
- # generated from LandmarkRegistration
- # TODO: generalize for any GridTransform node
- # -- here we assume it is axial as generated by LandmarkTransform
-
- sizes = (3,) + imageData.GetDimensions()
- sizes = " ".join(list(map(str, sizes)))
-
- spacing = list(imageData.GetSpacing())
- spacing[0] *= -1 # RAS to LPS
- spacing[1] *= -1 # RAS to LPS
- directions = '(%g,0,0) (0,%g,0) (0,0,%g)' % tuple(spacing)
-
- origin = list(imageData.GetOrigin())
- origin[0] *= -1 # RAS to LPS
- origin[1] *= -1 # RAS to LPS
- origin = '(%g,%g,%g)' % tuple(origin)
-
- # should look like:
- # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0)
- # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172)
-
- nrrdHeader = """NRRD0004
+ nrrdData = nrrdHeader.encode() + volumeArray.tobytes()
+ return nrrdData
+
+ def getTransformNRRD(self, transformID):
+ """Return a nrrd binary blob with contents of the transform node
+ """
+ transformNode = slicer.util.getNode(transformID)
+ transformArray = slicer.util.array(transformID)
+
+ if transformNode is None or transformArray is None:
+ self.logMessage('Could not find requested transform')
+ return None
+ supportedNodes = ["vtkMRMLGridTransformNode", ]
+ if not transformNode.GetClassName() in supportedNodes:
+ self.logMessage('Can only get grid transforms')
+ return None
+
+ # map the vectors to be in the LPS measurement frame
+ # (need to make a copy so as not to change the slicer transform)
+ lpsArray = numpy.array(transformArray)
+ lpsArray *= numpy.array([-1, -1, 1])
+
+ imageData = transformNode.GetTransformFromParent().GetDisplacementGrid()
+
+ # for now, only handle non-oriented grid transform as
+ # generated from LandmarkRegistration
+ # TODO: generalize for any GridTransform node
+ # -- here we assume it is axial as generated by LandmarkTransform
+
+ sizes = (3,) + imageData.GetDimensions()
+ sizes = " ".join(list(map(str, sizes)))
+
+ spacing = list(imageData.GetSpacing())
+ spacing[0] *= -1 # RAS to LPS
+ spacing[1] *= -1 # RAS to LPS
+ directions = '(%g,0,0) (0,%g,0) (0,0,%g)' % tuple(spacing)
+
+ origin = list(imageData.GetOrigin())
+ origin[0] *= -1 # RAS to LPS
+ origin[1] *= -1 # RAS to LPS
+ origin = '(%g,%g,%g)' % tuple(origin)
+
+ # should look like:
+ # space directions: (0,1,0) (0,0,-1) (-1.2999954223632812,0,0)
+ # space origin: (86.644897460937486,-133.92860412597656,116.78569793701172)
+
+ nrrdHeader = """NRRD0004
# Complete NRRD file format specification at:
# http://teem.sourceforge.net/nrrd/format.html
type: float
@@ -547,401 +547,401 @@ def getTransformNRRD(self, transformID):
""".replace("%%sizes%%", sizes).replace("%%directions%%", directions).replace("%%origin%%", origin)
- nrrdData = nrrdHeader.encode() + lpsArray.tobytes()
- return nrrdData
-
- def fiducials(self, request, requestBody):
- """return fiducials list in ad hoc json structure
- TODO: should use the markups json version
- """
- fiducials = {}
- for markupsNode in slicer.util.getNodesByClass('vtkMRMLMarkupsFiducialNode'):
- displayNode = markupsNode.GetDisplayNode()
- node = {}
- node['name'] = markupsNode.GetName()
- node['color'] = displayNode.GetSelectedColor()
- node['scale'] = displayNode.GetGlyphScale()
- node['markups'] = []
- for markupIndex in range(markupsNode.GetNumberOfMarkups()):
- position = [0, ] * 3
- markupsNode.GetNthFiducialPosition(markupIndex, position)
- position
- node['markups'].append({
- 'label': markupsNode.GetNthFiducialLabel(markupIndex),
- 'position': position
- })
- fiducials[markupsNode.GetID()] = node
- return (json.dumps(fiducials).encode()), b'application/json'
-
- def fiducial(self, request, requestBody):
- """
- Set the location of a control point in a markups fiducial
- :param id: mrml id of the fiducial list
- :param r: Right coordinate
- :param a: Anterior coordinate
- :param s: Superior coordinate
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- fiducialID = q['id'][0].strip()
- except KeyError:
- fiducialID = 'vtkMRMLMarkupsFiducialNode*'
- try:
- index = q['index'][0].strip()
- except KeyError:
- index = 0
- try:
- r = q['r'][0].strip()
- except KeyError:
- r = 0
- try:
- a = q['a'][0].strip()
- except KeyError:
- a = 0
- try:
- s = q['s'][0].strip()
- except KeyError:
- s = 0
-
- fiducialNode = slicer.util.getNode(fiducialID)
- fiducialNode.SetNthFiducialPosition(index, float(r), float(a), float(s));
- return "{'result': 'ok'}", b'application/json'
-
- def accessDICOMwebStudy(self, request, requestBody):
- """
- Access DICOMweb server to download requested study, add it to
- Slicer's dicom database, and load it into the scene.
- :param requestBody: is a json string
- :param requestBody['dicomWEBPrefix']: is the start of the url
- :param requestBody['dicomWEBStore']: is the middle of the url
- :param requestBody['studyUID']: is the end of the url
- :param requestBody['accessToken']: is the authorization bearer token for the DICOMweb server
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
-
- request = json.loads(requestBody), b'application/json'
-
- dicomWebEndpoint = request['dicomWEBPrefix'] + '/' + request['dicomWEBStore']
- print(f"Loading from {dicomWebEndpoint}")
-
- from DICOMLib import DICOMUtils
- loadedUIDs = DICOMUtils.importFromDICOMWeb(
- dicomWebEndpoint=request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'],
- studyInstanceUID=request['studyUID'],
- accessToken=request['accessToken'])
-
- files = []
- for studyUID in loadedUIDs:
- for seriesUID in slicer.dicomDatabase.seriesForStudy(studyUID):
- for instance in slicer.dicomDatabase.instancesForSeries(seriesUID):
- files.append(slicer.dicomDatabase.fileForInstance(instance))
- loadables = DICOMUtils.getLoadablesFromFileLists([files])
- loadedNodes = DICOMUtils.loadLoadables(loadLoadables)
-
- print(f"Loaded {loadedUIDs}, and {loadedNodes}")
-
- return b'{"result": "ok"}'
-
- def mrml(self, request):
- """
- Returns a json list of all the mrml nodes
- """
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- return (json.dumps(list(slicer.util.getNodes('*').keys())).encode()), b'application/json'
-
- def screenshot(self, request):
- """
- Returns screenshot of the application main window.
- """
- slicer.app.processEvents()
- slicer.util.forceRenderAllViews()
- screenshot = slicer.util.mainWindow().grab()
- bArray = qt.QByteArray()
- buffer = qt.QBuffer(bArray)
- buffer.open(qt.QIODevice.WriteOnly)
- screenshot.save(buffer, "PNG")
- pngData = bArray.data()
- self.logMessage('returning an image of %d length' % len(pngData))
- return pngData, b'image/png'
-
- @staticmethod
- def setViewersLayout(layoutName):
- for att in dir(slicer.vtkMRMLLayoutNode):
- if att.startswith("SlicerLayout") and att.endswith("View"):
- foundLayoutName = att[12:-4]
- if layoutName.lower() == foundLayoutName.lower():
- layoutId = eval(f"slicer.vtkMRMLLayoutNode.{att}")
- slicer.app.layoutManager().setLayout(layoutId)
- return
- raise ValueError("Unknown layout name: " + layoutName)
-
- def gui(self, request):
- """return a png of the application GUI.
- :param contents: {full, viewers}
- :param viewersLayout: {fourup, oneup3d, ...} slicer.vtkMRMLLayoutNode constants (SlicerLayout...View)
- :return: png encoded screenshot after applying params
- """
-
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
-
- try:
- contents = q['contents'][0].strip().lower()
- except KeyError:
- contents = None
- if contents == "viewers":
- slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").hide()
- slicer.util.setStatusBarVisible(False)
- slicer.util.setMenuBarsVisible(False)
- slicer.util.setToolbarsVisible(False)
- elif contents == "full":
- slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").show()
- slicer.util.setStatusBarVisible(True)
- slicer.util.setMenuBarsVisible(True)
- slicer.util.setToolbarsVisible(True)
- else:
- if contents:
- raise ValueError("contents must be 'viewers' or 'full'")
-
- try:
- viewersLayout = q['viewersLayout'][0].strip().lower()
- except KeyError:
- viewersLayout = None
- if viewersLayout is not None:
- SlicerRequestHandler.setViewersLayout(viewersLayout)
-
- return (f"Switched {contents} to {viewersLayout}".encode()), b'text/plain'
-
- def slice(self, request):
- """return a png for a slice view.
- :param view: {red, yellow, green}
- :param scrollTo: 0 to 1 for slice position within volume
- :param offset: mm offset relative to slice origin (position of slice slider)
- :param size: pixel size of output png
- :param copySliceGeometryFrom: view name of other slice to copy from
- :param orientation: {axial, sagittal, coronal}
- :return: png encoded slice screenshot after applying params
- """
-
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- view = q['view'][0].strip().lower()
- except KeyError:
- view = 'red'
- options = ['red', 'yellow', 'green']
- if not view in options:
- view = 'red'
- layoutManager = slicer.app.layoutManager()
- sliceLogic = layoutManager.sliceWidget(view.capitalize()).sliceLogic()
- try:
- mode = str(q['mode'][0].strip())
- except (KeyError, ValueError):
- mode = None
- try:
- offset = float(q['offset'][0].strip())
- except (KeyError, ValueError):
- offset = None
- try:
- copySliceGeometryFrom = q['copySliceGeometryFrom'][0].strip()
- except (KeyError, ValueError):
- copySliceGeometryFrom = None
- try:
- scrollTo = float(q['scrollTo'][0].strip())
- except (KeyError, ValueError):
- scrollTo = None
- try:
- size = int(q['size'][0].strip())
- except (KeyError, ValueError):
- size = None
- try:
- orientation = q['orientation'][0].strip()
- except (KeyError, ValueError):
- orientation = None
-
- offsetKey = 'offset.' + view
- # if mode == 'start' or not self.interactionState.has_key(offsetKey):
- # self.interactionState[offsetKey] = sliceLogic.GetSliceOffset()
-
- if scrollTo:
- volumeNode = sliceLogic.GetBackgroundLayer().GetVolumeNode()
- bounds = [0, ] * 6
- sliceLogic.GetVolumeSliceBounds(volumeNode, bounds)
- sliceLogic.SetSliceOffset(bounds[4] + (scrollTo * (bounds[5] - bounds[4])))
- if offset:
- # startOffset = self.interactionState[offsetKey]
- sliceLogic.SetSliceOffset(startOffset + offset)
- if copySliceGeometryFrom:
- otherSliceLogic = layoutManager.sliceWidget(copySliceGeometryFrom.capitalize()).sliceLogic()
- otherSliceNode = otherSliceLogic.GetSliceNode()
- sliceNode = sliceLogic.GetSliceNode()
- # technique from vtkMRMLSliceLinkLogic (TODO: should be exposed as method)
- sliceNode.GetSliceToRAS().DeepCopy(otherSliceNode.GetSliceToRAS())
- fov = sliceNode.GetFieldOfView()
- otherFOV = otherSliceNode.GetFieldOfView()
- sliceNode.SetFieldOfView(otherFOV[0],
- otherFOV[0] * fov[1] / fov[0],
- fov[2])
-
- if orientation:
- sliceNode = sliceLogic.GetSliceNode()
- previousOrientation = sliceNode.GetOrientationString().lower()
- if orientation.lower() == 'axial':
- sliceNode.SetOrientationToAxial()
- if orientation.lower() == 'sagittal':
- sliceNode.SetOrientationToSagittal()
- if orientation.lower() == 'coronal':
- sliceNode.SetOrientationToCoronal()
- if orientation.lower() != previousOrientation:
- sliceLogic.FitSliceToAll()
-
- imageData = sliceLogic.GetBlend().Update(0)
- imageData = sliceLogic.GetBlend().GetOutputDataObject(0)
- pngData = []
- if imageData:
+ nrrdData = nrrdHeader.encode() + lpsArray.tobytes()
+ return nrrdData
+
+ def fiducials(self, request, requestBody):
+ """return fiducials list in ad hoc json structure
+ TODO: should use the markups json version
+ """
+ fiducials = {}
+ for markupsNode in slicer.util.getNodesByClass('vtkMRMLMarkupsFiducialNode'):
+ displayNode = markupsNode.GetDisplayNode()
+ node = {}
+ node['name'] = markupsNode.GetName()
+ node['color'] = displayNode.GetSelectedColor()
+ node['scale'] = displayNode.GetGlyphScale()
+ node['markups'] = []
+ for markupIndex in range(markupsNode.GetNumberOfMarkups()):
+ position = [0, ] * 3
+ markupsNode.GetNthFiducialPosition(markupIndex, position)
+ position
+ node['markups'].append({
+ 'label': markupsNode.GetNthFiducialLabel(markupIndex),
+ 'position': position
+ })
+ fiducials[markupsNode.GetID()] = node
+ return (json.dumps(fiducials).encode()), b'application/json'
+
+ def fiducial(self, request, requestBody):
+ """
+ Set the location of a control point in a markups fiducial
+ :param id: mrml id of the fiducial list
+ :param r: Right coordinate
+ :param a: Anterior coordinate
+ :param s: Superior coordinate
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ fiducialID = q['id'][0].strip()
+ except KeyError:
+ fiducialID = 'vtkMRMLMarkupsFiducialNode*'
+ try:
+ index = q['index'][0].strip()
+ except KeyError:
+ index = 0
+ try:
+ r = q['r'][0].strip()
+ except KeyError:
+ r = 0
+ try:
+ a = q['a'][0].strip()
+ except KeyError:
+ a = 0
+ try:
+ s = q['s'][0].strip()
+ except KeyError:
+ s = 0
+
+ fiducialNode = slicer.util.getNode(fiducialID)
+ fiducialNode.SetNthFiducialPosition(index, float(r), float(a), float(s));
+ return "{'result': 'ok'}", b'application/json'
+
+ def accessDICOMwebStudy(self, request, requestBody):
+ """
+ Access DICOMweb server to download requested study, add it to
+ Slicer's dicom database, and load it into the scene.
+ :param requestBody: is a json string
+ :param requestBody['dicomWEBPrefix']: is the start of the url
+ :param requestBody['dicomWEBStore']: is the middle of the url
+ :param requestBody['studyUID']: is the end of the url
+ :param requestBody['accessToken']: is the authorization bearer token for the DICOMweb server
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+
+ request = json.loads(requestBody), b'application/json'
+
+ dicomWebEndpoint = request['dicomWEBPrefix'] + '/' + request['dicomWEBStore']
+ print(f"Loading from {dicomWebEndpoint}")
+
+ from DICOMLib import DICOMUtils
+ loadedUIDs = DICOMUtils.importFromDICOMWeb(
+ dicomWebEndpoint=request['dicomWEBPrefix'] + '/' + request['dicomWEBStore'],
+ studyInstanceUID=request['studyUID'],
+ accessToken=request['accessToken'])
+
+ files = []
+ for studyUID in loadedUIDs:
+ for seriesUID in slicer.dicomDatabase.seriesForStudy(studyUID):
+ for instance in slicer.dicomDatabase.instancesForSeries(seriesUID):
+ files.append(slicer.dicomDatabase.fileForInstance(instance))
+ loadables = DICOMUtils.getLoadablesFromFileLists([files])
+ loadedNodes = DICOMUtils.loadLoadables(loadLoadables)
+
+ print(f"Loaded {loadedUIDs}, and {loadedNodes}")
+
+ return b'{"result": "ok"}'
+
+ def mrml(self, request):
+ """
+ Returns a json list of all the mrml nodes
+ """
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ return (json.dumps(list(slicer.util.getNodes('*').keys())).encode()), b'application/json'
+
+ def screenshot(self, request):
+ """
+ Returns screenshot of the application main window.
+ """
+ slicer.app.processEvents()
+ slicer.util.forceRenderAllViews()
+ screenshot = slicer.util.mainWindow().grab()
+ bArray = qt.QByteArray()
+ buffer = qt.QBuffer(bArray)
+ buffer.open(qt.QIODevice.WriteOnly)
+ screenshot.save(buffer, "PNG")
+ pngData = bArray.data()
+ self.logMessage('returning an image of %d length' % len(pngData))
+ return pngData, b'image/png'
+
+ @staticmethod
+ def setViewersLayout(layoutName):
+ for att in dir(slicer.vtkMRMLLayoutNode):
+ if att.startswith("SlicerLayout") and att.endswith("View"):
+ foundLayoutName = att[12:-4]
+ if layoutName.lower() == foundLayoutName.lower():
+ layoutId = eval(f"slicer.vtkMRMLLayoutNode.{att}")
+ slicer.app.layoutManager().setLayout(layoutId)
+ return
+ raise ValueError("Unknown layout name: " + layoutName)
+
+ def gui(self, request):
+ """return a png of the application GUI.
+ :param contents: {full, viewers}
+ :param viewersLayout: {fourup, oneup3d, ...} slicer.vtkMRMLLayoutNode constants (SlicerLayout...View)
+ :return: png encoded screenshot after applying params
+ """
+
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+
+ try:
+ contents = q['contents'][0].strip().lower()
+ except KeyError:
+ contents = None
+ if contents == "viewers":
+ slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").hide()
+ slicer.util.setStatusBarVisible(False)
+ slicer.util.setMenuBarsVisible(False)
+ slicer.util.setToolbarsVisible(False)
+ elif contents == "full":
+ slicer.util.findChild(slicer.util.mainWindow(), "PanelDockWidget").show()
+ slicer.util.setStatusBarVisible(True)
+ slicer.util.setMenuBarsVisible(True)
+ slicer.util.setToolbarsVisible(True)
+ else:
+ if contents:
+ raise ValueError("contents must be 'viewers' or 'full'")
+
+ try:
+ viewersLayout = q['viewersLayout'][0].strip().lower()
+ except KeyError:
+ viewersLayout = None
+ if viewersLayout is not None:
+ SlicerRequestHandler.setViewersLayout(viewersLayout)
+
+ return (f"Switched {contents} to {viewersLayout}".encode()), b'text/plain'
+
+ def slice(self, request):
+ """return a png for a slice view.
+ :param view: {red, yellow, green}
+ :param scrollTo: 0 to 1 for slice position within volume
+ :param offset: mm offset relative to slice origin (position of slice slider)
+ :param size: pixel size of output png
+ :param copySliceGeometryFrom: view name of other slice to copy from
+ :param orientation: {axial, sagittal, coronal}
+ :return: png encoded slice screenshot after applying params
+ """
+
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ view = q['view'][0].strip().lower()
+ except KeyError:
+ view = 'red'
+ options = ['red', 'yellow', 'green']
+ if not view in options:
+ view = 'red'
+ layoutManager = slicer.app.layoutManager()
+ sliceLogic = layoutManager.sliceWidget(view.capitalize()).sliceLogic()
+ try:
+ mode = str(q['mode'][0].strip())
+ except (KeyError, ValueError):
+ mode = None
+ try:
+ offset = float(q['offset'][0].strip())
+ except (KeyError, ValueError):
+ offset = None
+ try:
+ copySliceGeometryFrom = q['copySliceGeometryFrom'][0].strip()
+ except (KeyError, ValueError):
+ copySliceGeometryFrom = None
+ try:
+ scrollTo = float(q['scrollTo'][0].strip())
+ except (KeyError, ValueError):
+ scrollTo = None
+ try:
+ size = int(q['size'][0].strip())
+ except (KeyError, ValueError):
+ size = None
+ try:
+ orientation = q['orientation'][0].strip()
+ except (KeyError, ValueError):
+ orientation = None
+
+ offsetKey = 'offset.' + view
+ # if mode == 'start' or not self.interactionState.has_key(offsetKey):
+ # self.interactionState[offsetKey] = sliceLogic.GetSliceOffset()
+
+ if scrollTo:
+ volumeNode = sliceLogic.GetBackgroundLayer().GetVolumeNode()
+ bounds = [0, ] * 6
+ sliceLogic.GetVolumeSliceBounds(volumeNode, bounds)
+ sliceLogic.SetSliceOffset(bounds[4] + (scrollTo * (bounds[5] - bounds[4])))
+ if offset:
+ # startOffset = self.interactionState[offsetKey]
+ sliceLogic.SetSliceOffset(startOffset + offset)
+ if copySliceGeometryFrom:
+ otherSliceLogic = layoutManager.sliceWidget(copySliceGeometryFrom.capitalize()).sliceLogic()
+ otherSliceNode = otherSliceLogic.GetSliceNode()
+ sliceNode = sliceLogic.GetSliceNode()
+ # technique from vtkMRMLSliceLinkLogic (TODO: should be exposed as method)
+ sliceNode.GetSliceToRAS().DeepCopy(otherSliceNode.GetSliceToRAS())
+ fov = sliceNode.GetFieldOfView()
+ otherFOV = otherSliceNode.GetFieldOfView()
+ sliceNode.SetFieldOfView(otherFOV[0],
+ otherFOV[0] * fov[1] / fov[0],
+ fov[2])
+
+ if orientation:
+ sliceNode = sliceLogic.GetSliceNode()
+ previousOrientation = sliceNode.GetOrientationString().lower()
+ if orientation.lower() == 'axial':
+ sliceNode.SetOrientationToAxial()
+ if orientation.lower() == 'sagittal':
+ sliceNode.SetOrientationToSagittal()
+ if orientation.lower() == 'coronal':
+ sliceNode.SetOrientationToCoronal()
+ if orientation.lower() != previousOrientation:
+ sliceLogic.FitSliceToAll()
+
+ imageData = sliceLogic.GetBlend().Update(0)
+ imageData = sliceLogic.GetBlend().GetOutputDataObject(0)
+ pngData = []
+ if imageData:
+ pngData = self.vtkImageDataToPNG(imageData)
+ self.logMessage('returning an image of %d length' % len(pngData))
+ return pngData, b'image/png'
+
+ def threeD(self, request):
+ """return a png for a threeD view
+ :param lookFromAxis: {L, R, A, P, I, S}
+ :return: png binary buffer
+ """
+
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ view = q['view'][0].strip().lower()
+ except KeyError:
+ view = '1'
+ try:
+ lookFromAxis = q['lookFromAxis'][0].strip().lower()
+ except KeyError:
+ lookFromAxis = None
+ try:
+ size = int(q['size'][0].strip())
+ except (KeyError, ValueError):
+ size = None
+ try:
+ mode = str(q['mode'][0].strip())
+ except (KeyError, ValueError):
+ mode = None
+ try:
+ roll = float(q['roll'][0].strip())
+ except (KeyError, ValueError):
+ roll = None
+ try:
+ panX = float(q['panX'][0].strip())
+ except (KeyError, ValueError):
+ panX = None
+ try:
+ panY = float(q['panY'][0].strip())
+ except (KeyError, ValueError):
+ panY = None
+ try:
+ orbitX = float(q['orbitX'][0].strip())
+ except (KeyError, ValueError):
+ orbitX = None
+ try:
+ orbitY = float(q['orbitY'][0].strip())
+ except (KeyError, ValueError):
+ orbitY = None
+
+ layoutManager = slicer.app.layoutManager()
+ view = layoutManager.threeDWidget(0).threeDView()
+ view.renderEnabled = False
+
+ if lookFromAxis:
+ axes = ['None', 'r', 'l', 's', 'i', 'a', 'p']
+ try:
+ axis = axes.index(lookFromAxis[0].lower())
+ view.lookFromViewAxis(axis)
+ except ValueError:
+ pass
+
+ view.renderWindow().Render()
+ view.renderEnabled = True
+ view.forceRender()
+ w2i = vtk.vtkWindowToImageFilter()
+ w2i.SetInput(view.renderWindow())
+ w2i.SetReadFrontBuffer(0)
+ w2i.Update()
+ imageData = w2i.GetOutput()
+
pngData = self.vtkImageDataToPNG(imageData)
- self.logMessage('returning an image of %d length' % len(pngData))
- return pngData, b'image/png'
-
- def threeD(self, request):
- """return a png for a threeD view
- :param lookFromAxis: {L, R, A, P, I, S}
- :return: png binary buffer
- """
-
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- view = q['view'][0].strip().lower()
- except KeyError:
- view = '1'
- try:
- lookFromAxis = q['lookFromAxis'][0].strip().lower()
- except KeyError:
- lookFromAxis = None
- try:
- size = int(q['size'][0].strip())
- except (KeyError, ValueError):
- size = None
- try:
- mode = str(q['mode'][0].strip())
- except (KeyError, ValueError):
- mode = None
- try:
- roll = float(q['roll'][0].strip())
- except (KeyError, ValueError):
- roll = None
- try:
- panX = float(q['panX'][0].strip())
- except (KeyError, ValueError):
- panX = None
- try:
- panY = float(q['panY'][0].strip())
- except (KeyError, ValueError):
- panY = None
- try:
- orbitX = float(q['orbitX'][0].strip())
- except (KeyError, ValueError):
- orbitX = None
- try:
- orbitY = float(q['orbitY'][0].strip())
- except (KeyError, ValueError):
- orbitY = None
-
- layoutManager = slicer.app.layoutManager()
- view = layoutManager.threeDWidget(0).threeDView()
- view.renderEnabled = False
-
- if lookFromAxis:
- axes = ['None', 'r', 'l', 's', 'i', 'a', 'p']
- try:
- axis = axes.index(lookFromAxis[0].lower())
- view.lookFromViewAxis(axis)
- except ValueError:
- pass
-
- view.renderWindow().Render()
- view.renderEnabled = True
- view.forceRender()
- w2i = vtk.vtkWindowToImageFilter()
- w2i.SetInput(view.renderWindow())
- w2i.SetReadFrontBuffer(0)
- w2i.Update()
- imageData = w2i.GetOutput()
-
- pngData = self.vtkImageDataToPNG(imageData)
- self.logMessage('threeD returning an image of %d length' % len(pngData))
- return pngData, b'image/png'
-
- def timeimage(self, request=''):
- """
- For timing and debugging - return an image with the current time
- rendered as text down to the hundredth of a second
- :param color: hex encoded RGB of dashed border (default 333 for dark gray)
- :return: png image
- """
-
- # check arguments
- p = urllib.parse.urlparse(request.decode())
- q = urllib.parse.parse_qs(p.query)
- try:
- color = "#" + q['color'][0].strip().lower()
- except KeyError:
- color = "#330"
-
- #
- # make a generally transparent image,
- #
- imageWidth = 128
- imageHeight = 32
- timeImage = qt.QImage(imageWidth, imageHeight, qt.QImage().Format_ARGB32)
- timeImage.fill(0)
-
- # a painter to use for various jobs
- painter = qt.QPainter()
-
- # draw a border around the pixmap
- painter.begin(timeImage)
- pen = qt.QPen()
- color = qt.QColor(color)
- color.setAlphaF(0.8)
- pen.setColor(color)
- pen.setWidth(5)
- pen.setStyle(3) # dotted line (Qt::DotLine)
- painter.setPen(pen)
- rect = qt.QRect(1, 1, imageWidth - 2, imageHeight - 2)
- painter.drawRect(rect)
- color = qt.QColor("#333")
- pen.setColor(color)
- painter.setPen(pen)
- position = qt.QPoint(10, 20)
- text = str(time.time()) # text to draw
- painter.drawText(position, text)
- painter.end()
-
- # convert the image to vtk, then to png from there
- vtkTimeImage = vtk.vtkImageData()
- slicer.qMRMLUtils().qImageToVtkImageData(timeImage, vtkTimeImage)
- pngData = self.vtkImageDataToPNG(vtkTimeImage)
- return pngData, b'image/png'
-
- def vtkImageDataToPNG(self, imageData):
- """Return a buffer of png data using the data
- from the vtkImageData.
- :param imageData: a vtkImageData instance
- :return: bytes of a png image
- """
- writer = vtk.vtkPNGWriter()
- writer.SetWriteToMemory(True)
- writer.SetInputData(imageData)
- # use compression 0 since data transfer is faster than compressing
- writer.SetCompressionLevel(0)
- writer.Write()
- result = writer.GetResult()
- pngArray = vtk.util.numpy_support.vtk_to_numpy(result)
- pngData = pngArray.tobytes()
-
- return pngData
+ self.logMessage('threeD returning an image of %d length' % len(pngData))
+ return pngData, b'image/png'
+
+ def timeimage(self, request=''):
+ """
+ For timing and debugging - return an image with the current time
+ rendered as text down to the hundredth of a second
+ :param color: hex encoded RGB of dashed border (default 333 for dark gray)
+ :return: png image
+ """
+
+ # check arguments
+ p = urllib.parse.urlparse(request.decode())
+ q = urllib.parse.parse_qs(p.query)
+ try:
+ color = "#" + q['color'][0].strip().lower()
+ except KeyError:
+ color = "#330"
+
+ #
+ # make a generally transparent image,
+ #
+ imageWidth = 128
+ imageHeight = 32
+ timeImage = qt.QImage(imageWidth, imageHeight, qt.QImage().Format_ARGB32)
+ timeImage.fill(0)
+
+ # a painter to use for various jobs
+ painter = qt.QPainter()
+
+ # draw a border around the pixmap
+ painter.begin(timeImage)
+ pen = qt.QPen()
+ color = qt.QColor(color)
+ color.setAlphaF(0.8)
+ pen.setColor(color)
+ pen.setWidth(5)
+ pen.setStyle(3) # dotted line (Qt::DotLine)
+ painter.setPen(pen)
+ rect = qt.QRect(1, 1, imageWidth - 2, imageHeight - 2)
+ painter.drawRect(rect)
+ color = qt.QColor("#333")
+ pen.setColor(color)
+ painter.setPen(pen)
+ position = qt.QPoint(10, 20)
+ text = str(time.time()) # text to draw
+ painter.drawText(position, text)
+ painter.end()
+
+ # convert the image to vtk, then to png from there
+ vtkTimeImage = vtk.vtkImageData()
+ slicer.qMRMLUtils().qImageToVtkImageData(timeImage, vtkTimeImage)
+ pngData = self.vtkImageDataToPNG(vtkTimeImage)
+ return pngData, b'image/png'
+
+ def vtkImageDataToPNG(self, imageData):
+ """Return a buffer of png data using the data
+ from the vtkImageData.
+ :param imageData: a vtkImageData instance
+ :return: bytes of a png image
+ """
+ writer = vtk.vtkPNGWriter()
+ writer.SetWriteToMemory(True)
+ writer.SetInputData(imageData)
+ # use compression 0 since data transfer is faster than compressing
+ writer.SetCompressionLevel(0)
+ writer.Write()
+ result = writer.GetResult()
+ pngArray = vtk.util.numpy_support.vtk_to_numpy(result)
+ pngData = pngArray.tobytes()
+
+ return pngData
diff --git a/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py b/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py
index 2282a0a0032..ae95d7bf9cf 100644
--- a/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py
+++ b/Modules/Scripted/WebServer/WebServerLib/StaticPagesRequestHandler.py
@@ -4,58 +4,58 @@
class StaticPagesRequestHandler(object):
- """Serves static pages content (files) from the configured docroot
- """
-
- def __init__(self, docroot):
- """
- :param docroot: directory path of static pages content
- :param logMessage: callable to log messages
+ """Serves static pages content (files) from the configured docroot
"""
- self.docroot = docroot
- self.logMessage('docroot: %s' % self.docroot)
+ def __init__(self, docroot):
+ """
+ :param docroot: directory path of static pages content
+ :param logMessage: callable to log messages
+ """
- def logMessage(self, *args):
- logging.debug(args)
+ self.docroot = docroot
+ self.logMessage('docroot: %s' % self.docroot)
- def canHandleRequest(self, uri, requestBody):
- return 0.1
+ def logMessage(self, *args):
+ logging.debug(args)
- def handleRequest(self, uri, requestBody):
- """Return directory listing or binary contents of files
- TODO: other header fields like modified time
+ def canHandleRequest(self, uri, requestBody):
+ return 0.1
- :param uri: portion of the url specifying the file path
- :param requestBody: binary data passed with the http request
- :return: tuple of content type (based on file ext) and request body binary (contents of file)
- """
- contentType = b'text/plain'
- responseBody = None
- if uri.startswith(b'/'):
- uri = uri[1:]
- path = os.path.join(self.docroot, uri)
- self.logMessage('docroot: %s' % self.docroot)
- if os.path.isdir(path):
- for index in b"index.html", b"index.htm":
- index = os.path.join(path, index)
- if os.path.exists(index):
- path = index
- self.logMessage(b'Serving: %s' % path)
- if os.path.isdir(path):
- contentType = b"text/html"
- responseBody = b"
"
- for entry in os.listdir(path):
- responseBody += b"- %s
" % (os.path.join(uri, entry), entry)
- responseBody += b"
"
- else:
- ext = os.path.splitext(path)[-1].decode()
- if ext in mimetypes.types_map:
- contentType = mimetypes.types_map[ext].encode()
- try:
- fp = open(path, 'rb')
- responseBody = fp.read()
- fp.close()
- except IOError:
+ def handleRequest(self, uri, requestBody):
+ """Return directory listing or binary contents of files
+ TODO: other header fields like modified time
+
+ :param uri: portion of the url specifying the file path
+ :param requestBody: binary data passed with the http request
+ :return: tuple of content type (based on file ext) and request body binary (contents of file)
+ """
+ contentType = b'text/plain'
responseBody = None
- return contentType, responseBody
+ if uri.startswith(b'/'):
+ uri = uri[1:]
+ path = os.path.join(self.docroot, uri)
+ self.logMessage('docroot: %s' % self.docroot)
+ if os.path.isdir(path):
+ for index in b"index.html", b"index.htm":
+ index = os.path.join(path, index)
+ if os.path.exists(index):
+ path = index
+ self.logMessage(b'Serving: %s' % path)
+ if os.path.isdir(path):
+ contentType = b"text/html"
+ responseBody = b""
+ for entry in os.listdir(path):
+ responseBody += b"- %s
" % (os.path.join(uri, entry), entry)
+ responseBody += b"
"
+ else:
+ ext = os.path.splitext(path)[-1].decode()
+ if ext in mimetypes.types_map:
+ contentType = mimetypes.types_map[ext].encode()
+ try:
+ fp = open(path, 'rb')
+ responseBody = fp.read()
+ fp.close()
+ except IOError:
+ responseBody = None
+ return contentType, responseBody
diff --git a/Testing/ModelRender.py b/Testing/ModelRender.py
index 53b6ab51cbf..c266b3ee0f5 100644
--- a/Testing/ModelRender.py
+++ b/Testing/ModelRender.py
@@ -4,27 +4,27 @@
def newSphere(name=''):
- if name == "":
- name = "sphere-%g" % time.time()
+ if name == "":
+ name = "sphere-%g" % time.time()
- sphere = Slicer.slicer.vtkSphereSource()
- sphere.SetCenter(-100 + 200 * random.random(), -100 + 200 * random.random(), -100 + 200 * random.random())
- sphere.SetRadius(10 + 20 * random.random())
- sphere.GetOutput().Update()
- modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode()
- modelDisplayNode.SetColor(random.random(), random.random(), random.random())
- Slicer.slicer.MRMLScene.AddNode(modelDisplayNode)
- modelNode = Slicer.slicer.vtkMRMLModelNode()
+ sphere = Slicer.slicer.vtkSphereSource()
+ sphere.SetCenter(-100 + 200 * random.random(), -100 + 200 * random.random(), -100 + 200 * random.random())
+ sphere.SetRadius(10 + 20 * random.random())
+ sphere.GetOutput().Update()
+ modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode()
+ modelDisplayNode.SetColor(random.random(), random.random(), random.random())
+ Slicer.slicer.MRMLScene.AddNode(modelDisplayNode)
+ modelNode = Slicer.slicer.vtkMRMLModelNode()
# VTK6 TODO
- modelNode.SetAndObservePolyData(sphere.GetOutput())
- modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID())
- modelNode.SetName(name)
- Slicer.slicer.MRMLScene.AddNode(modelNode)
+ modelNode.SetAndObservePolyData(sphere.GetOutput())
+ modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID())
+ modelNode.SetName(name)
+ Slicer.slicer.MRMLScene.AddNode(modelNode)
def sphereMovie(dir="."):
- for i in range(20):
- newSphere()
- Slicer.TkCall("update")
- Slicer.TkCall("SlicerSaveLargeImage %s/spheres-%d.png 3" % (dir, i))
+ for i in range(20):
+ newSphere()
+ Slicer.TkCall("update")
+ Slicer.TkCall("SlicerSaveLargeImage %s/spheres-%d.png 3" % (dir, i))
diff --git a/Testing/TextureModel.py b/Testing/TextureModel.py
index ae0836fda5e..2b45b0ccc59 100644
--- a/Testing/TextureModel.py
+++ b/Testing/TextureModel.py
@@ -4,74 +4,74 @@
def newPlane():
- # create a plane polydata
- plane = Slicer.slicer.vtkPlaneSource()
- plane.SetOrigin(0., 0., 0.)
- plane.SetPoint1(100., 0., 0.)
- plane.SetPoint2(0., 0., 100.)
- plane.GetOutput().Update()
-
- # create a simple texture image
- imageSource = Slicer.slicer.vtkImageEllipsoidSource()
- imageSource.GetOutput().Update()
-
- # set up display node that includes the texture
- modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode()
- modelDisplayNode.SetBackfaceCulling(0)
+ # create a plane polydata
+ plane = Slicer.slicer.vtkPlaneSource()
+ plane.SetOrigin(0., 0., 0.)
+ plane.SetPoint1(100., 0., 0.)
+ plane.SetPoint2(0., 0., 100.)
+ plane.GetOutput().Update()
+
+ # create a simple texture image
+ imageSource = Slicer.slicer.vtkImageEllipsoidSource()
+ imageSource.GetOutput().Update()
+
+ # set up display node that includes the texture
+ modelDisplayNode = Slicer.slicer.vtkMRMLModelDisplayNode()
+ modelDisplayNode.SetBackfaceCulling(0)
# VTK6 TODO
- modelDisplayNode.SetAndObserveTextureImageData(imageSource.GetOutput())
- Slicer.slicer.MRMLScene.AddNode(modelDisplayNode)
+ modelDisplayNode.SetAndObserveTextureImageData(imageSource.GetOutput())
+ Slicer.slicer.MRMLScene.AddNode(modelDisplayNode)
- # transform node
- transformNode = Slicer.slicer.vtkMRMLLinearTransformNode()
- transformNode.SetName('PlaneToWorld')
- Slicer.slicer.MRMLScene.AddNode(transformNode)
+ # transform node
+ transformNode = Slicer.slicer.vtkMRMLLinearTransformNode()
+ transformNode.SetName('PlaneToWorld')
+ Slicer.slicer.MRMLScene.AddNode(transformNode)
- # set up model node
- modelNode = Slicer.slicer.vtkMRMLModelNode()
+ # set up model node
+ modelNode = Slicer.slicer.vtkMRMLModelNode()
# VTK6 TODO
- modelNode.SetAndObservePolyData(plane.GetOutput())
- modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID())
- modelNode.SetAndObserveTransformNodeID(transformNode.GetID())
- modelNode.SetName("Plane")
- Slicer.slicer.MRMLScene.AddNode(modelNode)
+ modelNode.SetAndObservePolyData(plane.GetOutput())
+ modelNode.SetAndObserveDisplayNodeID(modelDisplayNode.GetID())
+ modelNode.SetAndObserveTransformNodeID(transformNode.GetID())
+ modelNode.SetName("Plane")
+ Slicer.slicer.MRMLScene.AddNode(modelNode)
- # need to invoke a NodeAddedEvent since some GUI elements
- # don't respond to each event (for efficiency). In C++
- # you would use the vtkMRMLScene::NodeAddedEvent enum but
- # it's not directly available from scripts
- Slicer.slicer.MRMLScene.InvokeEvent(66000)
+ # need to invoke a NodeAddedEvent since some GUI elements
+ # don't respond to each event (for efficiency). In C++
+ # you would use the vtkMRMLScene::NodeAddedEvent enum but
+ # it's not directly available from scripts
+ Slicer.slicer.MRMLScene.InvokeEvent(66000)
- return (modelNode, transformNode, imageSource)
+ return (modelNode, transformNode, imageSource)
def texturedPlane():
- # create the plane and modify the texture and transform
- # every iteration. Call Modified on the PolyData so the
- # viewer will know to update. Call Tk's "update" to flush
- # the event queue so the Render will appear on screen
- # (update is called here as part of the demo - most applications
- # should not directly call update since it can lead to duplicate
- # renders and choppy interaction)
+ # create the plane and modify the texture and transform
+ # every iteration. Call Modified on the PolyData so the
+ # viewer will know to update. Call Tk's "update" to flush
+ # the event queue so the Render will appear on screen
+ # (update is called here as part of the demo - most applications
+ # should not directly call update since it can lead to duplicate
+ # renders and choppy interaction)
- steps = 200
- startTime = time.time()
+ steps = 200
+ startTime = time.time()
- modelNode, transformNode, imageSource = newPlane()
+ modelNode, transformNode, imageSource = newPlane()
- toParent = vtk.vtkMatrix4x4()
- transformNode.GetMatrixTransformToParent(toParent)
- for i in range(steps):
- imageSource.SetInValue(200 * (i % 2))
+ toParent = vtk.vtkMatrix4x4()
+ transformNode.GetMatrixTransformToParent(toParent)
+ for i in range(steps):
+ imageSource.SetInValue(200 * (i % 2))
- toParent.SetElement(0, 3, i)
- transformNode.SetMatrixTransformToParent(toParent)
+ toParent.SetElement(0, 3, i)
+ transformNode.SetMatrixTransformToParent(toParent)
- modelNode.GetPolyData().Modified()
- Slicer.TkCall("update")
+ modelNode.GetPolyData().Modified()
+ Slicer.TkCall("update")
- endTime = time.time()
- elapsed = endTime - startTime
- hertz = int(steps / elapsed)
- print('ran %d iterations in %g seconds (%g hertz)' % (steps, elapsed, hertz))
+ endTime = time.time()
+ elapsed = endTime - startTime
+ hertz = int(steps / elapsed)
+ print('ran %d iterations in %g seconds (%g hertz)' % (steps, elapsed, hertz))
diff --git a/Utilities/Scripts/ExtensionWizard.py b/Utilities/Scripts/ExtensionWizard.py
index 6eff6639784..2101b49c5b8 100755
--- a/Utilities/Scripts/ExtensionWizard.py
+++ b/Utilities/Scripts/ExtensionWizard.py
@@ -7,5 +7,5 @@
if __name__ == "__main__":
- w = ExtensionWizard()
- w.execute()
+ w = ExtensionWizard()
+ w.execute()
diff --git a/Utilities/Scripts/ModuleWizard.py b/Utilities/Scripts/ModuleWizard.py
index 1feb12813bc..6c65ffe5271 100755
--- a/Utilities/Scripts/ModuleWizard.py
+++ b/Utilities/Scripts/ModuleWizard.py
@@ -6,118 +6,118 @@
def findSource(dir):
- fileList = []
- for root, subFolders, files in os.walk(dir):
- for file in files:
- if fnmatch.fnmatch(file, "*.h") or \
- fnmatch.fnmatch(file, "*.cxx") or \
- fnmatch.fnmatch(file, "*.cpp") or \
- fnmatch.fnmatch(file, "CMakeLists.txt") or \
- fnmatch.fnmatch(file, "*.cmake") or \
- fnmatch.fnmatch(file, "*.ui") or \
- fnmatch.fnmatch(file, "*.qrc") or \
- fnmatch.fnmatch(file, "*.py") or \
- fnmatch.fnmatch(file, "*.xml") or \
- fnmatch.fnmatch(file, "*.xml.in") or \
- fnmatch.fnmatch(file, "*.md5") or \
- fnmatch.fnmatch(file, "*.png") or \
- fnmatch.fnmatch(file, "*.dox"):
- file = os.path.join(root, file)
- file = file[len(dir):] # strip common dir
- fileList.append(file)
- return fileList
+ fileList = []
+ for root, subFolders, files in os.walk(dir):
+ for file in files:
+ if fnmatch.fnmatch(file, "*.h") or \
+ fnmatch.fnmatch(file, "*.cxx") or \
+ fnmatch.fnmatch(file, "*.cpp") or \
+ fnmatch.fnmatch(file, "CMakeLists.txt") or \
+ fnmatch.fnmatch(file, "*.cmake") or \
+ fnmatch.fnmatch(file, "*.ui") or \
+ fnmatch.fnmatch(file, "*.qrc") or \
+ fnmatch.fnmatch(file, "*.py") or \
+ fnmatch.fnmatch(file, "*.xml") or \
+ fnmatch.fnmatch(file, "*.xml.in") or \
+ fnmatch.fnmatch(file, "*.md5") or \
+ fnmatch.fnmatch(file, "*.png") or \
+ fnmatch.fnmatch(file, "*.dox"):
+ file = os.path.join(root, file)
+ file = file[len(dir):] # strip common dir
+ fileList.append(file)
+ return fileList
def copyAndReplace(inFile, template, target, key, moduleName):
- newFile = os.path.join(target, inFile.replace(key, moduleName))
- print("creating %s" % newFile)
- path = os.path.dirname(newFile)
- if not os.path.exists(path):
- os.makedirs(path)
-
- fp = open(os.path.join(template, inFile))
- contents = fp.read()
- fp.close()
- contents = contents.replace(key, moduleName)
- contents = contents.replace(key.upper(), moduleName.upper())
- fp = open(newFile, "w")
- fp.write(contents)
- fp.close()
+ newFile = os.path.join(target, inFile.replace(key, moduleName))
+ print("creating %s" % newFile)
+ path = os.path.dirname(newFile)
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ fp = open(os.path.join(template, inFile))
+ contents = fp.read()
+ fp.close()
+ contents = contents.replace(key, moduleName)
+ contents = contents.replace(key.upper(), moduleName.upper())
+ fp = open(newFile, "w")
+ fp.write(contents)
+ fp.close()
def usage():
- print("")
- print("Usage:")
- print("ModuleWizard [--template ] [--templateKey ] [--target ] ")
- print(" --template default ./Extensions/Testing/LoadableExtensionTemplate")
- print(" --templateKey default is dirname of template")
- print(" --target default ./Modules/Loadable/")
- print("Examples (from Slicer source directory):")
- print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/LoadableExtensionTemplate --target ../MyExtension MyExtension")
- print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate --target ../MyScript MyScript")
- print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/EditorExtensionTemplate --target ../MyEditorEffect MyEditorEffect")
- print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/CLIExtensionTemplate --target ../MyCLI MyCLI")
- print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/SuperBuildExtensionTemplate --target ../MySuperBuild MySuperBuild")
- print("")
+ print("")
+ print("Usage:")
+ print("ModuleWizard [--template ] [--templateKey ] [--target ] ")
+ print(" --template default ./Extensions/Testing/LoadableExtensionTemplate")
+ print(" --templateKey default is dirname of template")
+ print(" --target default ./Modules/Loadable/")
+ print("Examples (from Slicer source directory):")
+ print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/LoadableExtensionTemplate --target ../MyExtension MyExtension")
+ print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/ScriptedLoadableExtensionTemplate --target ../MyScript MyScript")
+ print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/EditorExtensionTemplate --target ../MyEditorEffect MyEditorEffect")
+ print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/CLIExtensionTemplate --target ../MyCLI MyCLI")
+ print(" ./Utilities/Scripts/ModuleWizard.py --template ./Extensions/Testing/SuperBuildExtensionTemplate --target ../MySuperBuild MySuperBuild")
+ print("")
def main(argv):
- template = ""
- templateKey = ""
- target = ""
- moduleName = ""
-
- while argv != []:
- arg = argv.pop(0)
- if arg == "--template":
- template = argv.pop(0)
- continue
- if arg == "--templateKey":
- templateKey = argv.pop(0)
- continue
- if arg == "--target":
- target = argv.pop(0)
- continue
- if arg == "--help":
- usage()
- exit()
- moduleName = arg
-
- if moduleName == "":
- print("Please specify module name")
- usage()
- exit()
-
- if template == "":
- template = "Extensions/Testing/LoadableExtensionTemplate/"
- if template[-1] != '/':
- template += '/'
-
- if templateKey == "":
- templateKey = os.path.split(template[:-1])[-1]
-
- if target == "":
- target = "Modules/Loadable/" + moduleName
-
- if os.path.exists(target):
- print((target, "exists - delete it first"))
- exit()
-
- if not os.path.exists(template):
- print((template, "does not exist - run from Slicer source dir or specify with --template"))
- usage()
- exit()
-
- print(f"\nWill copy \n\t{template} \nto \n\t{target} \nreplacing \"{templateKey}\" with \"{moduleName}\"\n")
- sources = findSource(template)
- print(sources)
-
- for file in sources:
- copyAndReplace(file, template, target, templateKey, moduleName)
-
- print('\nModule %s created!' % moduleName)
+ template = ""
+ templateKey = ""
+ target = ""
+ moduleName = ""
+
+ while argv != []:
+ arg = argv.pop(0)
+ if arg == "--template":
+ template = argv.pop(0)
+ continue
+ if arg == "--templateKey":
+ templateKey = argv.pop(0)
+ continue
+ if arg == "--target":
+ target = argv.pop(0)
+ continue
+ if arg == "--help":
+ usage()
+ exit()
+ moduleName = arg
+
+ if moduleName == "":
+ print("Please specify module name")
+ usage()
+ exit()
+
+ if template == "":
+ template = "Extensions/Testing/LoadableExtensionTemplate/"
+ if template[-1] != '/':
+ template += '/'
+
+ if templateKey == "":
+ templateKey = os.path.split(template[:-1])[-1]
+
+ if target == "":
+ target = "Modules/Loadable/" + moduleName
+
+ if os.path.exists(target):
+ print((target, "exists - delete it first"))
+ exit()
+
+ if not os.path.exists(template):
+ print((template, "does not exist - run from Slicer source dir or specify with --template"))
+ usage()
+ exit()
+
+ print(f"\nWill copy \n\t{template} \nto \n\t{target} \nreplacing \"{templateKey}\" with \"{moduleName}\"\n")
+ sources = findSource(template)
+ print(sources)
+
+ for file in sources:
+ copyAndReplace(file, template, target, templateKey, moduleName)
+
+ print('\nModule %s created!' % moduleName)
if __name__ == "__main__":
- main(sys.argv[1:])
+ main(sys.argv[1:])
diff --git a/Utilities/Scripts/SEMToMediaWiki.py b/Utilities/Scripts/SEMToMediaWiki.py
index a44cb5328be..abd64e3125f 100644
--- a/Utilities/Scripts/SEMToMediaWiki.py
+++ b/Utilities/Scripts/SEMToMediaWiki.py
@@ -28,7 +28,7 @@ def getThisNodesInfoAsText(currentNode, label):
Only get the text info for the matching label at this level of the tree
"""
labelNodeList = [node for node in
- currentNode.childNodes if node.nodeName == label]
+ currentNode.childNodes if node.nodeName == label]
if len(labelNodeList) > 0:
labelNode = labelNodeList[0] # Only get the first one
@@ -44,7 +44,7 @@ def getLongFlagDefinition(currentNode):
if labelNodeList.length > 0:
labelNode = labelNodeList[0] # Only get the first one
return "{}{}{}".format("[--",
- getTextValuesFromNode(labelNode.childNodes), "]")
+ getTextValuesFromNode(labelNode.childNodes), "]")
return ""
@@ -56,7 +56,7 @@ def getFlagDefinition(currentNode):
if labelNodeList.length > 0:
labelNode = labelNodeList[0] # Only get the first one
return "{}{}{}".format("[-",
- getTextValuesFromNode(labelNode.childNodes), "]")
+ getTextValuesFromNode(labelNode.childNodes), "]")
return ""
@@ -68,7 +68,7 @@ def getLabelDefinition(currentNode):
if labelNodeList.length > 0:
labelNode = labelNodeList[0] # Only get the first one
return "{}{}{}".format("** '''",
- getTextValuesFromNode(labelNode.childNodes), "'''")
+ getTextValuesFromNode(labelNode.childNodes), "'''")
return ""
@@ -80,7 +80,7 @@ def getDefaultValueDefinition(currentNode):
if labelNodeList.length > 0:
labelNode = labelNodeList[0] # Only get the first one
return "{}{}{}".format("''Default value: ",
- getTextValuesFromNode(labelNode.childNodes), "''")
+ getTextValuesFromNode(labelNode.childNodes), "''")
return ""
@@ -91,7 +91,7 @@ def GetSEMDoc(filename):
"""
doc = xml.dom.minidom.parse(filename)
executableNode = [node for node in doc.childNodes if
- node.nodeName == "executable"]
+ node.nodeName == "executable"]
# Only use the first
return executableNode[0]
@@ -185,7 +185,7 @@ def DumpSEMMediaWikiFeatures(executableNode):
outRegion = ""
outRegion += "===Quick Tour of Features and Use===\n\n"
outRegion += "{}{}".format("A list panels in the interface,",
- " their features, what they mean, and how to use them.\n")
+ " their features, what they mean, and how to use them.\n")
outRegion += "{|\n|\n"
# Now print all the command line arguments and the labels
# that showup in the GUI interface
@@ -201,23 +201,23 @@ def DumpSEMMediaWikiFeatures(executableNode):
# if this node has a default value -- document it!
if getThisNodesInfoAsText(currentNode, "default") != "":
outRegion += "{} {} {}: {} {}\n".format(
- getLabelDefinition(currentNode),
- getLongFlagDefinition(currentNode),
- getFlagDefinition(currentNode),
- getThisNodesInfoAsText(currentNode,
- "description"),
- getDefaultValueDefinition(currentNode))
+ getLabelDefinition(currentNode),
+ getLongFlagDefinition(currentNode),
+ getFlagDefinition(currentNode),
+ getThisNodesInfoAsText(currentNode,
+ "description"),
+ getDefaultValueDefinition(currentNode))
else:
outRegion += "{} {} {}: {}\n\n".format(
- getLabelDefinition(currentNode),
- getLongFlagDefinition(currentNode),
- getFlagDefinition(currentNode),
- getThisNodesInfoAsText(currentNode,
- "description"))
+ getLabelDefinition(currentNode),
+ getLongFlagDefinition(currentNode),
+ getFlagDefinition(currentNode),
+ getThisNodesInfoAsText(currentNode,
+ "description"))
currentNode = currentNode.nextSibling
outRegion += "{}{}\n".format("|[[Image:screenshotBlankNotOptional.png|",
- "thumb|280px|User Interface]]")
+ "thumb|280px|User Interface]]")
outRegion += "|}\n\n"
return outRegion
@@ -287,15 +287,15 @@ def SEMToMediaWikiProg():
version = "%prog v0.1"
parser = OptionParser()
parser.add_option("-x", "--xmlfile", dest="xmlfilename",
- action="store", type="string",
- metavar="XMLFILE", help="The SEM formatted XMLFILE file")
+ action="store", type="string",
+ metavar="XMLFILE", help="The SEM formatted XMLFILE file")
parser.add_option("-o", "--outfile", dest="outfilename",
- action="store", type="string", default=None,
- metavar="MEDIAWIKIFILE",
- help="The MEDIAWIKIFILE ascii file with media-wiki formatted text.")
+ action="store", type="string", default=None,
+ metavar="MEDIAWIKIFILE",
+ help="The MEDIAWIKIFILE ascii file with media-wiki formatted text.")
parser.add_option("-p", "--parts", dest="parts",
- action="store", type="string", default="hbf",
- help="The parts to print out, h=Header,b=body,f=footer")
+ action="store", type="string", default="hbf",
+ help="The parts to print out, h=Header,b=body,f=footer")
parser.epilog = program_description
# print program_description
(options, args) = parser.parse_args()
diff --git a/Utilities/Scripts/SlicerWizard/CMakeParser.py b/Utilities/Scripts/SlicerWizard/CMakeParser.py
index 09e2a722f53..e25f7faa79a 100644
--- a/Utilities/Scripts/SlicerWizard/CMakeParser.py
+++ b/Utilities/Scripts/SlicerWizard/CMakeParser.py
@@ -25,340 +25,340 @@
# =============================================================================
class Token:
- """Base class for CMake script tokens.
+ """Base class for CMake script tokens.
- This is the base class for CMake script tokens. An occurrence of a token
- whose type is exactly :class:`.Token` (i.e. not a subclass thereof) is a
- syntactic error unless the token text is empty.
+ This is the base class for CMake script tokens. An occurrence of a token
+ whose type is exactly :class:`.Token` (i.e. not a subclass thereof) is a
+ syntactic error unless the token text is empty.
- .. attribute:: text
+ .. attribute:: text
- The textual content of the token.
+ The textual content of the token.
- .. attribute:: indent
+ .. attribute:: indent
- The whitespace (including newlines) which preceded the token. As the parser
- is strictly preserving of whitespace, note that this must be non-empty in
- many cases in order to produce a syntactically correct script.
- """
+ The whitespace (including newlines) which preceded the token. As the parser
+ is strictly preserving of whitespace, note that this must be non-empty in
+ many cases in order to produce a syntactically correct script.
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, text, indent=""):
- self.text = text
- self.indent = indent
+ # ---------------------------------------------------------------------------
+ def __init__(self, text, indent=""):
+ self.text = text
+ self.indent = indent
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return "Token(text=%(text)r, indent=%(indent)r)" % self.__dict__
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "Token(text=%(text)r, indent=%(indent)r)" % self.__dict__
- # ---------------------------------------------------------------------------
- def __str__(self):
- return self.indent + self.text
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return self.indent + self.text
# =============================================================================
class String(Token):
- """String token.
+ """String token.
- .. attribute:: text
+ .. attribute:: text
- The textual content of the string. Note that escapes are not evaluated and
- will appear in their raw (escaped) form.
+ The textual content of the string. Note that escapes are not evaluated and
+ will appear in their raw (escaped) form.
- .. attribute:: prefix
+ .. attribute:: prefix
- The delimiter which starts this string. The delimiter may be empty,
- ``'"'``, or a lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.).
+ The delimiter which starts this string. The delimiter may be empty,
+ ``'"'``, or a lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.).
- .. attribute:: suffix
+ .. attribute:: suffix
- The delimiter which ends this string, which shall match the :attr:`prefix`.
+ The delimiter which ends this string, which shall match the :attr:`prefix`.
- String tokens appear as arguments to :class:`.Command`, as they are not valid
- outside of a command context.
- """
+ String tokens appear as arguments to :class:`.Command`, as they are not valid
+ outside of a command context.
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, text, indent="", prefix="", suffix=""):
- text = super().__init__(text, indent)
- self.prefix = prefix
- self.suffix = suffix
+ # ---------------------------------------------------------------------------
+ def __init__(self, text, indent="", prefix="", suffix=""):
+ text = super().__init__(text, indent)
+ self.prefix = prefix
+ self.suffix = suffix
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return "String(prefix=%(prefix)r, suffix=%(suffix)r," \
- " text=%(text)r, indent=%(indent)r)" % self.__dict__
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "String(prefix=%(prefix)r, suffix=%(suffix)r," \
+ " text=%(text)r, indent=%(indent)r)" % self.__dict__
- # ---------------------------------------------------------------------------
- def __str__(self):
- return self.indent + self.prefix + self.text + self.suffix
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return self.indent + self.prefix + self.text + self.suffix
# =============================================================================
class Comment(Token):
- """Comment token.
+ """Comment token.
- .. attribute:: text
+ .. attribute:: text
- The textual content of the comment.
+ The textual content of the comment.
- .. attribute:: prefix
+ .. attribute:: prefix
- The delimiter which starts this comment: ``'#'``, optionally followed by a
- lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.).
+ The delimiter which starts this comment: ``'#'``, optionally followed by a
+ lua-style long bracket (e.g. ``'[['``, ``'[===['``, etc.).
- .. attribute:: suffix
+ .. attribute:: suffix
- The delimiter which ends this comment: either empty, or a lua-style long
- bracket which shall match the long bracket in :attr:`prefix`.
- """
+ The delimiter which ends this comment: either empty, or a lua-style long
+ bracket which shall match the long bracket in :attr:`prefix`.
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, prefix, text, indent="", suffix=""):
- text = super().__init__(text, indent)
- self.prefix = prefix
- self.suffix = suffix
+ # ---------------------------------------------------------------------------
+ def __init__(self, prefix, text, indent="", suffix=""):
+ text = super().__init__(text, indent)
+ self.prefix = prefix
+ self.suffix = suffix
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return "Comment(prefix=%(prefix)r, suffix=%(suffix)r," \
- " text=%(text)r, indent=%(indent)r)" % self.__dict__
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "Comment(prefix=%(prefix)r, suffix=%(suffix)r," \
+ " text=%(text)r, indent=%(indent)r)" % self.__dict__
- # ---------------------------------------------------------------------------
- def __str__(self):
- return self.indent + self.prefix + self.text + self.suffix
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return self.indent + self.prefix + self.text + self.suffix
# =============================================================================
class Command(Token):
- """Command token.
+ """Command token.
- .. attribute:: text
+ .. attribute:: text
- The name of the command.
+ The name of the command.
- .. attribute:: prefix
+ .. attribute:: prefix
- The delimiter which starts the command's argument list. This shall end with
- ``'('`` and may begin with whitespace if there is whitespace separating the
- command name from the '('.
+ The delimiter which starts the command's argument list. This shall end with
+ ``'('`` and may begin with whitespace if there is whitespace separating the
+ command name from the '('.
- .. attribute:: suffix
+ .. attribute:: suffix
- The delimiter which ends the command's argument list. This shall end with
- ``')'`` and may begin with whitespace if there is whitespace separating the
- last argument (or the opening '(' if there are no arguments) from the ')'.
+ The delimiter which ends the command's argument list. This shall end with
+ ``')'`` and may begin with whitespace if there is whitespace separating the
+ last argument (or the opening '(' if there are no arguments) from the ')'.
- .. attribute:: arguments
+ .. attribute:: arguments
- A :class:`list` of :class:`.String` tokens which comprise the arguments of
- the command.
- """
+ A :class:`list` of :class:`.String` tokens which comprise the arguments of
+ the command.
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, text, arguments=[], indent="", prefix="(", suffix=")"):
- text = super().__init__(text, indent)
- self.prefix = prefix
- self.suffix = suffix
- self.arguments = arguments
+ # ---------------------------------------------------------------------------
+ def __init__(self, text, arguments=[], indent="", prefix="(", suffix=")"):
+ text = super().__init__(text, indent)
+ self.prefix = prefix
+ self.suffix = suffix
+ self.arguments = arguments
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return "Command(text=%(text)r, prefix=%(prefix)r," \
- " suffix=%(suffix)r, arguments=%(arguments)r," \
- " indent=%(indent)r)" % self.__dict__
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return "Command(text=%(text)r, prefix=%(prefix)r," \
+ " suffix=%(suffix)r, arguments=%(arguments)r," \
+ " indent=%(indent)r)" % self.__dict__
- # ---------------------------------------------------------------------------
- def __str__(self):
- args = "".join([str(a) for a in self.arguments])
- return self.indent + self.text + self.prefix + args + self.suffix
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ args = "".join([str(a) for a in self.arguments])
+ return self.indent + self.text + self.prefix + args + self.suffix
# =============================================================================
class CMakeScript:
- """Tokenized representation of a CMake script.
+ """Tokenized representation of a CMake script.
- .. attribute:: tokens
+ .. attribute:: tokens
- The :class:`list` of tokens which comprise the script. Manipulations of
- this list should be used to change the content of the script.
- """
+ The :class:`list` of tokens which comprise the script. Manipulations of
+ this list should be used to change the content of the script.
+ """
- _reWhitespace = re.compile(r"\s")
- _reCommand = re.compile(r"([" + string.ascii_letters + r"]\w*)(\s*\()")
- _reComment = re.compile(r"#(\[=*\[)?")
- _reQuote = re.compile("\"")
- _reBracketQuote = re.compile(r"\[=*\[")
- _reEscape = re.compile(r"\\[\\\"nrt$ ]")
+ _reWhitespace = re.compile(r"\s")
+ _reCommand = re.compile(r"([" + string.ascii_letters + r"]\w*)(\s*\()")
+ _reComment = re.compile(r"#(\[=*\[)?")
+ _reQuote = re.compile("\"")
+ _reBracketQuote = re.compile(r"\[=*\[")
+ _reEscape = re.compile(r"\\[\\\"nrt$ ]")
- # ---------------------------------------------------------------------------
- def __init__(self, content):
- """
- :param content: Textual content of a CMake script.
- :type content: :class:`str`
+ # ---------------------------------------------------------------------------
+ def __init__(self, content):
+ """
+ :param content: Textual content of a CMake script.
+ :type content: :class:`str`
- :raises:
- :exc:`~exceptions.SyntaxError` or :exc:`~exceptions.EOFError` if a
- parsing error occurs (i.e. if the input text is not syntactically valid).
+ :raises:
+ :exc:`~exceptions.SyntaxError` or :exc:`~exceptions.EOFError` if a
+ parsing error occurs (i.e. if the input text is not syntactically valid).
- .. code-block:: python
+ .. code-block:: python
- with open('CMakeLists.txt') as input_file:
- script = CMakeParser.CMakeScript(input_file.read())
+ with open('CMakeLists.txt') as input_file:
+ script = CMakeParser.CMakeScript(input_file.read())
- with open('CMakeLists.txt.new', 'w') as output_file:
- output_file.write(str(script))
- """
+ with open('CMakeLists.txt.new', 'w') as output_file:
+ output_file.write(str(script))
+ """
- self.tokens = []
+ self.tokens = []
- self._content = content
- self._match = None
+ self._content = content
+ self._match = None
- while len(self._content):
- indent = self._chompSpace()
+ while len(self._content):
+ indent = self._chompSpace()
- # Consume comments
- if self._is(self._reComment):
- self.tokens.append(self._parseComment(self._match, indent))
+ # Consume comments
+ if self._is(self._reComment):
+ self.tokens.append(self._parseComment(self._match, indent))
- # Consume commands
- elif self._is(self._reCommand):
- self.tokens.append(self._parseCommand(self._match, indent))
+ # Consume commands
+ elif self._is(self._reCommand):
+ self.tokens.append(self._parseCommand(self._match, indent))
- # Consume other tokens (pedantically, if we get here, the script is
- # malformed, except at EOF)
- else:
- m = self._reWhitespace.search(self._content)
- n = m.start() if m is not None else len(self._content)
- self.tokens.append(Token(text=self._content[:n], indent=indent))
- self._content = self._content[n:]
+ # Consume other tokens (pedantically, if we get here, the script is
+ # malformed, except at EOF)
+ else:
+ m = self._reWhitespace.search(self._content)
+ n = m.start() if m is not None else len(self._content)
+ self.tokens.append(Token(text=self._content[:n], indent=indent))
+ self._content = self._content[n:]
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return repr(self.tokens)
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return repr(self.tokens)
- # ---------------------------------------------------------------------------
- def __str__(self):
- return "".join([str(t) for t in self.tokens])
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ return "".join([str(t) for t in self.tokens])
- # ---------------------------------------------------------------------------
- def _chomp(self):
- result = self._content[0]
- self._content = self._content[1:]
- return result
+ # ---------------------------------------------------------------------------
+ def _chomp(self):
+ result = self._content[0]
+ self._content = self._content[1:]
+ return result
- # ---------------------------------------------------------------------------
- def _chompSpace(self):
- result = ""
+ # ---------------------------------------------------------------------------
+ def _chompSpace(self):
+ result = ""
- while len(self._content) and self._content[0].isspace():
- result += self._content[0]
- self._content = self._content[1:]
+ while len(self._content) and self._content[0].isspace():
+ result += self._content[0]
+ self._content = self._content[1:]
- return result
+ return result
- # ---------------------------------------------------------------------------
- def _chompString(self, end, escapes):
- result = ""
+ # ---------------------------------------------------------------------------
+ def _chompString(self, end, escapes):
+ result = ""
- while len(self._content):
- if escapes and self._is(self._reEscape):
- e = self._match.group(0)
- result += e
- self._content = self._content[len(e):]
+ while len(self._content):
+ if escapes and self._is(self._reEscape):
+ e = self._match.group(0)
+ result += e
+ self._content = self._content[len(e):]
- elif self._content.startswith(end):
- self._content = self._content[len(end):]
- return result
+ elif self._content.startswith(end):
+ self._content = self._content[len(end):]
+ return result
- else:
- result += self._chomp()
+ else:
+ result += self._chomp()
- raise EOFError("unexpected EOF while parsing string (expected %r)" % end)
+ raise EOFError("unexpected EOF while parsing string (expected %r)" % end)
- # ---------------------------------------------------------------------------
- def _parseArgument(self, indent):
- text = ""
+ # ---------------------------------------------------------------------------
+ def _parseArgument(self, indent):
+ text = ""
- while len(self._content):
- if self._is(self._reQuote) or self._is(self._reBracketQuote):
- prefix = self._match.group(0)
- self._content = self._content[len(prefix):]
+ while len(self._content):
+ if self._is(self._reQuote) or self._is(self._reBracketQuote):
+ prefix = self._match.group(0)
+ self._content = self._content[len(prefix):]
- if prefix == "\"":
- suffix = prefix
- s = self._chompString(suffix, escapes=True)
+ if prefix == "\"":
+ suffix = prefix
+ s = self._chompString(suffix, escapes=True)
- else:
- suffix = prefix.replace("[", "]")
- s = self._chompString(suffix, escapes=False)
+ else:
+ suffix = prefix.replace("[", "]")
+ s = self._chompString(suffix, escapes=False)
- if not len(text):
- return String(prefix=prefix, suffix=suffix, text=s, indent=indent)
+ if not len(text):
+ return String(prefix=prefix, suffix=suffix, text=s, indent=indent)
- text += prefix + s + suffix
+ text += prefix + s + suffix
- elif self._content[0].isspace():
- break
+ elif self._content[0].isspace():
+ break
- elif self._is(self._reEscape):
- e = self._match.group(0)
- text += e
- self._content = self._content[len(e):]
+ elif self._is(self._reEscape):
+ e = self._match.group(0)
+ text += e
+ self._content = self._content[len(e):]
- elif self._content[0] == ")":
- break
+ elif self._content[0] == ")":
+ break
- else:
- text += self._chomp()
+ else:
+ text += self._chomp()
- return String(text=text, indent=indent)
+ return String(text=text, indent=indent)
- # ---------------------------------------------------------------------------
- def _parseComment(self, match, indent):
- b = match.group(1)
- e = "\n" if b is None else b.replace("[", "]")
- n = self._content.find(e)
- if n < 0:
- raise EOFError("unexpected EOF while parsing comment (expected %r" % e)
+ # ---------------------------------------------------------------------------
+ def _parseComment(self, match, indent):
+ b = match.group(1)
+ e = "\n" if b is None else b.replace("[", "]")
+ n = self._content.find(e)
+ if n < 0:
+ raise EOFError("unexpected EOF while parsing comment (expected %r" % e)
- i = match.end()
- suffix = e.strip()
- token = Comment(prefix=self._content[:i], suffix=suffix,
- text=self._content[i:n], indent=indent)
+ i = match.end()
+ suffix = e.strip()
+ token = Comment(prefix=self._content[:i], suffix=suffix,
+ text=self._content[i:n], indent=indent)
- self._content = self._content[n + len(suffix):]
+ self._content = self._content[n + len(suffix):]
- return token
+ return token
- # ---------------------------------------------------------------------------
- def _parseCommand(self, match, indent):
- command = match.group(1)
- prefix = match.group(2)
- arguments = []
+ # ---------------------------------------------------------------------------
+ def _parseCommand(self, match, indent):
+ command = match.group(1)
+ prefix = match.group(2)
+ arguments = []
- self._content = self._content[match.end():]
+ self._content = self._content[match.end():]
- while len(self._content):
- argIndent = self._chompSpace()
+ while len(self._content):
+ argIndent = self._chompSpace()
- if not len(self._content):
- break
+ if not len(self._content):
+ break
- if self._content[0] == ")":
- self._content = self._content[1:]
- return Command(text=command, arguments=arguments, indent=indent,
- prefix=prefix, suffix=argIndent + ")")
- elif self._is(self._reComment):
- arguments.append(self._parseComment(self._match, argIndent))
+ if self._content[0] == ")":
+ self._content = self._content[1:]
+ return Command(text=command, arguments=arguments, indent=indent,
+ prefix=prefix, suffix=argIndent + ")")
+ elif self._is(self._reComment):
+ arguments.append(self._parseComment(self._match, argIndent))
- else:
- arguments.append(self._parseArgument(argIndent))
+ else:
+ arguments.append(self._parseArgument(argIndent))
- raise EOFError("unexpected EOF while parsing command (expected ')')")
+ raise EOFError("unexpected EOF while parsing command (expected ')')")
- # ---------------------------------------------------------------------------
- def _is(self, regex):
- self._match = regex.match(self._content)
- return self._match is not None
+ # ---------------------------------------------------------------------------
+ def _is(self, regex):
+ self._match = regex.match(self._content)
+ return self._match is not None
diff --git a/Utilities/Scripts/SlicerWizard/ExtensionDescription.py b/Utilities/Scripts/SlicerWizard/ExtensionDescription.py
index 60cd4b0969c..44822a46eff 100644
--- a/Utilities/Scripts/SlicerWizard/ExtensionDescription.py
+++ b/Utilities/Scripts/SlicerWizard/ExtensionDescription.py
@@ -8,305 +8,305 @@
# =============================================================================
class ExtensionDescription:
- """Representation of an extension description.
-
- This class provides a Python object representation of an extension
- description. The extension information is made available as attributes on the
- object. The "well known" attributes are described
- :wikidoc:`Developers/Extensions/DescriptionFile here`. Custom attributes may
- be added with :func:`setattr`. Attributes may be removed with :func:`delattr`
- or the :meth:`.clear` method.
- """
-
- _reParam = re.compile(r"([a-zA-Z][a-zA-Z0-9_]*)\s+(.+)")
-
- DESCRIPTION_FILE_TEMPLATE = None
-
- # ---------------------------------------------------------------------------
- def __init__(self, repo=None, filepath=None, sourcedir=None, cmakefile="CMakeLists.txt"):
- """
- :param repo:
- Extension repository from which to create the description.
- :type repo:
- :class:`git.Repo `,
- :class:`.Subversion.Repository` or ``None``.
- :param filepath:
- Path to an existing ``.s4ext`` to read.
- :type filepath:
- :class:`str` or ``None``.
- :param sourcedir:
- Path to an extension source directory.
- :type sourcedir:
- :class:`str` or ``None``.
- :param cmakefile:
- Name of the CMake file where `EXTENSION_*` CMake variables
- are set. Default is `CMakeLists.txt`.
- :type cmakefile:
- :class:`str`
-
- :raises:
- * :exc:`~exceptions.KeyError` if the extension description is missing a
- required attribute.
- * :exc:`~exceptions.Exception` if there is some other problem
- constructing the description.
-
- The description may be created from a repository instance (in which case
- the description repository information will be populated), a path to the
- extension source directory, or a path to an existing ``.s4ext`` file.
- No more than one of ``repo``, ``filepath`` or ``sourcedir`` may be given.
- If none are provided, the description will be incomplete.
+ """Representation of an extension description.
+
+ This class provides a Python object representation of an extension
+ description. The extension information is made available as attributes on the
+ object. The "well known" attributes are described
+ :wikidoc:`Developers/Extensions/DescriptionFile here`. Custom attributes may
+ be added with :func:`setattr`. Attributes may be removed with :func:`delattr`
+ or the :meth:`.clear` method.
"""
- args = (repo, filepath, sourcedir)
- if args.count(None) < len(args) - 1:
- raise Exception("cannot construct %s: only one of"
- " (repo, filepath, sourcedir) may be given" %
- type(self).__name__)
-
- if filepath is not None:
- with open(filepath) as fp:
- self._read(fp)
-
- elif repo is not None:
- # Handle git repositories
- if hasattr(repo, "remotes"):
- remote = None
- svnRemote = None
-
- # Get SHA of HEAD (may not exist if no commit has been made yet!)
- try:
- sha = repo.head.commit.hexsha
-
- except ValueError:
- sha = "NA"
-
- # Try to get git remote
- try:
- remote = repo.remotes.origin
- except:
- if len(repo.remotes) == 1:
- remote = repo.remotes[0]
-
- if remote is None:
- # Try to get svn remote
- config = repo.config_reader()
- for s in config.sections():
- if s.startswith("svn-remote"):
- svnRemote = s[12:-1]
- break
-
- if svnRemote is None:
- # Do we have any remotes?
- if len(repo.remotes) == 0:
- setattr(self, "scm", "git")
- setattr(self, "scmurl", "NA")
- setattr(self, "scmrevision", sha)
-
- else:
- raise Exception("unable to determine repository's primary remote")
-
- else:
- si = self._gitSvnInfo(repo, svnRemote)
- setattr(self, "scm", "svn")
- setattr(self, "scmurl", si["URL"])
- setattr(self, "scmrevision", si["Revision"])
+ _reParam = re.compile(r"([a-zA-Z][a-zA-Z0-9_]*)\s+(.+)")
+
+ DESCRIPTION_FILE_TEMPLATE = None
+
+ # ---------------------------------------------------------------------------
+ def __init__(self, repo=None, filepath=None, sourcedir=None, cmakefile="CMakeLists.txt"):
+ """
+ :param repo:
+ Extension repository from which to create the description.
+ :type repo:
+ :class:`git.Repo `,
+ :class:`.Subversion.Repository` or ``None``.
+ :param filepath:
+ Path to an existing ``.s4ext`` to read.
+ :type filepath:
+ :class:`str` or ``None``.
+ :param sourcedir:
+ Path to an extension source directory.
+ :type sourcedir:
+ :class:`str` or ``None``.
+ :param cmakefile:
+ Name of the CMake file where `EXTENSION_*` CMake variables
+ are set. Default is `CMakeLists.txt`.
+ :type cmakefile:
+ :class:`str`
+
+ :raises:
+ * :exc:`~exceptions.KeyError` if the extension description is missing a
+ required attribute.
+ * :exc:`~exceptions.Exception` if there is some other problem
+ constructing the description.
+
+ The description may be created from a repository instance (in which case
+ the description repository information will be populated), a path to the
+ extension source directory, or a path to an existing ``.s4ext`` file.
+ No more than one of ``repo``, ``filepath`` or ``sourcedir`` may be given.
+ If none are provided, the description will be incomplete.
+ """
+
+ args = (repo, filepath, sourcedir)
+ if args.count(None) < len(args) - 1:
+ raise Exception("cannot construct %s: only one of"
+ " (repo, filepath, sourcedir) may be given" %
+ type(self).__name__)
+
+ if filepath is not None:
+ with open(filepath) as fp:
+ self._read(fp)
+
+ elif repo is not None:
+ # Handle git repositories
+ if hasattr(repo, "remotes"):
+ remote = None
+ svnRemote = None
+
+ # Get SHA of HEAD (may not exist if no commit has been made yet!)
+ try:
+ sha = repo.head.commit.hexsha
+
+ except ValueError:
+ sha = "NA"
+
+ # Try to get git remote
+ try:
+ remote = repo.remotes.origin
+ except:
+ if len(repo.remotes) == 1:
+ remote = repo.remotes[0]
+
+ if remote is None:
+ # Try to get svn remote
+ config = repo.config_reader()
+ for s in config.sections():
+ if s.startswith("svn-remote"):
+ svnRemote = s[12:-1]
+ break
+
+ if svnRemote is None:
+ # Do we have any remotes?
+ if len(repo.remotes) == 0:
+ setattr(self, "scm", "git")
+ setattr(self, "scmurl", "NA")
+ setattr(self, "scmrevision", sha)
+
+ else:
+ raise Exception("unable to determine repository's primary remote")
+
+ else:
+ si = self._gitSvnInfo(repo, svnRemote)
+ setattr(self, "scm", "svn")
+ setattr(self, "scmurl", si["URL"])
+ setattr(self, "scmrevision", si["Revision"])
+
+ else:
+ setattr(self, "scm", "git")
+ setattr(self, "scmurl", self._remotePublicUrl(remote))
+ setattr(self, "scmrevision", sha)
+
+ sourcedir = repo.working_tree_dir
+
+ # Handle svn repositories
+ elif hasattr(repo, "wc_root"):
+ setattr(self, "scm", "svn")
+ setattr(self, "scmurl", repo.url)
+ setattr(self, "scmrevision", repo.last_change_revision)
+ sourcedir = repo.wc_root
+
+ # Handle local source directory
+ elif hasattr(repo, "relative_directory"):
+ setattr(self, "scm", "local")
+ setattr(self, "scmurl", repo.relative_directory)
+ setattr(self, "scmrevision", "NA")
+ sourcedir = os.path.join(repo.root, repo.relative_directory)
else:
- setattr(self, "scm", "git")
- setattr(self, "scmurl", self._remotePublicUrl(remote))
- setattr(self, "scmrevision", sha)
-
- sourcedir = repo.working_tree_dir
-
- # Handle svn repositories
- elif hasattr(repo, "wc_root"):
- setattr(self, "scm", "svn")
- setattr(self, "scmurl", repo.url)
- setattr(self, "scmrevision", repo.last_change_revision)
- sourcedir = repo.wc_root
-
- # Handle local source directory
- elif hasattr(repo, "relative_directory"):
- setattr(self, "scm", "local")
- setattr(self, "scmurl", repo.relative_directory)
- setattr(self, "scmrevision", "NA")
- sourcedir = os.path.join(repo.root, repo.relative_directory)
-
- else:
- setattr(self, "scm", "local")
- setattr(self, "scmurl", "NA")
- setattr(self, "scmrevision", "NA")
-
- if sourcedir is not None:
- p = ExtensionProject(sourcedir, filename=cmakefile)
- self._setProjectAttribute("homepage", p, required=True)
- self._setProjectAttribute("category", p, required=True)
- self._setProjectAttribute("description", p)
- self._setProjectAttribute("contributors", p)
-
- self._setProjectAttribute("status", p)
- self._setProjectAttribute("enabled", p, default="1")
- self._setProjectAttribute("depends", p, default="NA")
- self._setProjectAttribute("build_subdirectory", p, default=".")
-
- self._setProjectAttribute("iconurl", p)
- self._setProjectAttribute("screenshoturls", p)
-
- if self.scm == "svn":
- self._setProjectAttribute("svnusername", p, elideempty=True)
- self._setProjectAttribute("svnpassword", p, elideempty=True)
-
- # ---------------------------------------------------------------------------
- def __repr__(self):
- return repr(self.__dict__)
-
- # ---------------------------------------------------------------------------
- @staticmethod
- def _remotePublicUrl(remote):
- url = remote.url
- if url.startswith("git@"):
- return url.replace(":", "/").replace("git@", "https://")
-
- return url
-
- # ---------------------------------------------------------------------------
- @staticmethod
- def _gitSvnInfo(repo, remote):
- result = {}
- for l in repo.git.svn('info', R=remote).split("\n"):
- if len(l):
- key, value = l.split(":", 1)
- result[key] = value.strip()
- return result
-
- # ---------------------------------------------------------------------------
- def _setProjectAttribute(self, name, project, default=None, required=False,
- elideempty=False, substitute=True):
-
- if default is None and not required:
- default = ""
-
- v = project.getValue("EXTENSION_" + name.upper(), default, substitute)
-
- if len(v) or not elideempty:
- setattr(self, name, v)
-
- # ---------------------------------------------------------------------------
- def clear(self, attr=None):
- """Remove attributes from the extension description.
-
- :param attr: Name of attribute to remove.
- :type attr: :class:`str` or ``None``
-
- If ``attr`` is not ``None``, this removes the specified attribute from the
- description object, equivalent to calling ``delattr(instance, attr)``. If
- ``attr`` is ``None``, all attributes are removed.
- """
-
- for key in self.__dict__.keys() if attr is None else (attr,):
- delattr(self, key)
-
- # ---------------------------------------------------------------------------
- def _read(self, fp):
- for l in fp:
- m = self._reParam.match(l)
- if m is not None:
- setattr(self, m.group(1), m.group(2).strip())
+ setattr(self, "scm", "local")
+ setattr(self, "scmurl", "NA")
+ setattr(self, "scmrevision", "NA")
+
+ if sourcedir is not None:
+ p = ExtensionProject(sourcedir, filename=cmakefile)
+ self._setProjectAttribute("homepage", p, required=True)
+ self._setProjectAttribute("category", p, required=True)
+ self._setProjectAttribute("description", p)
+ self._setProjectAttribute("contributors", p)
+
+ self._setProjectAttribute("status", p)
+ self._setProjectAttribute("enabled", p, default="1")
+ self._setProjectAttribute("depends", p, default="NA")
+ self._setProjectAttribute("build_subdirectory", p, default=".")
+
+ self._setProjectAttribute("iconurl", p)
+ self._setProjectAttribute("screenshoturls", p)
+
+ if self.scm == "svn":
+ self._setProjectAttribute("svnusername", p, elideempty=True)
+ self._setProjectAttribute("svnpassword", p, elideempty=True)
+
+ # ---------------------------------------------------------------------------
+ def __repr__(self):
+ return repr(self.__dict__)
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def _remotePublicUrl(remote):
+ url = remote.url
+ if url.startswith("git@"):
+ return url.replace(":", "/").replace("git@", "https://")
+
+ return url
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def _gitSvnInfo(repo, remote):
+ result = {}
+ for l in repo.git.svn('info', R=remote).split("\n"):
+ if len(l):
+ key, value = l.split(":", 1)
+ result[key] = value.strip()
+ return result
+
+ # ---------------------------------------------------------------------------
+ def _setProjectAttribute(self, name, project, default=None, required=False,
+ elideempty=False, substitute=True):
+
+ if default is None and not required:
+ default = ""
+
+ v = project.getValue("EXTENSION_" + name.upper(), default, substitute)
+
+ if len(v) or not elideempty:
+ setattr(self, name, v)
+
+ # ---------------------------------------------------------------------------
+ def clear(self, attr=None):
+ """Remove attributes from the extension description.
+
+ :param attr: Name of attribute to remove.
+ :type attr: :class:`str` or ``None``
+
+ If ``attr`` is not ``None``, this removes the specified attribute from the
+ description object, equivalent to calling ``delattr(instance, attr)``. If
+ ``attr`` is ``None``, all attributes are removed.
+ """
+
+ for key in self.__dict__.keys() if attr is None else (attr,):
+ delattr(self, key)
+
+ # ---------------------------------------------------------------------------
+ def _read(self, fp):
+ for l in fp:
+ m = self._reParam.match(l)
+ if m is not None:
+ setattr(self, m.group(1), m.group(2).strip())
+
+ # ---------------------------------------------------------------------------
+ def read(self, path):
+ """Read extension description from directory.
+
+ :param path: Directory containing extension description.
+ :type path: :class:`str`
+
+ :raises:
+ :exc:`~exceptions.IOError` if ``path`` does not contain exactly one
+ extension description file.
+
+ This attempts to read an extension description from the specified ``path``
+ which contains a single extension description (``.s4ext``) file (usually an
+ extension build directory).
+ """
+
+ self.clear()
+
+ descriptionFiles = glob.glob(os.path.join(path, "*.[Ss]4[Ee][Xx][Tt]"))
+ if len(descriptionFiles) < 1:
+ raise OSError("extension description file not found")
+
+ if len(descriptionFiles) > 1:
+ raise OSError("multiple extension description files found")
+
+ with open(descriptionFiles[0]) as fp:
+ self._read(fp)
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def _findOccurences(a_str, sub):
+ start = 0
+ while True:
+ start = a_str.find(sub, start)
+ if start == -1: return
+ yield start
+ start += len(sub)
+
+ # ---------------------------------------------------------------------------
+ def _write(self, fp):
+ # Creation of the map
+ dictio = dict()
+ dictio["scm_type"] = getattr(self, "scm")
+ dictio["scm_url"] = getattr(self, "scmurl")
+ dictio["MY_EXTENSION_WC_REVISION"] = getattr(self, "scmrevision")
+ dictio["MY_EXTENSION_DEPENDS"] = getattr(self, "depends")
+ dictio["MY_EXTENSION_BUILD_SUBDIRECTORY"] = getattr(self, "build_subdirectory")
+ dictio["MY_EXTENSION_HOMEPAGE"] = getattr(self, "homepage")
+ dictio["MY_EXTENSION_CONTRIBUTORS"] = getattr(self, "contributors")
+ dictio["MY_EXTENSION_CATEGORY"] = getattr(self, "category")
+ dictio["MY_EXTENSION_ICONURL"] = getattr(self, "iconurl")
+ dictio["MY_EXTENSION_STATUS"] = getattr(self, "status")
+ dictio["MY_EXTENSION_DESCRIPTION"] = getattr(self, "description")
+ dictio["MY_EXTENSION_SCREENSHOTURLS"] = getattr(self, "screenshoturls")
+ dictio["MY_EXTENSION_ENABLED"] = getattr(self, "enabled")
+
+ if self.DESCRIPTION_FILE_TEMPLATE is not None:
+ extDescriptFile = open(self.DESCRIPTION_FILE_TEMPLATE)
+ for line in extDescriptFile.readlines():
+ if "${" in line:
+ variables = self._findOccurences(line, "$")
+ temp = line
+ for variable in variables:
+ if line[variable] == '$' and line[variable + 1] == '{':
+ var = ""
+ i = variable + 2
+ while line[i] != '}':
+ var += line[i]
+ i += 1
+ temp = temp.replace("${" + var + "}", dictio[var])
+ fp.write(temp)
+ else:
+ fp.write(line)
+ else:
+ logging.warning("failed to generate description file using template")
+ logging.warning("generating description file using fallback method")
+ for key in sorted(self.__dict__):
+ fp.write((f"{key} {getattr(self, key)}").strip() + "\n")
- # ---------------------------------------------------------------------------
- def read(self, path):
- """Read extension description from directory.
+ # ---------------------------------------------------------------------------
+ def write(self, out):
+ """Write extension description to a file or stream.
- :param path: Directory containing extension description.
- :type path: :class:`str`
+ :param out: Stream or path to which to write the description.
+ :type out: :class:`~io.IOBase` or :class:`str`
- :raises:
- :exc:`~exceptions.IOError` if ``path`` does not contain exactly one
- extension description file.
+ This writes the extension description to the specified file path or stream
+ object. This is suitable for producing a ``.s4ext`` file from a description
+ object.
+ """
- This attempts to read an extension description from the specified ``path``
- which contains a single extension description (``.s4ext``) file (usually an
- extension build directory).
- """
+ if hasattr(out, "write") and callable(out.write):
+ self._write(out)
- self.clear()
-
- descriptionFiles = glob.glob(os.path.join(path, "*.[Ss]4[Ee][Xx][Tt]"))
- if len(descriptionFiles) < 1:
- raise OSError("extension description file not found")
-
- if len(descriptionFiles) > 1:
- raise OSError("multiple extension description files found")
-
- with open(descriptionFiles[0]) as fp:
- self._read(fp)
-
- # ---------------------------------------------------------------------------
- @staticmethod
- def _findOccurences(a_str, sub):
- start = 0
- while True:
- start = a_str.find(sub, start)
- if start == -1: return
- yield start
- start += len(sub)
-
- # ---------------------------------------------------------------------------
- def _write(self, fp):
- # Creation of the map
- dictio = dict()
- dictio["scm_type"] = getattr(self, "scm")
- dictio["scm_url"] = getattr(self, "scmurl")
- dictio["MY_EXTENSION_WC_REVISION"] = getattr(self, "scmrevision")
- dictio["MY_EXTENSION_DEPENDS"] = getattr(self, "depends")
- dictio["MY_EXTENSION_BUILD_SUBDIRECTORY"] = getattr(self, "build_subdirectory")
- dictio["MY_EXTENSION_HOMEPAGE"] = getattr(self, "homepage")
- dictio["MY_EXTENSION_CONTRIBUTORS"] = getattr(self, "contributors")
- dictio["MY_EXTENSION_CATEGORY"] = getattr(self, "category")
- dictio["MY_EXTENSION_ICONURL"] = getattr(self, "iconurl")
- dictio["MY_EXTENSION_STATUS"] = getattr(self, "status")
- dictio["MY_EXTENSION_DESCRIPTION"] = getattr(self, "description")
- dictio["MY_EXTENSION_SCREENSHOTURLS"] = getattr(self, "screenshoturls")
- dictio["MY_EXTENSION_ENABLED"] = getattr(self, "enabled")
-
- if self.DESCRIPTION_FILE_TEMPLATE is not None:
- extDescriptFile = open(self.DESCRIPTION_FILE_TEMPLATE)
- for line in extDescriptFile.readlines():
- if "${" in line:
- variables = self._findOccurences(line, "$")
- temp = line
- for variable in variables:
- if line[variable] == '$' and line[variable + 1] == '{':
- var = ""
- i = variable + 2
- while line[i] != '}':
- var += line[i]
- i += 1
- temp = temp.replace("${" + var + "}", dictio[var])
- fp.write(temp)
else:
- fp.write(line)
- else:
- logging.warning("failed to generate description file using template")
- logging.warning("generating description file using fallback method")
- for key in sorted(self.__dict__):
- fp.write((f"{key} {getattr(self, key)}").strip() + "\n")
-
- # ---------------------------------------------------------------------------
- def write(self, out):
- """Write extension description to a file or stream.
-
- :param out: Stream or path to which to write the description.
- :type out: :class:`~io.IOBase` or :class:`str`
-
- This writes the extension description to the specified file path or stream
- object. This is suitable for producing a ``.s4ext`` file from a description
- object.
- """
-
- if hasattr(out, "write") and callable(out.write):
- self._write(out)
-
- else:
- with open(out, "w") as fp:
- self._write(fp)
+ with open(out, "w") as fp:
+ self._write(fp)
diff --git a/Utilities/Scripts/SlicerWizard/ExtensionProject.py b/Utilities/Scripts/SlicerWizard/ExtensionProject.py
index f0aed946474..ee36cf94272 100644
--- a/Utilities/Scripts/SlicerWizard/ExtensionProject.py
+++ b/Utilities/Scripts/SlicerWizard/ExtensionProject.py
@@ -8,401 +8,401 @@
# -----------------------------------------------------------------------------
def _isCommand(token, name):
- return isinstance(token, CMakeParser.Command) and token.text.lower() == name
+ return isinstance(token, CMakeParser.Command) and token.text.lower() == name
# -----------------------------------------------------------------------------
def _trimIndent(indent):
- indent = "\n" + indent
- n = indent.rindex("\n")
- return indent[n:]
+ indent = "\n" + indent
+ n = indent.rindex("\n")
+ return indent[n:]
# =============================================================================
class ExtensionProject:
- """Convenience class for manipulating an extension project.
+ """Convenience class for manipulating an extension project.
- This class provides an additional layer of convenience for users that wish to
- manipulate the CMakeLists.txt of an extension project. The term "build
- script" is used throughout to refer to the CMakeLists.txt so encapsulated.
+ This class provides an additional layer of convenience for users that wish to
+ manipulate the CMakeLists.txt of an extension project. The term "build
+ script" is used throughout to refer to the CMakeLists.txt so encapsulated.
- Modifications to the script are made to the in-memory, parsed representation.
- Use :meth:`.save` to write changes back to disk.
+ Modifications to the script are made to the in-memory, parsed representation.
+ Use :meth:`.save` to write changes back to disk.
- This class may be used as a context manager. When used in this manner, any
- changes made are automatically written back to the project's CMakeLists.txt
- when the context goes out of scope.
- """
+ This class may be used as a context manager. When used in this manner, any
+ changes made are automatically written back to the project's CMakeLists.txt
+ when the context goes out of scope.
+ """
- _moduleInsertPlaceholder = "# NEXT_MODULE"
+ _moduleInsertPlaceholder = "# NEXT_MODULE"
- _referencedVariables = re.compile(r"\$\{([\w_\/\.\+\-]+)\}")
+ _referencedVariables = re.compile(r"\$\{([\w_\/\.\+\-]+)\}")
+
+ # ---------------------------------------------------------------------------
+ def __init__(self, path, encoding=None, filename="CMakeLists.txt", ):
+ """
+ :param path: Top level directory of the extension project.
+ :type path: :class:`str`
+ :param encoding: Encoding of extension CMakeLists.txt.
+ :type encoding: :class:`str` or ``None``
+ :param filename: CMake file to parse. Default is `CMakeLists.txt`.
+ :type filename: :class:`str`
+
+ If ``encoding`` is ``None``, the encoding will be guessed using
+ :meth:`~SlicerWizard.Utilities.detectEncoding`.
+ """
+ cmakeFile = os.path.join(path, filename)
+ if not os.path.exists(cmakeFile):
+ raise OSError("%s not found" % filename)
- # ---------------------------------------------------------------------------
- def __init__(self, path, encoding=None, filename="CMakeLists.txt", ):
- """
- :param path: Top level directory of the extension project.
- :type path: :class:`str`
- :param encoding: Encoding of extension CMakeLists.txt.
- :type encoding: :class:`str` or ``None``
- :param filename: CMake file to parse. Default is `CMakeLists.txt`.
- :type filename: :class:`str`
-
- If ``encoding`` is ``None``, the encoding will be guessed using
- :meth:`~SlicerWizard.Utilities.detectEncoding`.
- """
- cmakeFile = os.path.join(path, filename)
- if not os.path.exists(cmakeFile):
- raise OSError("%s not found" % filename)
-
- self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding)
- try:
- self._scriptPath = cmakeFile
- self.getValue("EXTENSION_HOMEPAGE")
- except KeyError:
- for cmakeFile in self._collect_cmakefiles(path, filename):
self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding)
try:
- self._scriptPath = cmakeFile
- self.getValue("EXTENSION_HOMEPAGE")
- break
+ self._scriptPath = cmakeFile
+ self.getValue("EXTENSION_HOMEPAGE")
except KeyError:
- continue
-
- @staticmethod
- def _collect_cmakefiles(path, filename="CMakeLists.txt"):
- """Return list of `filename` found in `path` at depth=1"""
- cmakeFiles = []
- dirnames = []
- for _, dirnames, _ in os.walk(path):
- break
- for dirname in dirnames:
- cmakeFile = os.path.join(path, dirname, filename)
- if os.path.exists(cmakeFile):
- cmakeFiles.append(cmakeFile)
- return cmakeFiles
-
- # ---------------------------------------------------------------------------
- @staticmethod
- def _parse(cmakeFile, encoding=None):
- with open(cmakeFile, "rb") as fp:
- contents = fp.read()
-
- if encoding is None:
- encoding, confidence = detectEncoding(contents)
-
- if encoding is not None:
- if confidence < 0.5:
- logging.warning("%s: encoding detection confidence is %f:"
- " project contents might be corrupt" %
- (path, confidence))
-
- if encoding is None:
- # If unable to determine encoding, skip unicode conversion... users
- # must not feed any unicode into the script or things will likely break
- # later (e.g. when trying to save the project)
- contents = CMakeParser.CMakeScript(contents)
-
- else:
- # Otherwise, decode the contents into unicode
- contents = contents.decode(encoding)
- contents = CMakeParser.CMakeScript(contents)
-
- return contents, encoding
-
- # ---------------------------------------------------------------------------
- def __enter__(self):
- return self
-
- # ---------------------------------------------------------------------------
- def __exit__(self, exc_type, exc_value, traceback):
- self.save()
-
- # ---------------------------------------------------------------------------
- @property
- def encoding(self):
- """Character encoding of the extension project CMakeLists.txt.
-
- :type: :class:`str` or ``None``
-
- This provides the character encoding of the CMakeLists.txt file from which
- the project instance was created. If the encoding cannot be determined, the
- property will have the value ``None``.
-
- .. 'note' directive needs '\' to span multiple lines!
- .. note:: If ``encoding`` is ``None``, the project information is stored \
- as raw bytes using :class:`str`. In such case, passing a \
- non-ASCII :class:`unicode` to any method or property \
- assignment that modifies the project may make it impossible to \
- write the project back to disk.
- """
-
- return self._encoding
-
- # ---------------------------------------------------------------------------
- @property
- def project(self):
- """Name of extension project.
-
- :type: :class:`str`
+ for cmakeFile in self._collect_cmakefiles(path, filename):
+ self._scriptContents, self._encoding = self._parse(cmakeFile, encoding=encoding)
+ try:
+ self._scriptPath = cmakeFile
+ self.getValue("EXTENSION_HOMEPAGE")
+ break
+ except KeyError:
+ continue
+
+ @staticmethod
+ def _collect_cmakefiles(path, filename="CMakeLists.txt"):
+ """Return list of `filename` found in `path` at depth=1"""
+ cmakeFiles = []
+ dirnames = []
+ for _, dirnames, _ in os.walk(path):
+ break
+ for dirname in dirnames:
+ cmakeFile = os.path.join(path, dirname, filename)
+ if os.path.exists(cmakeFile):
+ cmakeFiles.append(cmakeFile)
+ return cmakeFiles
+
+ # ---------------------------------------------------------------------------
+ @staticmethod
+ def _parse(cmakeFile, encoding=None):
+ with open(cmakeFile, "rb") as fp:
+ contents = fp.read()
+
+ if encoding is None:
+ encoding, confidence = detectEncoding(contents)
+
+ if encoding is not None:
+ if confidence < 0.5:
+ logging.warning("%s: encoding detection confidence is %f:"
+ " project contents might be corrupt" %
+ (path, confidence))
+
+ if encoding is None:
+ # If unable to determine encoding, skip unicode conversion... users
+ # must not feed any unicode into the script or things will likely break
+ # later (e.g. when trying to save the project)
+ contents = CMakeParser.CMakeScript(contents)
+
+ else:
+ # Otherwise, decode the contents into unicode
+ contents = contents.decode(encoding)
+ contents = CMakeParser.CMakeScript(contents)
+
+ return contents, encoding
+
+ # ---------------------------------------------------------------------------
+ def __enter__(self):
+ return self
+
+ # ---------------------------------------------------------------------------
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.save()
+
+ # ---------------------------------------------------------------------------
+ @property
+ def encoding(self):
+ """Character encoding of the extension project CMakeLists.txt.
+
+ :type: :class:`str` or ``None``
+
+ This provides the character encoding of the CMakeLists.txt file from which
+ the project instance was created. If the encoding cannot be determined, the
+ property will have the value ``None``.
+
+ .. 'note' directive needs '\' to span multiple lines!
+ .. note:: If ``encoding`` is ``None``, the project information is stored \
+ as raw bytes using :class:`str`. In such case, passing a \
+ non-ASCII :class:`unicode` to any method or property \
+ assignment that modifies the project may make it impossible to \
+ write the project back to disk.
+ """
+
+ return self._encoding
+
+ # ---------------------------------------------------------------------------
+ @property
+ def project(self):
+ """Name of extension project.
+
+ :type: :class:`str`
+
+ :raises:
+ :exc:`~exceptions.EOFError` if no ``project()`` command is present in the
+ build script.
+
+ This provides the name of the extension project, i.e. the identifier passed
+ to ``project()`` in the extension's build script.
+
+ Assigning the property modifies the build script.
+ """
+
+ for t in self._scriptContents.tokens:
+ if _isCommand(t, "project") and len(t.arguments):
+ return t.arguments[0].text
+
+ # Support older extension that do not call "project(Name)"
+ # in top-level CMakeLists.txt
+ try:
+ return self.getValue("EXTENSION_NAME")
+ except KeyError:
+ pass
- :raises:
- :exc:`~exceptions.EOFError` if no ``project()`` command is present in the
- build script.
+ raise EOFError("could not find project")
- This provides the name of the extension project, i.e. the identifier passed
- to ``project()`` in the extension's build script.
+ # ---------------------------------------------------------------------------
+ @project.setter
+ def project(self, value):
- Assigning the property modifies the build script.
- """
+ for t in self._scriptContents.tokens:
+ if _isCommand(t, "project"):
+ if len(t.arguments):
+ t.arguments[0].text = value
+ else:
+ t.arguments.append(CMakeParser.String(text=value))
- for t in self._scriptContents.tokens:
- if _isCommand(t, "project") and len(t.arguments):
- return t.arguments[0].text
+ return
- # Support older extension that do not call "project(Name)"
- # in top-level CMakeLists.txt
- try:
- return self.getValue("EXTENSION_NAME")
- except KeyError:
- pass
+ raise EOFError("could not find project")
- raise EOFError("could not find project")
+ # ---------------------------------------------------------------------------
+ def substituteVariableReferences(self, text):
+ """Return a copy of ``text`` where all valid '``${var}``' occurrences
+ have been replaced.
- # ---------------------------------------------------------------------------
- @project.setter
- def project(self, value):
+ Note that variable references can nest and are evaluated from the inside
+ out, e.g. '``${outer_${inner_variable}_variable}``'.
- for t in self._scriptContents.tokens:
- if _isCommand(t, "project"):
- if len(t.arguments):
- t.arguments[0].text = value
- else:
- t.arguments.append(CMakeParser.String(text=value))
+ :param text: A text with zero or more variable references.
+ :type text: :class:`str`
+ """
- return
+ def _substitue(text):
+ variableNames = self._referencedVariables.findall(text)
+ if len(variableNames) == 0:
+ return text
- raise EOFError("could not find project")
+ prefinedVariables = {}
+ try:
+ prefinedVariables["PROJECT_NAME"] = self.project
+ except EOFError:
+ pass
- # ---------------------------------------------------------------------------
- def substituteVariableReferences(self, text):
- """Return a copy of ``text`` where all valid '``${var}``' occurrences
- have been replaced.
+ for name in prefinedVariables.keys():
+ try:
+ text = text.replace("${%s}" % name, prefinedVariables[name])
+ except KeyError:
+ continue
- Note that variable references can nest and are evaluated from the inside
- out, e.g. '``${outer_${inner_variable}_variable}``'.
+ for name in variableNames:
+ try:
+ text = text.replace("${%s}" % name, self.getValue(name))
+ except KeyError:
+ text = text.replace("${%s}" % name, "%s-NOTFOUND" % name)
- :param text: A text with zero or more variable references.
- :type text: :class:`str`
- """
+ return text
- def _substitue(text):
- variableNames = self._referencedVariables.findall(text)
- if len(variableNames) == 0:
+ while len(self._referencedVariables.findall(text)) > 0:
+ text = _substitue(text)
return text
- prefinedVariables = {}
- try:
- prefinedVariables["PROJECT_NAME"] = self.project
- except EOFError:
- pass
+ # ---------------------------------------------------------------------------
+ def getValue(self, name, default=None, substitute=False):
+ """Get value of CMake variable set in project.
- for name in prefinedVariables.keys():
- try:
- text = text.replace("${%s}" % name, prefinedVariables[name])
- except KeyError:
- continue
+ :param name: Name of the variable.
+ :type name: :class:`str`
+ :param default: Value to return if no such variable exists.
+ :param substitute: If ``True``, expand variable references in value.
+ :type substitute: :class:`bool`
- for name in variableNames:
- try:
- text = text.replace("${%s}" % name, self.getValue(name))
- except KeyError:
- text = text.replace("${%s}" % name, "%s-NOTFOUND" % name)
+ :returns: Value of the variable, or ``default`` if not set.
+ :rtype: :class:`str` or ``type(default)``
- return text
+ :raises:
+ :exc:`~exceptions.KeyError` if no such ``name`` is set and ``default`` is
+ ``None``.
- while len(self._referencedVariables.findall(text)) > 0:
- text = _substitue(text)
- return text
+ This returns the raw value of the variable ``name`` which is set in the
+ build script. By default, no substitution is performed (the result is taken
+ from the raw argument). If more than one ``set()`` command sets the same
+ ``name``, the result is the raw argument to the last such command. If the
+ value consists of more than one argument, they are all concatenated together
+ while stripping newlines, tabs and extra whitespaces.
+
+ If ``substitute`` is ``True``, each occurrence of '``${var}``' will be
+ replaced with the corresponding variable if it has been set. Variable
+ references can nest and are evaluated from the inside out,
+ e.g. '``${outer_${inner_variable}_variable}``'. If a variable
+ reference is not found, it will be replaced with '``-NOTFOUND``'.
- # ---------------------------------------------------------------------------
- def getValue(self, name, default=None, substitute=False):
- """Get value of CMake variable set in project.
+ If no ``set()`` command sets ``name``, and ``default`` is not ``None``,
+ ``default`` is returned. Otherwise a :exc:`~exceptions.KeyError` is raised.
+
+ .. note::
+
+ Variables set using a nested reference are not supported.
+ For example, if the underlying CMake code is ``set(foo \"world\")``
+ and ``set(hello_${foo} \"earth\")``. Occurrences of
+ '``${hello_${foo}}``' will be replaced by '``hello_world-NOTFOUND``'
+
+ .. seealso:: :func:`.substituteVariableReferences`
+ """
+
+ for t in reversed(self._scriptContents.tokens):
+ if _isCommand(t, "set") and len(t.arguments) and \
+ t.arguments[0].text == name:
+ if len(t.arguments) < 2:
+ return None
+ value = " ".join([argument.text for argument in t.arguments[1:] if isinstance(argument, CMakeParser.String)])
+ if substitute:
+ value = self.substituteVariableReferences(value)
+ return value
- :param name: Name of the variable.
- :type name: :class:`str`
- :param default: Value to return if no such variable exists.
- :param substitute: If ``True``, expand variable references in value.
- :type substitute: :class:`bool`
+ if default is not None:
+ return default
- :returns: Value of the variable, or ``default`` if not set.
- :rtype: :class:`str` or ``type(default)``
+ raise KeyError("script does not set %r" % name)
- :raises:
- :exc:`~exceptions.KeyError` if no such ``name`` is set and ``default`` is
- ``None``.
+ # ---------------------------------------------------------------------------
+ def setValue(self, name, value):
+ """Change value of CMake variable set in project.
- This returns the raw value of the variable ``name`` which is set in the
- build script. By default, no substitution is performed (the result is taken
- from the raw argument). If more than one ``set()`` command sets the same
- ``name``, the result is the raw argument to the last such command. If the
- value consists of more than one argument, they are all concatenated together
- while stripping newlines, tabs and extra whitespaces.
+ :param name: Name of the variable.
+ :type name: :class:`str`
+ :param value: Value to assign to the variable.
+ :type value: :class:`str`
- If ``substitute`` is ``True``, each occurrence of '``${var}``' will be
- replaced with the corresponding variable if it has been set. Variable
- references can nest and are evaluated from the inside out,
- e.g. '``${outer_${inner_variable}_variable}``'. If a variable
- reference is not found, it will be replaced with '``-NOTFOUND``'.
+ :raises: :exc:`~exceptions.KeyError` if no such ``name`` is set.
- If no ``set()`` command sets ``name``, and ``default`` is not ``None``,
- ``default`` is returned. Otherwise a :exc:`~exceptions.KeyError` is raised.
+ This modifies the build script to set the variable ``name`` to ``value``.
+ If more than one ``set()`` command sets the same ``name``, only the first
+ is modified. If the value of the modified ``set()`` command has more than
+ one argument, only the first is modified.
- .. note::
+ The build script must already contain a ``set()`` command which sets
+ ``name``. If it does not, a :exc:`~exceptions.KeyError` is raised.
+ """
- Variables set using a nested reference are not supported.
- For example, if the underlying CMake code is ``set(foo \"world\")``
- and ``set(hello_${foo} \"earth\")``. Occurrences of
- '``${hello_${foo}}``' will be replaced by '``hello_world-NOTFOUND``'
+ for t in self._scriptContents.tokens:
+ if _isCommand(t, "set") and len(t.arguments) and \
+ t.arguments[0].text == name:
+ if len(t.arguments) < 2:
+ t.arguments.append(CMakeParser.String(text=value, indent=" ",
+ prefix="\"", suffix="\""))
- .. seealso:: :func:`.substituteVariableReferences`
- """
-
- for t in reversed(self._scriptContents.tokens):
- if _isCommand(t, "set") and len(t.arguments) and \
- t.arguments[0].text == name:
- if len(t.arguments) < 2:
- return None
- value = " ".join([argument.text for argument in t.arguments[1:] if isinstance(argument, CMakeParser.String)])
- if substitute:
- value = self.substituteVariableReferences(value)
- return value
+ else:
+ varg = t.arguments[1]
+ varg.text = value
+ varg.prefix = "\""
+ varg.suffix = "\""
- if default is not None:
- return default
+ return
- raise KeyError("script does not set %r" % name)
+ raise KeyError("script does not set %r" % name)
- # ---------------------------------------------------------------------------
- def setValue(self, name, value):
- """Change value of CMake variable set in project.
+ # ---------------------------------------------------------------------------
+ def addModule(self, name):
+ """Add a module to the build rules of the project.
- :param name: Name of the variable.
- :type name: :class:`str`
- :param value: Value to assign to the variable.
- :type value: :class:`str`
+ :param name: Name of the module to be added.
+ :type name: :class:`str`
+
+ :raises: :exc:`~exceptions.EOFError` if no insertion point can be found.
- :raises: :exc:`~exceptions.KeyError` if no such ``name`` is set.
+ This adds an ``add_subdirectory()`` call for ``name`` to the build script.
+ If possible, the new call is inserted immediately before a placeholder
+ comment which is designated for this purpose. Otherwise, the new call is
+ inserted after the last existing call to ``add_subdirectory()``.
+ """
- This modifies the build script to set the variable ``name`` to ``value``.
- If more than one ``set()`` command sets the same ``name``, only the first
- is modified. If the value of the modified ``set()`` command has more than
- one argument, only the first is modified.
+ indent = ""
+ after = -1
- The build script must already contain a ``set()`` command which sets
- ``name``. If it does not, a :exc:`~exceptions.KeyError` is raised.
- """
+ for n in range(len(self._scriptContents.tokens)):
+ t = self._scriptContents.tokens[n]
- for t in self._scriptContents.tokens:
- if _isCommand(t, "set") and len(t.arguments) and \
- t.arguments[0].text == name:
- if len(t.arguments) < 2:
- t.arguments.append(CMakeParser.String(text=value, indent=" ",
- prefix="\"", suffix="\""))
-
- else:
- varg = t.arguments[1]
- varg.text = value
- varg.prefix = "\""
- varg.suffix = "\""
+ if isinstance(t, CMakeParser.Comment) and \
+ t.text.startswith(self._moduleInsertPlaceholder):
+ indent = t.indent
+ after = n
+ t.indent = _trimIndent(t.indent)
+ break
- return
+ if _isCommand(t, "add_subdirectory"):
+ indent = _trimIndent(t.indent)
+ after = n + 1
- raise KeyError("script does not set %r" % name)
+ if after < 0:
+ raise EOFError("failed to find insertion point for module")
- # ---------------------------------------------------------------------------
- def addModule(self, name):
- """Add a module to the build rules of the project.
-
- :param name: Name of the module to be added.
- :type name: :class:`str`
-
- :raises: :exc:`~exceptions.EOFError` if no insertion point can be found.
-
- This adds an ``add_subdirectory()`` call for ``name`` to the build script.
- If possible, the new call is inserted immediately before a placeholder
- comment which is designated for this purpose. Otherwise, the new call is
- inserted after the last existing call to ``add_subdirectory()``.
- """
+ arguments = [CMakeParser.String(text=name)]
+ t = CMakeParser.Command(text="add_subdirectory", arguments=arguments,
+ indent=indent)
+ self._scriptContents.tokens.insert(after, t)
- indent = ""
- after = -1
+ # ---------------------------------------------------------------------------
+ def save(self, destination=None, encoding=None):
+ """Save the project.
- for n in range(len(self._scriptContents.tokens)):
- t = self._scriptContents.tokens[n]
+ :param destination: Location to which to write the build script.
+ :type destination: :class:`str` or ``None``
+ :param encoding: Encoding with which to write the build script.
+ :type destination: :class:`str` or ``None``
- if isinstance(t, CMakeParser.Comment) and \
- t.text.startswith(self._moduleInsertPlaceholder):
- indent = t.indent
- after = n
- t.indent = _trimIndent(t.indent)
- break
+ This saves the extension project CMake script to the specified file:
- if _isCommand(t, "add_subdirectory"):
- indent = _trimIndent(t.indent)
- after = n + 1
+ .. code-block:: python
- if after < 0:
- raise EOFError("failed to find insertion point for module")
+ # Open a project
+ p = ExtensionProject('.')
- arguments = [CMakeParser.String(text=name)]
- t = CMakeParser.Command(text="add_subdirectory", arguments=arguments,
- indent=indent)
- self._scriptContents.tokens.insert(after, t)
+ # Set a value in the project
+ p.setValue('EXTENSION_DESCRIPTION', 'This is an awesome extension!')
- # ---------------------------------------------------------------------------
- def save(self, destination=None, encoding=None):
- """Save the project.
+ # Save the changes
+ p.save()
- :param destination: Location to which to write the build script.
- :type destination: :class:`str` or ``None``
- :param encoding: Encoding with which to write the build script.
- :type destination: :class:`str` or ``None``
+ If ``destination`` is ``None``, the CMakeLists.txt file from which the
+ project instance was created is overwritten. Similarly, if ``encoding`` is
+ ``None``, the file is written with the original encoding of the
+ CMakeLists.txt file from which the project instance was created, if such
+ encoding is other than ASCII; otherwise the file is written in UTF-8.
+ """
- This saves the extension project CMake script to the specified file:
+ if destination is None:
+ destination = self._scriptPath
- .. code-block:: python
+ if encoding is None and self.encoding is not None:
+ encoding = self.encoding if self.encoding.lower() != "ascii" else "utf-8"
- # Open a project
- p = ExtensionProject('.')
+ if encoding is None:
+ # If no encoding is specified and we don't know the original encoding,
+ # perform no conversion and hope for the best (will only work if there
+ # are no unicode instances in the script)
+ with open(destination, "w") as fp:
+ fp.write(str(self._scriptContents))
- # Set a value in the project
- p.setValue('EXTENSION_DESCRIPTION', 'This is an awesome extension!')
-
- # Save the changes
- p.save()
-
- If ``destination`` is ``None``, the CMakeLists.txt file from which the
- project instance was created is overwritten. Similarly, if ``encoding`` is
- ``None``, the file is written with the original encoding of the
- CMakeLists.txt file from which the project instance was created, if such
- encoding is other than ASCII; otherwise the file is written in UTF-8.
- """
-
- if destination is None:
- destination = self._scriptPath
-
- if encoding is None and self.encoding is not None:
- encoding = self.encoding if self.encoding.lower() != "ascii" else "utf-8"
-
- if encoding is None:
- # If no encoding is specified and we don't know the original encoding,
- # perform no conversion and hope for the best (will only work if there
- # are no unicode instances in the script)
- with open(destination, "w") as fp:
- fp.write(str(self._scriptContents))
-
- else:
- # Otherwise, write the file using full encoding conversion
- with open(destination, "wb") as fp:
- fp.write(str(self._scriptContents).encode(encoding))
+ else:
+ # Otherwise, write the file using full encoding conversion
+ with open(destination, "wb") as fp:
+ fp.write(str(self._scriptContents).encode(encoding))
diff --git a/Utilities/Scripts/SlicerWizard/ExtensionWizard.py b/Utilities/Scripts/SlicerWizard/ExtensionWizard.py
index 077fae5b33a..37f773ea666 100644
--- a/Utilities/Scripts/SlicerWizard/ExtensionWizard.py
+++ b/Utilities/Scripts/SlicerWizard/ExtensionWizard.py
@@ -10,31 +10,31 @@
# -----------------------------------------------------------------------------
def haveGit():
- """Return True if git is available.
-
- A side effect of `import git` is that it shows a popup window on
- macOS, asking the user to install XCode (if git is not installed already),
- therefore this method should only be called if git is actually needed.
- """
-
- # If Python is not built with SSL support then do not even try to import
- # GithubHelper (it would throw missing attribute error for HTTPSConnection)
- import http.client
- if hasattr(http.client, "HTTPSConnection"):
- # SSL is available
- try:
- global git, GithubHelper, NotSet
- import git # noqa: F401
- from . import GithubHelper
- from .GithubHelper import NotSet
- _haveGit = True
- except ImportError:
- _haveGit = False
- else:
- logging.debug("ExtensionWizard: git support is disabled because http.client.HTTPSConnection is not available")
- _haveGit = False
-
- return _haveGit
+ """Return True if git is available.
+
+ A side effect of `import git` is that it shows a popup window on
+ macOS, asking the user to install XCode (if git is not installed already),
+ therefore this method should only be called if git is actually needed.
+ """
+
+ # If Python is not built with SSL support then do not even try to import
+ # GithubHelper (it would throw missing attribute error for HTTPSConnection)
+ import http.client
+ if hasattr(http.client, "HTTPSConnection"):
+ # SSL is available
+ try:
+ global git, GithubHelper, NotSet
+ import git # noqa: F401
+ from . import GithubHelper
+ from .GithubHelper import NotSet
+ _haveGit = True
+ except ImportError:
+ _haveGit = False
+ else:
+ logging.debug("ExtensionWizard: git support is disabled because http.client.HTTPSConnection is not available")
+ _haveGit = False
+
+ return _haveGit
from . import __version__, __version_info__
@@ -48,297 +48,297 @@ def haveGit():
# =============================================================================
class ExtensionWizard:
- """Implementation class for the Extension Wizard.
-
- This class provides the entry point and implementation of the Extension
- wizard. One normally uses it by writing a small bootstrap script to load the
- module, which then calls code like:
-
- .. code-block:: python
-
- wizard = ExtensionWizard()
- wizard.execute()
-
- Interaction with `GitHub `_ uses
- :func:`.GithubHelper.logIn` to authenticate.
+ """Implementation class for the Extension Wizard.
- .. 'note' directive needs '\' to span multiple lines!
- .. note:: Most methods will signal the application to exit if \
- something goes wrong. This behavior is hidden by the \
- :meth:`~ExtensionWizard.execute` method when passing \
- ``exit=False``; callers that need to continue execution after \
- calling one of the other methods directly should catch \
- :exc:`~exceptions.SystemExit`.
- """
+ This class provides the entry point and implementation of the Extension
+ wizard. One normally uses it by writing a small bootstrap script to load the
+ module, which then calls code like:
- _reModuleInsertPlaceholder = re.compile("(?<=\n)([ \t]*)## NEXT_MODULE")
- _reAddSubdirectory = \
- re.compile("(?<=\n)([ \t]*)add_subdirectory[(][^)]+[)][^\n]*\n")
+ .. code-block:: python
- # ---------------------------------------------------------------------------
- def __init__(self):
- self._templateManager = TemplateManager()
+ wizard = ExtensionWizard()
+ wizard.execute()
- # ---------------------------------------------------------------------------
- def create(self, args, name, kind="default"):
- """Create a new extension from specified extension template.
+ Interaction with `GitHub `_ uses
+ :func:`.GithubHelper.logIn` to authenticate.
- :param args.destination: Directory wherein the new extension is created.
- :type args.destination: :class:`str`
- :param name: Name for the new extension.
- :type name: :class:`str`
- :param kind: Identifier of the template from which to create the extension.
- :type kind: :class:`str`
-
- Note that the extension is written to a *new subdirectory* which is created
- in ``args.destination``. The ``name`` is used both as the name of this
- directory, and as the replacement value when substituting the template key.
-
- If an error occurs, the application displays an error message and exits.
-
- .. seealso:: :meth:`.TemplateManager.copyTemplate`
+ .. 'note' directive needs '\' to span multiple lines!
+ .. note:: Most methods will signal the application to exit if \
+ something goes wrong. This behavior is hidden by the \
+ :meth:`~ExtensionWizard.execute` method when passing \
+ ``exit=False``; callers that need to continue execution after \
+ calling one of the other methods directly should catch \
+ :exc:`~exceptions.SystemExit`.
"""
- try:
- dest = args.destination
- args.destination = self._templateManager.copyTemplate(dest, "extensions",
- kind, name)
- logging.info("created extension '%s'" % name)
+ _reModuleInsertPlaceholder = re.compile("(?<=\n)([ \t]*)## NEXT_MODULE")
+ _reAddSubdirectory = \
+ re.compile("(?<=\n)([ \t]*)add_subdirectory[(][^)]+[)][^\n]*\n")
- except:
- die("failed to create extension: %s" % sys.exc_info()[1])
+ # ---------------------------------------------------------------------------
+ def __init__(self):
+ self._templateManager = TemplateManager()
- # ---------------------------------------------------------------------------
- def addModule(self, args, kind, name):
- """Add a module to an existing extension.
+ # ---------------------------------------------------------------------------
+ def create(self, args, name, kind="default"):
+ """Create a new extension from specified extension template.
- :param args.destination: Location (directory) of the extension to modify.
- :type args.destination: :class:`str`
- :param kind: Identifier of the template from which to create the module.
- :type kind: :class:`str`
- :param name: Name for the new module.
- :type name: :class:`str`
+ :param args.destination: Directory wherein the new extension is created.
+ :type args.destination: :class:`str`
+ :param name: Name for the new extension.
+ :type name: :class:`str`
+ :param kind: Identifier of the template from which to create the extension.
+ :type kind: :class:`str`
- This creates a new module from the specified module template and adds it to
- the CMakeLists.txt of the extension. The ``name`` is used both as the name
- of the new module subdirectory (created in ``args.destination``) and as the
- replacement value when substituting the template key.
+ Note that the extension is written to a *new subdirectory* which is created
+ in ``args.destination``. The ``name`` is used both as the name of this
+ directory, and as the replacement value when substituting the template key.
- If an error occurs, the extension is not modified, and the application
- displays an error message and then exits.
+ If an error occurs, the application displays an error message and exits.
- .. seealso:: :meth:`.ExtensionProject.addModule`,
- :meth:`.TemplateManager.copyTemplate`
- """
+ .. seealso:: :meth:`.TemplateManager.copyTemplate`
+ """
- try:
- dest = args.destination
- p = ExtensionProject(dest)
- p.addModule(name)
- self._templateManager.copyTemplate(dest, "modules", kind, name)
- p.save()
- logging.info("created module '%s'" % name)
+ try:
+ dest = args.destination
+ args.destination = self._templateManager.copyTemplate(dest, "extensions",
+ kind, name)
+ logging.info("created extension '%s'" % name)
- except:
- die("failed to add module: %s" % sys.exc_info()[1])
+ except:
+ die("failed to create extension: %s" % sys.exc_info()[1])
- # ---------------------------------------------------------------------------
- def describe(self, args):
- """Generate extension description and write it to :attr:`sys.stdout`.
+ # ---------------------------------------------------------------------------
+ def addModule(self, args, kind, name):
+ """Add a module to an existing extension.
- :param args.destination: Location (directory) of the extension to describe.
- :type args.destination: :class:`str`
+ :param args.destination: Location (directory) of the extension to modify.
+ :type args.destination: :class:`str`
+ :param kind: Identifier of the template from which to create the module.
+ :type kind: :class:`str`
+ :param name: Name for the new module.
+ :type name: :class:`str`
- If something goes wrong, the application displays a suitable error message.
- """
+ This creates a new module from the specified module template and adds it to
+ the CMakeLists.txt of the extension. The ``name`` is used both as the name
+ of the new module subdirectory (created in ``args.destination``) and as the
+ replacement value when substituting the template key.
- try:
- r = None
+ If an error occurs, the extension is not modified, and the application
+ displays an error message and then exits.
- if args.localExtensionsDir:
- r = SourceTreeDirectory(args.localExtensionsDir, os.path.relpath(args.destination, args.localExtensionsDir))
+ .. seealso:: :meth:`.ExtensionProject.addModule`,
+ :meth:`.TemplateManager.copyTemplate`
+ """
- else:
- r = getRepo(args.destination)
+ try:
+ dest = args.destination
+ p = ExtensionProject(dest)
+ p.addModule(name)
+ self._templateManager.copyTemplate(dest, "modules", kind, name)
+ p.save()
+ logging.info("created module '%s'" % name)
- if r is None:
- xd = ExtensionDescription(sourcedir=args.destination)
+ except:
+ die("failed to add module: %s" % sys.exc_info()[1])
- else:
- xd = ExtensionDescription(repo=r)
+ # ---------------------------------------------------------------------------
+ def describe(self, args):
+ """Generate extension description and write it to :attr:`sys.stdout`.
- xd.write(sys.stdout)
+ :param args.destination: Location (directory) of the extension to describe.
+ :type args.destination: :class:`str`
- except:
- die("failed to describe extension: %s" % sys.exc_info()[1])
+ If something goes wrong, the application displays a suitable error message.
+ """
- # ---------------------------------------------------------------------------
- def _setExtensionUrl(self, project, name, value):
- name = "EXTENSION_%s" % name
+ try:
+ r = None
- oldValue = project.getValue(name)
+ if args.localExtensionsDir:
+ r = SourceTreeDirectory(args.localExtensionsDir, os.path.relpath(args.destination, args.localExtensionsDir))
- try:
- url = urlparse(oldValue)
- confirm = not url.hostname.endswith("example.com")
+ else:
+ r = getRepo(args.destination)
- except:
- confirm = True
+ if r is None:
+ xd = ExtensionDescription(sourcedir=args.destination)
- if confirm:
- logging.info("Your extension currently uses '%s' for %s,"
- " which can be changed to '%s' to point to your new"
- " public repository." % (oldValue, name, value))
- if not inquire("Change it"):
- return
+ else:
+ xd = ExtensionDescription(repo=r)
- project.setValue(name, value)
+ xd.write(sys.stdout)
- # ---------------------------------------------------------------------------
- def publish(self, args):
- """Publish extension to github repository.
+ except:
+ die("failed to describe extension: %s" % sys.exc_info()[1])
- :param args.destination: Location (directory) of the extension to publish.
- :type args.destination: :class:`str`
- :param args.cmakefile:
- Name of the CMake file where `EXTENSION_*` CMake variables
- are set. Default is `CMakeLists.txt`.
- :type args.cmakefile:
- :class:`str`
- :param args.name:
- Name of extension. Default is value associated with `project()`
- statement.
- :type args.name:
- :class:`str`
+ # ---------------------------------------------------------------------------
+ def _setExtensionUrl(self, project, name, value):
+ name = "EXTENSION_%s" % name
- This creates a public github repository for an extension (whose name is the
- extension name), adds it as a remote of the extension's local repository,
- and pushes the extension to the new github repository. The extension
- information (homepage, icon url) is also updated to refer to the new public
- repository.
+ oldValue = project.getValue(name)
- If the extension is not already tracked in a local git repository, a new
- local git repository is also created and populated by the files currently
- in the extension source directory.
+ try:
+ url = urlparse(oldValue)
+ confirm = not url.hostname.endswith("example.com")
- If the local repository is dirty or already has a remote, or a github
- repository with the name of the extension already exists, the application
- displays a suitable error message and then exits.
- """
+ except:
+ confirm = True
+
+ if confirm:
+ logging.info("Your extension currently uses '%s' for %s,"
+ " which can be changed to '%s' to point to your new"
+ " public repository." % (oldValue, name, value))
+ if not inquire("Change it"):
+ return
+
+ project.setValue(name, value)
+
+ # ---------------------------------------------------------------------------
+ def publish(self, args):
+ """Publish extension to github repository.
+
+ :param args.destination: Location (directory) of the extension to publish.
+ :type args.destination: :class:`str`
+ :param args.cmakefile:
+ Name of the CMake file where `EXTENSION_*` CMake variables
+ are set. Default is `CMakeLists.txt`.
+ :type args.cmakefile:
+ :class:`str`
+ :param args.name:
+ Name of extension. Default is value associated with `project()`
+ statement.
+ :type args.name:
+ :class:`str`
+
+ This creates a public github repository for an extension (whose name is the
+ extension name), adds it as a remote of the extension's local repository,
+ and pushes the extension to the new github repository. The extension
+ information (homepage, icon url) is also updated to refer to the new public
+ repository.
+
+ If the extension is not already tracked in a local git repository, a new
+ local git repository is also created and populated by the files currently
+ in the extension source directory.
+
+ If the local repository is dirty or already has a remote, or a github
+ repository with the name of the extension already exists, the application
+ displays a suitable error message and then exits.
+ """
+
+ createdRepo = False
+ r = getRepo(args.destination, tool="git")
+
+ if r is None:
+ # Create new git repository
+ import git
+ r = git.Repo.init(args.destination)
+ createdRepo = True
+
+ # Prepare the initial commit
+ branch = "master"
+ r.git.checkout(b=branch)
+ r.git.add(":/")
+
+ logging.info("Creating initial commit containing the following files:")
+ for e in r.index.entries:
+ logging.info(" %s" % e[0])
+ logging.info("")
+ if not inquire("Continue"):
+ prog = os.path.basename(sys.argv[0])
+ die("canceling at user request:"
+ " update your index and run %s again" % prog)
+
+ else:
+ # Check if repository is dirty
+ if r.is_dirty():
+ die("declined: working tree is dirty;"
+ " commit or stash your changes first")
+
+ # Check if a remote already exists
+ if len(r.remotes):
+ die("declined: publishing is only supported for repositories"
+ " with no pre-existing remotes")
+
+ branch = r.active_branch
+ if branch.name != "master":
+ logging.warning("You are currently on the '%s' branch. "
+ "It is strongly recommended to publish"
+ " the 'master' branch." % branch)
+ if not inquire("Continue anyway"):
+ die("canceled at user request")
+
+ logging.debug("preparing to publish %s branch", branch)
+
+ try:
+ # Get extension name
+ p = ExtensionProject(args.destination, filename=args.cmakefile)
+ if args.name is None:
+ name = p.project
+ else:
+ name = args.name
+ logging.debug("extension name: '%s'", name)
+
+ # Create github remote
+ logging.info("creating github repository")
+ gh = GithubHelper.logIn(r)
+ ghu = gh.get_user()
+ for ghr in ghu.get_repos():
+ if ghr.name == name:
+ die("declined: a github repository named '%s' already exists" % name)
+
+ description = p.getValue("EXTENSION_DESCRIPTION", default=NotSet)
+ ghr = ghu.create_repo(name, description=description)
+ logging.debug("created github repository: %s", ghr.url)
+
+ # Set extension meta-information
+ logging.info("updating extension meta-information")
+ raw_url = "{}/{}".format(ghr.html_url.replace("//", "//raw."), branch)
+ self._setExtensionUrl(p, "HOMEPAGE", ghr.html_url)
+ self._setExtensionUrl(p, "ICONURL", f"{raw_url}/{name}.png")
+ p.save()
+
+ # Commit the initial commit or updated meta-information
+ r.git.add(":/CMakeLists.txt")
+ if createdRepo:
+ logging.info("preparing initial commit")
+ r.index.commit("ENH: Initial commit for %s" % name)
+ else:
+ logging.info("committing changes")
+ r.index.commit("ENH: Update extension information\n\n"
+ "Set %s information to reference"
+ " new github repository." % name)
+
+ # Set up the remote and push
+ logging.info("preparing to push extension repository")
+ remote = r.create_remote("origin", ghr.clone_url)
+ remote.push(branch)
+ logging.info("extension published to %s", ghr.url)
+
+ except SystemExit:
+ raise
+ except:
+ die("failed to publish extension: %s" % sys.exc_info()[1])
- createdRepo = False
- r = getRepo(args.destination, tool="git")
-
- if r is None:
- # Create new git repository
- import git
- r = git.Repo.init(args.destination)
- createdRepo = True
-
- # Prepare the initial commit
- branch = "master"
- r.git.checkout(b=branch)
- r.git.add(":/")
-
- logging.info("Creating initial commit containing the following files:")
- for e in r.index.entries:
- logging.info(" %s" % e[0])
- logging.info("")
- if not inquire("Continue"):
- prog = os.path.basename(sys.argv[0])
- die("canceling at user request:"
- " update your index and run %s again" % prog)
+ # ---------------------------------------------------------------------------
+ def _extensionIndexCommitMessage(self, name, description, update, wrap=True):
+ args = description.__dict__
+ args["name"] = name
- else:
- # Check if repository is dirty
- if r.is_dirty():
- die("declined: working tree is dirty;"
- " commit or stash your changes first")
-
- # Check if a remote already exists
- if len(r.remotes):
- die("declined: publishing is only supported for repositories"
- " with no pre-existing remotes")
-
- branch = r.active_branch
- if branch.name != "master":
- logging.warning("You are currently on the '%s' branch. "
- "It is strongly recommended to publish"
- " the 'master' branch." % branch)
- if not inquire("Continue anyway"):
- die("canceled at user request")
-
- logging.debug("preparing to publish %s branch", branch)
-
- try:
- # Get extension name
- p = ExtensionProject(args.destination, filename=args.cmakefile)
- if args.name is None:
- name = p.project
- else:
- name = args.name
- logging.debug("extension name: '%s'", name)
-
- # Create github remote
- logging.info("creating github repository")
- gh = GithubHelper.logIn(r)
- ghu = gh.get_user()
- for ghr in ghu.get_repos():
- if ghr.name == name:
- die("declined: a github repository named '%s' already exists" % name)
-
- description = p.getValue("EXTENSION_DESCRIPTION", default=NotSet)
- ghr = ghu.create_repo(name, description=description)
- logging.debug("created github repository: %s", ghr.url)
-
- # Set extension meta-information
- logging.info("updating extension meta-information")
- raw_url = "{}/{}".format(ghr.html_url.replace("//", "//raw."), branch)
- self._setExtensionUrl(p, "HOMEPAGE", ghr.html_url)
- self._setExtensionUrl(p, "ICONURL", f"{raw_url}/{name}.png")
- p.save()
-
- # Commit the initial commit or updated meta-information
- r.git.add(":/CMakeLists.txt")
- if createdRepo:
- logging.info("preparing initial commit")
- r.index.commit("ENH: Initial commit for %s" % name)
- else:
- logging.info("committing changes")
- r.index.commit("ENH: Update extension information\n\n"
- "Set %s information to reference"
- " new github repository." % name)
-
- # Set up the remote and push
- logging.info("preparing to push extension repository")
- remote = r.create_remote("origin", ghr.clone_url)
- remote.push(branch)
- logging.info("extension published to %s", ghr.url)
-
- except SystemExit:
- raise
- except:
- die("failed to publish extension: %s" % sys.exc_info()[1])
-
- # ---------------------------------------------------------------------------
- def _extensionIndexCommitMessage(self, name, description, update, wrap=True):
- args = description.__dict__
- args["name"] = name
-
- if update:
- template = textwrap.dedent("""\
+ if update:
+ template = textwrap.dedent("""\
ENH: Update %(name)s extension
This updates the %(name)s extension to %(scmrevision)s.
""")
- if wrap:
- paragraphs = (template % args).split("\n")
- return "\n".join([textwrap.fill(p, width=76) for p in paragraphs])
- else:
- return template % args
-
- else:
- template = textwrap.dedent("""\
+ if wrap:
+ paragraphs = (template % args).split("\n")
+ return "\n".join([textwrap.fill(p, width=76) for p in paragraphs])
+ else:
+ return template % args
+
+ else:
+ template = textwrap.dedent("""\
ENH: Add %(name)s extension
Description:
@@ -348,441 +348,441 @@ def _extensionIndexCommitMessage(self, name, description, update, wrap=True):
%(contributors)s
""")
- if wrap:
- for key in args:
- args[key] = textwrap.fill(args[key], width=72)
-
- return template % args
-
- # ---------------------------------------------------------------------------
- def contribute(self, args):
- """Add or update an extension to/in the index repository.
-
- :param args.destination:
- Location (directory) of the extension to contribute.
- :type args.destination:
- :class:`str`
- :param args.cmakefile:
- Name of the CMake file where `EXTENSION_*` CMake variables
- are set. Default is `CMakeLists.txt`.
- :type args.cmakefile:
- :class:`str`
- :param args.name:
- Name of extension. Default is value associated with `project()`
- statement.
- :type args.name:
- :class:`str`
- :param args.target:
- Name of branch which the extension targets (must match a branch name in
- the extension index repository).
- :type args.target:
- :class:`str`
- :param args.index:
- Path to an existing clone of the extension index, or path to which the
- index should be cloned. If ``None``, a subdirectory in the extension's
- ``.git`` directory is used.
- :type args.index:
- :class:`str` or ``None``
- :param args.test:
- If ``True``, include a note in the pull request that the request is a
- test and should not be merged.
- :type args.test:
- :class:`bool`
-
- This writes the description of the specified extension --- which may be an
- addition, or an update to a previously contributed extension --- to a user
- fork of the `extension index repository`_, pushes the changes, and creates
- a pull request to merge the contribution. In case of an update to an
- extension with a github public repository, a "compare URL" (a github link
- to view the changes between the previously contributed version of the
- extension and the version being newly contributed) is included in the pull
- request message.
-
- This attempts to find the user's already existing fork of the index
- repository, and to create one if it does not already exist. The fork is
- then either cloned (adding remotes for both upstream and the user's fork)
- or updated, and the current upstream target branch pushed to the user's
- fork, ensuring that the target branch in the user's fork is up to date. The
- changes to the index repository are made in a separate branch.
-
- If a pull request for the extension already exists, its message is updated
- and the corresponding branch is force-pushed (which automatically updates
- the code portion of the request).
-
- If anything goes wrong, no pull request is created, and the application
- displays a suitable error message and then exits. Additionally, a branch
- push for the index changes only occurs if the failed operation is the
- creation or update of the pull request; other errors cause the application
- to exit before pushing the branch. (Updates of the user's fork to current
- upstream may still occur.)
-
- .. _extension index repository: https://github.com/Slicer/ExtensionsIndex
- """
+ if wrap:
+ for key in args:
+ args[key] = textwrap.fill(args[key], width=72)
+
+ return template % args
+
+ # ---------------------------------------------------------------------------
+ def contribute(self, args):
+ """Add or update an extension to/in the index repository.
+
+ :param args.destination:
+ Location (directory) of the extension to contribute.
+ :type args.destination:
+ :class:`str`
+ :param args.cmakefile:
+ Name of the CMake file where `EXTENSION_*` CMake variables
+ are set. Default is `CMakeLists.txt`.
+ :type args.cmakefile:
+ :class:`str`
+ :param args.name:
+ Name of extension. Default is value associated with `project()`
+ statement.
+ :type args.name:
+ :class:`str`
+ :param args.target:
+ Name of branch which the extension targets (must match a branch name in
+ the extension index repository).
+ :type args.target:
+ :class:`str`
+ :param args.index:
+ Path to an existing clone of the extension index, or path to which the
+ index should be cloned. If ``None``, a subdirectory in the extension's
+ ``.git`` directory is used.
+ :type args.index:
+ :class:`str` or ``None``
+ :param args.test:
+ If ``True``, include a note in the pull request that the request is a
+ test and should not be merged.
+ :type args.test:
+ :class:`bool`
+
+ This writes the description of the specified extension --- which may be an
+ addition, or an update to a previously contributed extension --- to a user
+ fork of the `extension index repository`_, pushes the changes, and creates
+ a pull request to merge the contribution. In case of an update to an
+ extension with a github public repository, a "compare URL" (a github link
+ to view the changes between the previously contributed version of the
+ extension and the version being newly contributed) is included in the pull
+ request message.
+
+ This attempts to find the user's already existing fork of the index
+ repository, and to create one if it does not already exist. The fork is
+ then either cloned (adding remotes for both upstream and the user's fork)
+ or updated, and the current upstream target branch pushed to the user's
+ fork, ensuring that the target branch in the user's fork is up to date. The
+ changes to the index repository are made in a separate branch.
+
+ If a pull request for the extension already exists, its message is updated
+ and the corresponding branch is force-pushed (which automatically updates
+ the code portion of the request).
+
+ If anything goes wrong, no pull request is created, and the application
+ displays a suitable error message and then exits. Additionally, a branch
+ push for the index changes only occurs if the failed operation is the
+ creation or update of the pull request; other errors cause the application
+ to exit before pushing the branch. (Updates of the user's fork to current
+ upstream may still occur.)
+
+ .. _extension index repository: https://github.com/Slicer/ExtensionsIndex
+ """
- try:
- r = getRepo(args.destination)
- if r is None:
- die("extension repository not found")
-
- xd = ExtensionDescription(repo=r, cmakefile=args.cmakefile)
- if args.name is None:
- name = ExtensionProject(localRoot(r), filename=args.cmakefile).project
- else:
- name = args.name
- logging.debug("extension name: '%s'", name)
-
- # Validate that extension has a SCM URL
- if xd.scmurl == "NA":
- raise Exception("extension 'scmurl' is not set")
-
- # Get (or create) the user's fork of the extension index
- logging.info("obtaining github repository information")
- gh = GithubHelper.logIn(r if xd.scm == "git" else None)
- upstreamRepo = GithubHelper.getRepo(gh, name="Slicer/ExtensionsIndex")
- if upstreamRepo is None:
- die("error accessing extension index upstream repository")
-
- logging.debug("index upstream: %s", upstreamRepo.url)
-
- forkedRepo = GithubHelper.getFork(user=gh.get_user(), create=True,
- upstream=upstreamRepo)
-
- logging.debug("index fork: %s", forkedRepo.url)
-
- # Get or create extension index repository
- if args.index is not None:
- xip = args.index
- else:
- xip = os.path.join(vcsPrivateDirectory(r), "extension-index")
-
- xiRepo = getRepo(xip)
-
- if xiRepo is None:
- logging.info("cloning index repository")
- xiRepo = getRepo(xip, create=createEmptyRepo)
- xiRemote = getRemote(xiRepo, [forkedRepo.clone_url], create="origin")
-
- else:
- # Check that the index repository is a clone of the github fork
- xiRemote = [forkedRepo.clone_url, forkedRepo.git_url]
- xiRemote = getRemote(xiRepo, xiRemote)
- if xiRemote is None:
- raise Exception("the extension index repository ('%s')"
- " is not a clone of %s" %
- (xiRepo.working_tree_dir, forkedRepo.clone_url))
-
- logging.debug("index fork remote: %s", xiRemote.url)
-
- # Find or create the upstream remote for the index repository
- xiUpstream = [upstreamRepo.clone_url, upstreamRepo.git_url]
- xiUpstream = getRemote(xiRepo, xiUpstream, create="upstream")
- logging.debug("index upstream remote: %s", xiUpstream.url)
-
- # Check that the index repository is clean
- if xiRepo.is_dirty():
- raise Exception("the extension index repository ('%s') is dirty" %
- xiRepo.working_tree_dir)
-
- # Update the index repository and get the base branch
- logging.info("updating local index clone")
- xiRepo.git.fetch(xiUpstream)
- if not args.target in xiUpstream.refs:
- die("target branch '%s' does not exist" % args.target)
-
- xiBase = xiUpstream.refs[args.target]
-
- # Ensure that user's fork is up to date
- logging.info("updating target branch (%s) branch on fork", args.target)
- xiRemote.push(f"{xiBase}:refs/heads/{args.target}")
-
- # Determine if this is an addition or update to the index
- xdf = name + ".s4ext"
- if xdf in xiBase.commit.tree:
- branch = f'update-{name}-{args.target}'
- update = True
- else:
- branch = f'add-{name}-{args.target}'
- update = False
-
- logging.debug("create index branch %s", branch)
- xiRepo.git.checkout(xiBase, B=branch)
-
- # Check to see if there is an existing pull request
- pullRequest = GithubHelper.getPullRequest(upstreamRepo, fork=forkedRepo,
- ref=branch)
- logging.debug("existing pull request: %s",
- pullRequest if pullRequest is None else pullRequest.url)
-
- if update:
- # Get old SCM revision
try:
- odPath = os.path.join(xiRepo.working_tree_dir, xdf)
- od = ExtensionDescription(filepath=odPath)
- if od.scmrevision != "NA":
- oldRef = od.scmrevision
-
+ r = getRepo(args.destination)
+ if r is None:
+ die("extension repository not found")
+
+ xd = ExtensionDescription(repo=r, cmakefile=args.cmakefile)
+ if args.name is None:
+ name = ExtensionProject(localRoot(r), filename=args.cmakefile).project
+ else:
+ name = args.name
+ logging.debug("extension name: '%s'", name)
+
+ # Validate that extension has a SCM URL
+ if xd.scmurl == "NA":
+ raise Exception("extension 'scmurl' is not set")
+
+ # Get (or create) the user's fork of the extension index
+ logging.info("obtaining github repository information")
+ gh = GithubHelper.logIn(r if xd.scm == "git" else None)
+ upstreamRepo = GithubHelper.getRepo(gh, name="Slicer/ExtensionsIndex")
+ if upstreamRepo is None:
+ die("error accessing extension index upstream repository")
+
+ logging.debug("index upstream: %s", upstreamRepo.url)
+
+ forkedRepo = GithubHelper.getFork(user=gh.get_user(), create=True,
+ upstream=upstreamRepo)
+
+ logging.debug("index fork: %s", forkedRepo.url)
+
+ # Get or create extension index repository
+ if args.index is not None:
+ xip = args.index
+ else:
+ xip = os.path.join(vcsPrivateDirectory(r), "extension-index")
+
+ xiRepo = getRepo(xip)
+
+ if xiRepo is None:
+ logging.info("cloning index repository")
+ xiRepo = getRepo(xip, create=createEmptyRepo)
+ xiRemote = getRemote(xiRepo, [forkedRepo.clone_url], create="origin")
+
+ else:
+ # Check that the index repository is a clone of the github fork
+ xiRemote = [forkedRepo.clone_url, forkedRepo.git_url]
+ xiRemote = getRemote(xiRepo, xiRemote)
+ if xiRemote is None:
+ raise Exception("the extension index repository ('%s')"
+ " is not a clone of %s" %
+ (xiRepo.working_tree_dir, forkedRepo.clone_url))
+
+ logging.debug("index fork remote: %s", xiRemote.url)
+
+ # Find or create the upstream remote for the index repository
+ xiUpstream = [upstreamRepo.clone_url, upstreamRepo.git_url]
+ xiUpstream = getRemote(xiRepo, xiUpstream, create="upstream")
+ logging.debug("index upstream remote: %s", xiUpstream.url)
+
+ # Check that the index repository is clean
+ if xiRepo.is_dirty():
+ raise Exception("the extension index repository ('%s') is dirty" %
+ xiRepo.working_tree_dir)
+
+ # Update the index repository and get the base branch
+ logging.info("updating local index clone")
+ xiRepo.git.fetch(xiUpstream)
+ if not args.target in xiUpstream.refs:
+ die("target branch '%s' does not exist" % args.target)
+
+ xiBase = xiUpstream.refs[args.target]
+
+ # Ensure that user's fork is up to date
+ logging.info("updating target branch (%s) branch on fork", args.target)
+ xiRemote.push(f"{xiBase}:refs/heads/{args.target}")
+
+ # Determine if this is an addition or update to the index
+ xdf = name + ".s4ext"
+ if xdf in xiBase.commit.tree:
+ branch = f'update-{name}-{args.target}'
+ update = True
+ else:
+ branch = f'add-{name}-{args.target}'
+ update = False
+
+ logging.debug("create index branch %s", branch)
+ xiRepo.git.checkout(xiBase, B=branch)
+
+ # Check to see if there is an existing pull request
+ pullRequest = GithubHelper.getPullRequest(upstreamRepo, fork=forkedRepo,
+ ref=branch)
+ logging.debug("existing pull request: %s",
+ pullRequest if pullRequest is None else pullRequest.url)
+
+ if update:
+ # Get old SCM revision
+ try:
+ odPath = os.path.join(xiRepo.working_tree_dir, xdf)
+ od = ExtensionDescription(filepath=odPath)
+ if od.scmrevision != "NA":
+ oldRef = od.scmrevision
+
+ except:
+ oldRef = None
+
+ # Write the extension description and prepare to commit
+ xd.write(os.path.join(xiRepo.working_tree_dir, xdf))
+ xiRepo.index.add([xdf])
+
+ # Commit and push the new/updated extension description
+ xiRepo.index.commit(self._extensionIndexCommitMessage(
+ name, xd, update=update))
+
+ try:
+ # We need the old branch, if it exists, to be fetched locally, so that
+ # push info resolution doesn't choke trying to resolve the old SHA
+ xiRemote.fetch(branch)
+ except:
+ pass
+
+ xiRemote.push("+%s" % branch)
+
+ # Get message formatted for pull request
+ msg = self._extensionIndexCommitMessage(name, xd, update=update,
+ wrap=False).split("\n")
+ if len(msg) > 2 and not len(msg[1].strip()):
+ del msg[1]
+
+ # Update PR title to indicate the target name
+ msg[0] += " [%s]" % args.target
+
+ # Try to add compare URL to pull request message, if applicable
+ if update and oldRef is not None:
+ extensionRepo = GithubHelper.getRepo(gh, url=xd.scmurl)
+
+ if extensionRepo is not None:
+ logging.info("building compare URL for update")
+ logging.debug(" repository: %s", extensionRepo.url)
+ logging.debug(" old SHA: %s", oldRef)
+ logging.debug(" new SHA: %s", xd.scmrevision)
+
+ try:
+ c = extensionRepo.compare(oldRef, xd.scmrevision)
+
+ msg.append("")
+ msg.append("See %s to view changes to the extension." % c.html_url)
+
+ except:
+ warn("failed to build compare URL: %s" % sys.exc_info()[1])
+
+ if args.test:
+ msg.append("")
+ msg.append("THIS PULL REQUEST WAS MACHINE GENERATED"
+ " FOR TESTING PURPOSES. DO NOT MERGE.")
+
+ if args.dryRun:
+ if pullRequest is not None:
+ logging.info("updating pull request %s", pullRequest.html_url)
+
+ logging.info("prepared pull request message:\n%s", "\n".join(msg))
+
+ return
+
+ # Create or update the pull request
+ if pullRequest is None:
+ logging.info("creating pull request")
+
+ pull = f"{forkedRepo.owner.login}:{branch}"
+ pullRequest = upstreamRepo.create_pull(
+ title=msg[0], body="\n".join(msg[1:]),
+ head=pull, base=args.target)
+
+ logging.info("created pull request %s", pullRequest.html_url)
+
+ else:
+ logging.info("updating pull request %s", pullRequest.html_url)
+ pullRequest.edit(title=msg[0], body="\n".join(msg[1:]), state="open")
+ logging.info("updated pull request %s", pullRequest.html_url)
+
+ except SystemExit:
+ raise
except:
- oldRef = None
-
- # Write the extension description and prepare to commit
- xd.write(os.path.join(xiRepo.working_tree_dir, xdf))
- xiRepo.index.add([xdf])
-
- # Commit and push the new/updated extension description
- xiRepo.index.commit(self._extensionIndexCommitMessage(
- name, xd, update=update))
-
- try:
- # We need the old branch, if it exists, to be fetched locally, so that
- # push info resolution doesn't choke trying to resolve the old SHA
- xiRemote.fetch(branch)
- except:
- pass
-
- xiRemote.push("+%s" % branch)
-
- # Get message formatted for pull request
- msg = self._extensionIndexCommitMessage(name, xd, update=update,
- wrap=False).split("\n")
- if len(msg) > 2 and not len(msg[1].strip()):
- del msg[1]
-
- # Update PR title to indicate the target name
- msg[0] += " [%s]" % args.target
-
- # Try to add compare URL to pull request message, if applicable
- if update and oldRef is not None:
- extensionRepo = GithubHelper.getRepo(gh, url=xd.scmurl)
-
- if extensionRepo is not None:
- logging.info("building compare URL for update")
- logging.debug(" repository: %s", extensionRepo.url)
- logging.debug(" old SHA: %s", oldRef)
- logging.debug(" new SHA: %s", xd.scmrevision)
-
- try:
- c = extensionRepo.compare(oldRef, xd.scmrevision)
-
- msg.append("")
- msg.append("See %s to view changes to the extension." % c.html_url)
-
- except:
- warn("failed to build compare URL: %s" % sys.exc_info()[1])
-
- if args.test:
- msg.append("")
- msg.append("THIS PULL REQUEST WAS MACHINE GENERATED"
- " FOR TESTING PURPOSES. DO NOT MERGE.")
-
- if args.dryRun:
- if pullRequest is not None:
- logging.info("updating pull request %s", pullRequest.html_url)
-
- logging.info("prepared pull request message:\n%s", "\n".join(msg))
-
- return
-
- # Create or update the pull request
- if pullRequest is None:
- logging.info("creating pull request")
-
- pull = f"{forkedRepo.owner.login}:{branch}"
- pullRequest = upstreamRepo.create_pull(
- title=msg[0], body="\n".join(msg[1:]),
- head=pull, base=args.target)
-
- logging.info("created pull request %s", pullRequest.html_url)
-
- else:
- logging.info("updating pull request %s", pullRequest.html_url)
- pullRequest.edit(title=msg[0], body="\n".join(msg[1:]), state="open")
- logging.info("updated pull request %s", pullRequest.html_url)
-
- except SystemExit:
- raise
- except:
- die("failed to register extension: %s" % sys.exc_info()[1])
-
- # ---------------------------------------------------------------------------
- def _execute(self, args):
- # Set up arguments
- parser = argparse.ArgumentParser(description="Slicer Wizard",
- formatter_class=WizardHelpFormatter)
-
- parser.add_argument('--version', action='version',
- version=__version__)
-
- parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
- parser.add_argument("--test", action="store_true", help=argparse.SUPPRESS)
- parser.add_argument("--dryRun", action="store_true", help=argparse.SUPPRESS)
- parser.add_argument("--localExtensionsDir", help=argparse.SUPPRESS)
-
- parser.add_argument("--create", metavar="NAME",
- help="create TYPE extension NAME"
- " under the destination directory;"
- " any modules are added to the new extension"
- " (default type: 'default')")
- parser.add_argument("--addModule", metavar="TYPE:NAME", action="append",
- help="add new TYPE module NAME to an existing project"
- " in the destination directory;"
- " may use more than once")
- self._templateManager.addArguments(parser)
- parser.add_argument("--listTemplates", action="store_true",
- help="show list of available templates"
- " and associated substitution keys")
- parser.add_argument("--describe", action="store_true",
- help="print the extension description (s4ext)"
- " to standard output")
-
- parser.add_argument("--name", metavar="NAME",
- help="name of the extension"
- " (default: value associated with 'project()' statement)")
-
- parser.add_argument("--publish", action="store_true",
- help="publish the extension in the destination"
- " directory to github (account required)")
- parser.add_argument("--contribute", action="store_true",
- help="register or update a compiled extension with"
- " the extension index (github account required)")
- parser.add_argument("--target", metavar="VERSION", default="master",
- help="version of Slicer for which the extension"
- " is intended (default='master')")
- parser.add_argument("--index", metavar="PATH",
- help="location for the extension index clone"
- " (default: private directory"
- " in the extension clone)")
-
- parser.add_argument("destination", default=os.getcwd(), nargs="?",
- help="location of output files / extension source"
- " (default: '.')")
-
- parser.add_argument("cmakefile", default="CMakeLists.txt", nargs="?",
- help="name of the CMake file where EXTENSION_* CMake variables are set"
- " (default: 'CMakeLists.txt')")
-
- args = parser.parse_args(args)
- initLogging(logging.getLogger(), args)
-
- # The following arguments are only available if haveGit() is True
- if not haveGit() and (args.publish or args.contribute or args.name):
- option = "--publish"
- if args.contribute:
- option = "--contribute"
- elif args.name:
- option = "--name"
- die(textwrap.dedent(
- """\
+ die("failed to register extension: %s" % sys.exc_info()[1])
+
+ # ---------------------------------------------------------------------------
+ def _execute(self, args):
+ # Set up arguments
+ parser = argparse.ArgumentParser(description="Slicer Wizard",
+ formatter_class=WizardHelpFormatter)
+
+ parser.add_argument('--version', action='version',
+ version=__version__)
+
+ parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
+ parser.add_argument("--test", action="store_true", help=argparse.SUPPRESS)
+ parser.add_argument("--dryRun", action="store_true", help=argparse.SUPPRESS)
+ parser.add_argument("--localExtensionsDir", help=argparse.SUPPRESS)
+
+ parser.add_argument("--create", metavar="NAME",
+ help="create TYPE extension NAME"
+ " under the destination directory;"
+ " any modules are added to the new extension"
+ " (default type: 'default')")
+ parser.add_argument("--addModule", metavar="TYPE:NAME", action="append",
+ help="add new TYPE module NAME to an existing project"
+ " in the destination directory;"
+ " may use more than once")
+ self._templateManager.addArguments(parser)
+ parser.add_argument("--listTemplates", action="store_true",
+ help="show list of available templates"
+ " and associated substitution keys")
+ parser.add_argument("--describe", action="store_true",
+ help="print the extension description (s4ext)"
+ " to standard output")
+
+ parser.add_argument("--name", metavar="NAME",
+ help="name of the extension"
+ " (default: value associated with 'project()' statement)")
+
+ parser.add_argument("--publish", action="store_true",
+ help="publish the extension in the destination"
+ " directory to github (account required)")
+ parser.add_argument("--contribute", action="store_true",
+ help="register or update a compiled extension with"
+ " the extension index (github account required)")
+ parser.add_argument("--target", metavar="VERSION", default="master",
+ help="version of Slicer for which the extension"
+ " is intended (default='master')")
+ parser.add_argument("--index", metavar="PATH",
+ help="location for the extension index clone"
+ " (default: private directory"
+ " in the extension clone)")
+
+ parser.add_argument("destination", default=os.getcwd(), nargs="?",
+ help="location of output files / extension source"
+ " (default: '.')")
+
+ parser.add_argument("cmakefile", default="CMakeLists.txt", nargs="?",
+ help="name of the CMake file where EXTENSION_* CMake variables are set"
+ " (default: 'CMakeLists.txt')")
+
+ args = parser.parse_args(args)
+ initLogging(logging.getLogger(), args)
+
+ # The following arguments are only available if haveGit() is True
+ if not haveGit() and (args.publish or args.contribute or args.name):
+ option = "--publish"
+ if args.contribute:
+ option = "--contribute"
+ elif args.name:
+ option = "--name"
+ die(textwrap.dedent(
+ """\
Option '%s' is not available.
Consider re-building Slicer with SSL support or downloading
Slicer from https://download.slicer.org
""" % option))
- # Add built-in templates
- scriptPath = os.path.dirname(os.path.realpath(__file__))
+ # Add built-in templates
+ scriptPath = os.path.dirname(os.path.realpath(__file__))
- candidateBuiltInTemplatePaths = [
- os.path.join(scriptPath, "..", "..", "..", "Utilities", "Templates"), # Run from source directory
- os.path.join(scriptPath, "..", "..", "..", "share", # Run from install
- "Slicer-%s.%s" % tuple(__version_info__[:2]),
- "Wizard", "Templates")
+ candidateBuiltInTemplatePaths = [
+ os.path.join(scriptPath, "..", "..", "..", "Utilities", "Templates"), # Run from source directory
+ os.path.join(scriptPath, "..", "..", "..", "share", # Run from install
+ "Slicer-%s.%s" % tuple(__version_info__[:2]),
+ "Wizard", "Templates")
]
- descriptionFileTemplate = None
- for candidate in candidateBuiltInTemplatePaths:
- if os.path.exists(candidate):
- self._templateManager.addPath(candidate)
- descriptionFileTemplate = os.path.join(candidate, "Extensions", "extension_description.s4ext.in")
- if descriptionFileTemplate is None or not os.path.exists(descriptionFileTemplate):
- logging.warning("failed to locate template 'Extensions/extension_description.s4ext.in' "
- "in these directories: %s" % candidateBuiltInTemplatePaths)
- else:
- ExtensionDescription.DESCRIPTION_FILE_TEMPLATE = descriptionFileTemplate
-
- # Add user-specified template paths and keys
- self._templateManager.parseArguments(args)
-
- acted = False
-
- # List available templates
- if args.listTemplates:
- self._templateManager.listTemplates()
- acted = True
-
- # Create requested extensions
- if args.create is not None:
- extArgs = args.create.split(":")
- extArgs.reverse()
- self.create(args, *extArgs)
- acted = True
-
- # Create requested modules
- if args.addModule is not None:
- for module in args.addModule:
- self.addModule(args, *module.split(":"))
- acted = True
-
- # Describe extension if requested
- if args.describe:
- self.describe(args)
- acted = True
-
- # Publish extension if requested
- if args.publish:
- self.publish(args)
- acted = True
-
- # Contribute extension if requested
- if args.contribute:
- self.contribute(args)
- acted = True
-
- # Check that we did something
- if not acted:
- die(("no action was requested!", "", parser.format_usage().rstrip()))
-
- # ---------------------------------------------------------------------------
- def execute(self, *args, **kwargs):
- """execute(*args, exit=True, **kwargs)
- Execute the wizard in |CLI| mode.
-
- :param exit:
- * ``True``: The call does not return and the application exits.
- * ``False``: The call returns an exit code, which is ``0`` if execution
- was successful, or non-zero otherwise.
- :type exit:
- :class:`bool`
- :param args:
- |CLI| arguments to use for execution.
- :type args:
- :class:`~collections.Sequence`
- :param kwargs:
- Named |CLI| options to use for execution.
- :type kwargs:
- :class:`dict`
-
- This sets up |CLI| argument parsing and executes the wizard, using the
- provided |CLI| arguments if any, or :attr:`sys.argv` otherwise. See
- :func:`.buildProcessArgs` for an explanation of how ``args`` and ``kwargs``
- are processed.
-
- If multiple commands are given, an error in one may cause others to be
- skipped.
-
- .. seealso:: :func:`.buildProcessArgs`
- """
-
- # Get values for non-CLI-argument named arguments
- exit = kwargs.pop('exit', True)
-
- # Convert other named arguments to CLI arguments
- args = buildProcessArgs(*args, **kwargs)
+ descriptionFileTemplate = None
+ for candidate in candidateBuiltInTemplatePaths:
+ if os.path.exists(candidate):
+ self._templateManager.addPath(candidate)
+ descriptionFileTemplate = os.path.join(candidate, "Extensions", "extension_description.s4ext.in")
+ if descriptionFileTemplate is None or not os.path.exists(descriptionFileTemplate):
+ logging.warning("failed to locate template 'Extensions/extension_description.s4ext.in' "
+ "in these directories: %s" % candidateBuiltInTemplatePaths)
+ else:
+ ExtensionDescription.DESCRIPTION_FILE_TEMPLATE = descriptionFileTemplate
+
+ # Add user-specified template paths and keys
+ self._templateManager.parseArguments(args)
+
+ acted = False
+
+ # List available templates
+ if args.listTemplates:
+ self._templateManager.listTemplates()
+ acted = True
+
+ # Create requested extensions
+ if args.create is not None:
+ extArgs = args.create.split(":")
+ extArgs.reverse()
+ self.create(args, *extArgs)
+ acted = True
+
+ # Create requested modules
+ if args.addModule is not None:
+ for module in args.addModule:
+ self.addModule(args, *module.split(":"))
+ acted = True
+
+ # Describe extension if requested
+ if args.describe:
+ self.describe(args)
+ acted = True
+
+ # Publish extension if requested
+ if args.publish:
+ self.publish(args)
+ acted = True
+
+ # Contribute extension if requested
+ if args.contribute:
+ self.contribute(args)
+ acted = True
+
+ # Check that we did something
+ if not acted:
+ die(("no action was requested!", "", parser.format_usage().rstrip()))
+
+ # ---------------------------------------------------------------------------
+ def execute(self, *args, **kwargs):
+ """execute(*args, exit=True, **kwargs)
+ Execute the wizard in |CLI| mode.
+
+ :param exit:
+ * ``True``: The call does not return and the application exits.
+ * ``False``: The call returns an exit code, which is ``0`` if execution
+ was successful, or non-zero otherwise.
+ :type exit:
+ :class:`bool`
+ :param args:
+ |CLI| arguments to use for execution.
+ :type args:
+ :class:`~collections.Sequence`
+ :param kwargs:
+ Named |CLI| options to use for execution.
+ :type kwargs:
+ :class:`dict`
+
+ This sets up |CLI| argument parsing and executes the wizard, using the
+ provided |CLI| arguments if any, or :attr:`sys.argv` otherwise. See
+ :func:`.buildProcessArgs` for an explanation of how ``args`` and ``kwargs``
+ are processed.
+
+ If multiple commands are given, an error in one may cause others to be
+ skipped.
+
+ .. seealso:: :func:`.buildProcessArgs`
+ """
+
+ # Get values for non-CLI-argument named arguments
+ exit = kwargs.pop('exit', True)
+
+ # Convert other named arguments to CLI arguments
+ args = buildProcessArgs(*args, **kwargs)
- try:
- self._execute(args if len(args) else None)
- sys.exit(0)
+ try:
+ self._execute(args if len(args) else None)
+ sys.exit(0)
- except SystemExit:
- if not exit:
- return sys.exc_info()[1].code
+ except SystemExit:
+ if not exit:
+ return sys.exc_info()[1].code
- raise
+ raise
diff --git a/Utilities/Scripts/SlicerWizard/GithubHelper.py b/Utilities/Scripts/SlicerWizard/GithubHelper.py
index bd59cf084cc..b089e35b5b9 100644
--- a/Utilities/Scripts/SlicerWizard/GithubHelper.py
+++ b/Utilities/Scripts/SlicerWizard/GithubHelper.py
@@ -10,305 +10,305 @@
from urllib.parse import urlparse
__all__ = [
- 'logIn',
- 'getRepo',
- 'getFork',
- 'getPullRequest',
+ 'logIn',
+ 'getRepo',
+ 'getFork',
+ 'getPullRequest',
]
# =============================================================================
class _CredentialToken:
- # ---------------------------------------------------------------------------
- def __init__(self, text=None, **kwargs):
- # Set attributes from named arguments
- self._keys = set(kwargs.keys())
- for k in kwargs:
- setattr(self, k, kwargs[k])
-
- # Set attributes from input text (i.e. 'git credential fill' output)
- if text is not None:
- for l in text.split("\n"):
- if "=" in l:
- t = l.split("=", 1)
- self._keys.add(t[0])
- setattr(self, t[0], t[1])
-
- # ---------------------------------------------------------------------------
- def __str__(self):
- # Return string representation suitable for being fed to 'git credential'
- lines = [f"{k}={getattr(self, k)}" for k in self._keys]
- return "%s\n\n" % "\n".join(lines)
+ # ---------------------------------------------------------------------------
+ def __init__(self, text=None, **kwargs):
+ # Set attributes from named arguments
+ self._keys = set(kwargs.keys())
+ for k in kwargs:
+ setattr(self, k, kwargs[k])
+
+ # Set attributes from input text (i.e. 'git credential fill' output)
+ if text is not None:
+ for l in text.split("\n"):
+ if "=" in l:
+ t = l.split("=", 1)
+ self._keys.add(t[0])
+ setattr(self, t[0], t[1])
+
+ # ---------------------------------------------------------------------------
+ def __str__(self):
+ # Return string representation suitable for being fed to 'git credential'
+ lines = [f"{k}={getattr(self, k)}" for k in self._keys]
+ return "%s\n\n" % "\n".join(lines)
# -----------------------------------------------------------------------------
def _credentials(client, request, action="fill"):
- # Set up and execute 'git credential' process, passing stringized token to
- # the process's stdin
- p = client.credential(action, as_process=True, istream=subprocess.PIPE)
- out, err = p.communicate(input=str(request).encode("utf-8"))
+ # Set up and execute 'git credential' process, passing stringized token to
+ # the process's stdin
+ p = client.credential(action, as_process=True, istream=subprocess.PIPE)
+ out, err = p.communicate(input=str(request).encode("utf-8"))
- # Raise exception if process failed
- if p.returncode != 0:
- raise git.GitCommandError(["credential", action], p.returncode,
- err.rstrip())
+ # Raise exception if process failed
+ if p.returncode != 0:
+ raise git.GitCommandError(["credential", action], p.returncode,
+ err.rstrip())
- # Return token parsed from the command's output
- return _CredentialToken(out.decode())
+ # Return token parsed from the command's output
+ return _CredentialToken(out.decode())
# -----------------------------------------------------------------------------
def logIn(repo=None):
- """Create github session.
+ """Create github session.
- :param repo:
- If not ``None``, use the git client (i.e. configuration) from the specified
- git repository; otherwise use a default client.
- :type repo:
- :class:`git.Repo ` or ``None``.
+ :param repo:
+ If not ``None``, use the git client (i.e. configuration) from the specified
+ git repository; otherwise use a default client.
+ :type repo:
+ :class:`git.Repo ` or ``None``.
- :returns: A logged in github session.
- :rtype: :class:`github.Github `.
+ :returns: A logged in github session.
+ :rtype: :class:`github.Github `.
- :raises:
- :class:`github:github.GithubException.BadCredentialsException` if
- authentication fails.
+ :raises:
+ :class:`github:github.GithubException.BadCredentialsException` if
+ authentication fails.
- This obtains and returns a logged in github session using the user's
- credentials, as managed by `git-credentials`_; login information is obtained
- as necessary via the same. On success, the credentials are also saved to any
- store that the user has configured.
+ This obtains and returns a logged in github session using the user's
+ credentials, as managed by `git-credentials`_; login information is obtained
+ as necessary via the same. On success, the credentials are also saved to any
+ store that the user has configured.
- If `GITHUB_TOKEN` environment variable is set, its value will be used
- as password when invoking `git-credentials`_.
+ If `GITHUB_TOKEN` environment variable is set, its value will be used
+ as password when invoking `git-credentials`_.
- .. _git-credentials: https://git-scm.com/docs/gitcredentials.html
- """
+ .. _git-credentials: https://git-scm.com/docs/gitcredentials.html
+ """
- # Get client; use generic client if no repository
- client = repo.git if repo is not None else git.cmd.Git()
+ # Get client; use generic client if no repository
+ client = repo.git if repo is not None else git.cmd.Git()
- # Request login credentials
- github_token = {}
- if "GITHUB_TOKEN" in os.environ:
- github_token = {"password": os.environ["GITHUB_TOKEN"]}
- credRequest = _CredentialToken(protocol="https", host="github.com", **github_token)
- cred = _credentials(client, credRequest)
+ # Request login credentials
+ github_token = {}
+ if "GITHUB_TOKEN" in os.environ:
+ github_token = {"password": os.environ["GITHUB_TOKEN"]}
+ credRequest = _CredentialToken(protocol="https", host="github.com", **github_token)
+ cred = _credentials(client, credRequest)
- # Log in
- session = Github(cred.username, cred.password)
+ # Log in
+ session = Github(cred.username, cred.password)
- # Try to get the logged in user name; will raise an exception if
- # authentication failed
- if session.get_user().login:
- # Save the credentials
- _credentials(client, cred, action="approve")
+ # Try to get the logged in user name; will raise an exception if
+ # authentication failed
+ if session.get_user().login:
+ # Save the credentials
+ _credentials(client, cred, action="approve")
- # Return github session
- return session
+ # Return github session
+ return session
# -----------------------------------------------------------------------------
def getRepo(session, name=None, url=None):
- """Get a github repository by name or URL.
-
- :param session:
- A github session object, e.g. as returned from :func:`.logIn`.
- :type session:
- :class:`github.Github ` or
- :class:`github:github.AuthenticatedUser.AuthenticatedUser`
- :param name:
- Name of the repository to look up.
- :type name:
- :class:`str` or ``None``
- :param url:
- Clone URL of the repository.
- :type url:
- :class:`str` or ``None``
-
- :returns: Matching repository, or ``None`` if no such repository was found.
- :rtype: :class:`github:github.Repository.Repository` or ``None``.
-
- This function attempts to look up a github repository by either its qualified
- github name (i.e. '**/**') or a clone URL:
-
- .. code-block:: python
-
- # Create session
- session = GithubHelper.logIn()
-
- # Look up repository by name
- repoA = GithubHelper.getRepo(session, 'octocat/Hello-World')
-
- # Look up repository by clone URL
- cloneUrl = 'https://github.com/octocat/Hello-World.git'
- repoB = GithubHelper.getRepo(session, cloneUrl)
-
- If both ``name`` and ``url`` are provided, only ``name`` is used. The ``url``
- must have "github.com" as the host.
- """
-
- try:
- # Look up repository by name
- if name is not None:
- return session.get_repo(name)
-
- # Look up repository by clone URL
- if url is not None:
- # Parse URL
- url = urlparse(url)
-
- # Check that this is a github URL
- if not url.hostname.endswith("github.com"):
- return None
-
- # Get repository name from clone URL
- name = url.path
- if name.startswith("/"):
- name = name[1:]
- if name.endswith(".git"):
- name = name[:-4]
+ """Get a github repository by name or URL.
+
+ :param session:
+ A github session object, e.g. as returned from :func:`.logIn`.
+ :type session:
+ :class:`github.Github ` or
+ :class:`github:github.AuthenticatedUser.AuthenticatedUser`
+ :param name:
+ Name of the repository to look up.
+ :type name:
+ :class:`str` or ``None``
+ :param url:
+ Clone URL of the repository.
+ :type url:
+ :class:`str` or ``None``
+
+ :returns: Matching repository, or ``None`` if no such repository was found.
+ :rtype: :class:`github:github.Repository.Repository` or ``None``.
+
+ This function attempts to look up a github repository by either its qualified
+ github name (i.e. '**/**') or a clone URL:
+
+ .. code-block:: python
+
+ # Create session
+ session = GithubHelper.logIn()
# Look up repository by name
- return getRepo(session, name=name)
+ repoA = GithubHelper.getRepo(session, 'octocat/Hello-World')
+
+ # Look up repository by clone URL
+ cloneUrl = 'https://github.com/octocat/Hello-World.git'
+ repoB = GithubHelper.getRepo(session, cloneUrl)
+
+ If both ``name`` and ``url`` are provided, only ``name`` is used. The ``url``
+ must have "github.com" as the host.
+ """
+
+ try:
+ # Look up repository by name
+ if name is not None:
+ return session.get_repo(name)
+
+ # Look up repository by clone URL
+ if url is not None:
+ # Parse URL
+ url = urlparse(url)
+
+ # Check that this is a github URL
+ if not url.hostname.endswith("github.com"):
+ return None
+
+ # Get repository name from clone URL
+ name = url.path
+ if name.startswith("/"):
+ name = name[1:]
+ if name.endswith(".git"):
+ name = name[:-4]
+
+ # Look up repository by name
+ return getRepo(session, name=name)
- except:
- pass
+ except:
+ pass
- return None
+ return None
# -----------------------------------------------------------------------------
def getFork(user, upstream, create=False):
- """Get user's fork of the specified repository.
+ """Get user's fork of the specified repository.
- :param user:
- Github user or organization which owns the requested fork.
- :type user:
- :class:`github:github.NamedUser.NamedUser`,
- :class:`github:github.AuthenticatedUser.AuthenticatedUser` or
- :class:`github:github.Organization.Organization`
- :param upstream:
- Upstream repository of the requested fork.
- :type upstream:
- :class:`github:github.Repository.Repository`
- :param create:
- If ``True``, create the forked repository if no such fork exists.
- :type create:
- :class:`bool`
+ :param user:
+ Github user or organization which owns the requested fork.
+ :type user:
+ :class:`github:github.NamedUser.NamedUser`,
+ :class:`github:github.AuthenticatedUser.AuthenticatedUser` or
+ :class:`github:github.Organization.Organization`
+ :param upstream:
+ Upstream repository of the requested fork.
+ :type upstream:
+ :class:`github:github.Repository.Repository`
+ :param create:
+ If ``True``, create the forked repository if no such fork exists.
+ :type create:
+ :class:`bool`
- :return:
- The specified fork repository, or ``None`` if no such fork exists and
- ``create`` is ``False``.
- :rtype:
- :class:`github:github.Repository.Repository` or ``None``.
+ :return:
+ The specified fork repository, or ``None`` if no such fork exists and
+ ``create`` is ``False``.
+ :rtype:
+ :class:`github:github.Repository.Repository` or ``None``.
- :raises:
- :class:`github:github.GithubException.GithubException` if ``user`` does not
- have permission to create a repository.
+ :raises:
+ :class:`github:github.GithubException.GithubException` if ``user`` does not
+ have permission to create a repository.
- This function attempts to look up a repository owned by the specified user or
- organization which is a fork of the specified upstream repository, optionally
- creating one if it does not exist:
+ This function attempts to look up a repository owned by the specified user or
+ organization which is a fork of the specified upstream repository, optionally
+ creating one if it does not exist:
- .. code-block:: python
+ .. code-block:: python
- # Create session
- session = GithubHelper.logIn()
+ # Create session
+ session = GithubHelper.logIn()
- # Get user
- user = session.get_user("jdoe")
+ # Get user
+ user = session.get_user("jdoe")
- # Get upstream repository
- upstream = GithubHelper.getRepo(session, 'octocat/Spoon-Knife')
+ # Get upstream repository
+ upstream = GithubHelper.getRepo(session, 'octocat/Spoon-Knife')
- # Look up fork
- fork = GithubHelper.getFork(user=user, upstream=upstream)
- """
+ # Look up fork
+ fork = GithubHelper.getFork(user=user, upstream=upstream)
+ """
- repo = user.get_repo(upstream.name)
- if repo.fork and repo.parent.url == upstream.url:
- return repo
+ repo = user.get_repo(upstream.name)
+ if repo.fork and repo.parent.url == upstream.url:
+ return repo
- if create:
- return user.create_fork(upstream)
+ if create:
+ return user.create_fork(upstream)
- return None
+ return None
# -----------------------------------------------------------------------------
def getPullRequest(upstream, ref, user=None, fork=None, target=None):
- """Get pull request for the specified user's fork and ref.
-
- :param upstream:
- Upstream (target) repository of the requested pull request.
- :type upstream:
- :class:`github:github.Repository.Repository`
- :param user:
- Github user or organization which owns the requested pull request.
- :type user:
- :class:`github:github.NamedUser.NamedUser`,
- :class:`github:github.AuthenticatedUser.AuthenticatedUser`,
- :class:`github:github.Organization.Organization` or ``None``
- :param ref:
- Branch name or git ref of the requested pull request.
- :type ref:
- :class:`str`
- :param fork:
- Downstream (fork) repository of the requested pull request.
- :type fork:
- :class:`github:github.Repository.Repository` or ``None``
- :param target:
- Branch name or git ref of the requested pull request target.
- :type target:
- :class:`str` or ``None``
-
- :return:
- The specified pull request, or ``None`` if no such pull request exists.
- :rtype:
- :class:`github:github.PullRequest.PullRequest` or ``None``.
-
- This function attempts to look up the pull request made by ``user`` for
- ``upstream`` to integrate the user's ``ref`` into upstream's ``target``:
-
- .. code-block:: python
-
- # Create session
- session = GithubHelper.logIn()
-
- # Get user and upstream repository
- user = session.get_user("jdoe")
- repo = GithubHelper.getRepo(session, 'octocat/Hello-World')
-
- # Look up request to merge 'my-branch' of any fork into 'master'
- pr = GithubHelper.getPullRequest(upstream=repo, user=user,
- ref='my-branch', target='master')
-
- If any of ``user``, ``fork`` or ``target`` are ``None``, those criteria are
- not considered when searching for a matching pull request. If multiple
- matching requests exist, the first matching request is returned.
- """
-
- if user is not None:
- user = user.login
-
- for p in upstream.get_pulls():
- # Check candidate request against specified criteria
- if p.head.ref != ref:
- continue
-
- if user is not None and p.head.user.login != user:
- continue
-
- if fork is not None and p.head.repo.url != fork.url:
- continue
-
- if target is not None and p.base.ref != target:
- continue
-
- # If we get here, we found a match
- return p
-
- # No match
- return None
+ """Get pull request for the specified user's fork and ref.
+
+ :param upstream:
+ Upstream (target) repository of the requested pull request.
+ :type upstream:
+ :class:`github:github.Repository.Repository`
+ :param user:
+ Github user or organization which owns the requested pull request.
+ :type user:
+ :class:`github:github.NamedUser.NamedUser`,
+ :class:`github:github.AuthenticatedUser.AuthenticatedUser`,
+ :class:`github:github.Organization.Organization` or ``None``
+ :param ref:
+ Branch name or git ref of the requested pull request.
+ :type ref:
+ :class:`str`
+ :param fork:
+ Downstream (fork) repository of the requested pull request.
+ :type fork:
+ :class:`github:github.Repository.Repository` or ``None``
+ :param target:
+ Branch name or git ref of the requested pull request target.
+ :type target:
+ :class:`str` or ``None``
+
+ :return:
+ The specified pull request, or ``None`` if no such pull request exists.
+ :rtype:
+ :class:`github:github.PullRequest.PullRequest` or ``None``.
+
+ This function attempts to look up the pull request made by ``user`` for
+ ``upstream`` to integrate the user's ``ref`` into upstream's ``target``:
+
+ .. code-block:: python
+
+ # Create session
+ session = GithubHelper.logIn()
+
+ # Get user and upstream repository
+ user = session.get_user("jdoe")
+ repo = GithubHelper.getRepo(session, 'octocat/Hello-World')
+
+ # Look up request to merge 'my-branch' of any fork into 'master'
+ pr = GithubHelper.getPullRequest(upstream=repo, user=user,
+ ref='my-branch', target='master')
+
+ If any of ``user``, ``fork`` or ``target`` are ``None``, those criteria are
+ not considered when searching for a matching pull request. If multiple
+ matching requests exist, the first matching request is returned.
+ """
+
+ if user is not None:
+ user = user.login
+
+ for p in upstream.get_pulls():
+ # Check candidate request against specified criteria
+ if p.head.ref != ref:
+ continue
+
+ if user is not None and p.head.user.login != user:
+ continue
+
+ if fork is not None and p.head.repo.url != fork.url:
+ continue
+
+ if target is not None and p.base.ref != target:
+ continue
+
+ # If we get here, we found a match
+ return p
+
+ # No match
+ return None
diff --git a/Utilities/Scripts/SlicerWizard/Subversion.py b/Utilities/Scripts/SlicerWizard/Subversion.py
index 07206303e72..cdc3da1f1fe 100644
--- a/Utilities/Scripts/SlicerWizard/Subversion.py
+++ b/Utilities/Scripts/SlicerWizard/Subversion.py
@@ -6,203 +6,203 @@
from .Utilities import *
__all__ = [
- 'Client',
- 'Repository',
+ 'Client',
+ 'Repository',
]
# =============================================================================
class CommandError(Exception):
- """
- .. attribute:: command
+ """
+ .. attribute:: command
- Complete command (including arguments) which experienced the error.
+ Complete command (including arguments) which experienced the error.
- .. attribute:: code
+ .. attribute:: code
- Command's status code.
+ Command's status code.
- .. attribute:: stderr
+ .. attribute:: stderr
- Raw text of the command's standard error stream.
- """
+ Raw text of the command's standard error stream.
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, command, code, stderr):
- super(Exception, self).__init__("%r command exited with non-zero status" %
- command[0])
- self.command = command
- self.code = code
- self.stderr = stderr
+ # ---------------------------------------------------------------------------
+ def __init__(self, command, code, stderr):
+ super(Exception, self).__init__("%r command exited with non-zero status" %
+ command[0])
+ self.command = command
+ self.code = code
+ self.stderr = stderr
# =============================================================================
class Client:
- """Wrapper for executing the ``svn`` process.
+ """Wrapper for executing the ``svn`` process.
- This class provides a convenience wrapping for invoking the ``svn`` process.
- In addition to the :meth:`~Client.execute` method, names of subversion
- commands are implicitly available as methods:
+ This class provides a convenience wrapping for invoking the ``svn`` process.
+ In addition to the :meth:`~Client.execute` method, names of subversion
+ commands are implicitly available as methods:
- .. code-block:: python
+ .. code-block:: python
- c = Subversion.Client()
- c.log('.', limit=5)
- """
+ c = Subversion.Client()
+ c.log('.', limit=5)
+ """
- # ---------------------------------------------------------------------------
- def __init__(self, repo=None):
- self._wc_root = repo.wc_root if repo is not None else None
+ # ---------------------------------------------------------------------------
+ def __init__(self, repo=None):
+ self._wc_root = repo.wc_root if repo is not None else None
- # ---------------------------------------------------------------------------
- def __getattr__(self, name):
- """Return a lambda to invoke the svn command ``name``."""
+ # ---------------------------------------------------------------------------
+ def __getattr__(self, name):
+ """Return a lambda to invoke the svn command ``name``."""
- if name[0] == "_":
- raise AttributeError("%r object has no attribute %r" %
- (self.__class__.__name__, name))
+ if name[0] == "_":
+ raise AttributeError("%r object has no attribute %r" %
+ (self.__class__.__name__, name))
- return lambda *args, **kwargs: self.execute(name, *args, **kwargs)
+ return lambda *args, **kwargs: self.execute(name, *args, **kwargs)
- # ---------------------------------------------------------------------------
- def execute(self, command, *args, **kwargs):
- """Execute ``command`` and return line-split output.
+ # ---------------------------------------------------------------------------
+ def execute(self, command, *args, **kwargs):
+ """Execute ``command`` and return line-split output.
- :param args: Subversion command to execute.
- :type args: :class:`str`
- :param args: Arguments to pass to ``command``.
- :type args: :class:`~collections.Sequence`
- :param kwargs: Named options to pass to ``command``.
- :type kwargs: :class:`dict`
+ :param args: Subversion command to execute.
+ :type args: :class:`str`
+ :param args: Arguments to pass to ``command``.
+ :type args: :class:`~collections.Sequence`
+ :param kwargs: Named options to pass to ``command``.
+ :type kwargs: :class:`dict`
- :return:
- Standard output from running the command, as a list (split by line).
- :rtype:
- :class:`list` of :class:`str`
+ :return:
+ Standard output from running the command, as a list (split by line).
+ :rtype:
+ :class:`list` of :class:`str`
- :raises: :class:`.CommandError` if the command exits with non-zero status.
+ :raises: :class:`.CommandError` if the command exits with non-zero status.
- This executes the specified ``svn`` command and returns the standard output
- from the execution. See :func:`.buildProcessArgs` for an explanation of how
- ``args`` and ``kwargs`` are processed.
+ This executes the specified ``svn`` command and returns the standard output
+ from the execution. See :func:`.buildProcessArgs` for an explanation of how
+ ``args`` and ``kwargs`` are processed.
- .. seealso:: :func:`.buildProcessArgs`
- """
+ .. seealso:: :func:`.buildProcessArgs`
+ """
- command = ["svn", command] + buildProcessArgs(*args, **kwargs)
- cwd = self._wc_root if self._wc_root is not None else os.getcwd()
+ command = ["svn", command] + buildProcessArgs(*args, **kwargs)
+ cwd = self._wc_root if self._wc_root is not None else os.getcwd()
- proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE,
- stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ proc = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE,
+ stderr=subprocess.PIPE, stdout=subprocess.PIPE)
- out, err = proc.communicate()
+ out, err = proc.communicate()
- # Raise exception if process failed
- if proc.returncode != 0:
- raise CommandError(command, proc.returncode, err)
+ # Raise exception if process failed
+ if proc.returncode != 0:
+ raise CommandError(command, proc.returncode, err)
- # Strip trailing newline(s)
- while out.endswith("\n"):
- out = out[:-1]
+ # Strip trailing newline(s)
+ while out.endswith("\n"):
+ out = out[:-1]
- return out.split("\n")
+ return out.split("\n")
- # ---------------------------------------------------------------------------
- def info(self, *args, **kwargs):
- """Return information about the specified item.
+ # ---------------------------------------------------------------------------
+ def info(self, *args, **kwargs):
+ """Return information about the specified item.
- :type args: :class:`str`
- :param args: Arguments to pass to ``svn info``.
- :type args: :class:`~collections.Sequence`
- :param kwargs: Named options to pass to ``svn info``.
- :type kwargs: :class:`dict`
+ :type args: :class:`str`
+ :param args: Arguments to pass to ``svn info``.
+ :type args: :class:`~collections.Sequence`
+ :param kwargs: Named options to pass to ``svn info``.
+ :type kwargs: :class:`dict`
- :return: Mapping of information fields returned by ``svn info``.
- :rtype: :class:`dict` of :class:`str` |rarr| :class:`str`
+ :return: Mapping of information fields returned by ``svn info``.
+ :rtype: :class:`dict` of :class:`str` |rarr| :class:`str`
- :raises: :class:`.CommandError` if the command exits with non-zero status.
+ :raises: :class:`.CommandError` if the command exits with non-zero status.
- This wraps the ``svn info`` command, returning the resulting information as
- a :class:`dict`. The dictionary keys are the value names as printed by
- ``svn info``.
+ This wraps the ``svn info`` command, returning the resulting information as
+ a :class:`dict`. The dictionary keys are the value names as printed by
+ ``svn info``.
- .. |rarr| unicode:: U+02192 .. right arrow
- """
+ .. |rarr| unicode:: U+02192 .. right arrow
+ """
- out = self.execute("info", *args, **kwargs)
+ out = self.execute("info", *args, **kwargs)
- result = {}
- for line in out:
- parts = line.split(": ", 1)
- result[parts[0]] = parts[1]
+ result = {}
+ for line in out:
+ parts = line.split(": ", 1)
+ result[parts[0]] = parts[1]
- return result
+ return result
# =============================================================================
class Repository:
- """Abstract representation of a subversion repository.
+ """Abstract representation of a subversion repository.
- .. attribute:: url
+ .. attribute:: url
- The remote URL of the base of the working copy checkout.
+ The remote URL of the base of the working copy checkout.
- .. attribute:: root_url
+ .. attribute:: root_url
- The root URL of the remote repository.
+ The root URL of the remote repository.
- .. attribute:: uuid
+ .. attribute:: uuid
- The universally unique identifier of the repository.
+ The universally unique identifier of the repository.
- .. attribute:: wc_root
+ .. attribute:: wc_root
- The absolute path to the top level directory of the repository working copy.
+ The absolute path to the top level directory of the repository working copy.
- .. attribute:: revision
+ .. attribute:: revision
- The revision at which the working copy is checked out.
+ The revision at which the working copy is checked out.
- .. attribute:: last_change_revision
+ .. attribute:: last_change_revision
- The last revision which contains a change to content contained in the
- working copy.
+ The last revision which contains a change to content contained in the
+ working copy.
- .. attribute:: svn_dir
+ .. attribute:: svn_dir
- The absolute path to the working copy ``.svn`` directory.
+ The absolute path to the working copy ``.svn`` directory.
- .. attribute:: client
+ .. attribute:: client
- A :class:`.Client` object which may be used to interact with the repository.
- The client interprets non-absolute paths as relative to the working copy
- root.
- """
-
- # ---------------------------------------------------------------------------
- def __init__(self, path=os.getcwd()):
- """
- :param path: Location of the repository checkout.
- :type path: :class:`str`
-
- :raises:
- * :exc:`.CommandError` if the request to get the repository information
- fails (e.g. if ``path`` is not a repository).
- * :exc:`~exceptions.KeyError` if the repository information is missing a
- required value.
+ A :class:`.Client` object which may be used to interact with the repository.
+ The client interprets non-absolute paths as relative to the working copy
+ root.
"""
- c = Client()
- info = c.info(path)
- info = c.info(info["Working Copy Root Path"])
-
- self.url = info["URL"]
- self.root_url = info["Repository Root"]
- self.uuid = info["Repository UUID"]
- self.wc_root = info["Working Copy Root Path"]
- self.revision = info["Revision"]
- self.last_change_revision = info["Last Changed Rev"]
-
- self.svn_dir = os.path.join(self.wc_root, ".svn")
-
- self.client = Client(self)
+ # ---------------------------------------------------------------------------
+ def __init__(self, path=os.getcwd()):
+ """
+ :param path: Location of the repository checkout.
+ :type path: :class:`str`
+
+ :raises:
+ * :exc:`.CommandError` if the request to get the repository information
+ fails (e.g. if ``path`` is not a repository).
+ * :exc:`~exceptions.KeyError` if the repository information is missing a
+ required value.
+ """
+
+ c = Client()
+ info = c.info(path)
+ info = c.info(info["Working Copy Root Path"])
+
+ self.url = info["URL"]
+ self.root_url = info["Repository Root"]
+ self.uuid = info["Repository UUID"]
+ self.wc_root = info["Working Copy Root Path"]
+ self.revision = info["Revision"]
+ self.last_change_revision = info["Last Changed Rev"]
+
+ self.svn_dir = os.path.join(self.wc_root, ".svn")
+
+ self.client = Client(self)
diff --git a/Utilities/Scripts/SlicerWizard/TemplateManager.py b/Utilities/Scripts/SlicerWizard/TemplateManager.py
index cd63a34eaf7..6d5cd5a160e 100644
--- a/Utilities/Scripts/SlicerWizard/TemplateManager.py
+++ b/Utilities/Scripts/SlicerWizard/TemplateManager.py
@@ -5,398 +5,398 @@
from .Utilities import die, detectEncoding
_sourcePatterns = [
- "*.h",
- "*.cxx",
- "*.cpp",
- "CMakeLists.txt",
- "*.cmake",
- "*.ui",
- "*.qrc",
- "*.py",
- "*.xml",
- "*.xml.in",
- "*.md5",
- "*.png",
- "*.dox",
- "*.sha256",
+ "*.h",
+ "*.cxx",
+ "*.cpp",
+ "CMakeLists.txt",
+ "*.cmake",
+ "*.ui",
+ "*.qrc",
+ "*.py",
+ "*.xml",
+ "*.xml.in",
+ "*.md5",
+ "*.png",
+ "*.dox",
+ "*.sha256",
]
_templateCategories = [
- "extensions",
- "modules",
+ "extensions",
+ "modules",
]
# -----------------------------------------------------------------------------
def _isSourceFile(name):
- for pat in _sourcePatterns:
- if fnmatch.fnmatch(name, pat):
- return True
+ for pat in _sourcePatterns:
+ if fnmatch.fnmatch(name, pat):
+ return True
- return False
+ return False
# -----------------------------------------------------------------------------
def _isTemplateCategory(name, relPath):
- if not os.path.isdir(os.path.join(relPath, name)):
- return False
+ if not os.path.isdir(os.path.join(relPath, name)):
+ return False
- name = name.lower()
- return name in _templateCategories
+ name = name.lower()
+ return name in _templateCategories
# -----------------------------------------------------------------------------
def _listSources(directory):
- for root, subFolders, files in os.walk(directory):
- for f in files:
- if _isSourceFile(f):
- f = os.path.join(root, f)
- yield f[len(directory) + 1:] # strip common dir
+ for root, subFolders, files in os.walk(directory):
+ for f in files:
+ if _isSourceFile(f):
+ f = os.path.join(root, f)
+ yield f[len(directory) + 1:] # strip common dir
# =============================================================================
class TemplateManager:
- """Template collection manager.
-
- This class provides a template collection and operations for managing and
- using that collection.
- """
-
- # ---------------------------------------------------------------------------
- def __init__(self):
- self._paths = {}
- self._keys = {}
-
- for c in _templateCategories:
- self._paths[c] = {}
-
- # ---------------------------------------------------------------------------
- def _getKey(self, kind):
- if kind in self._keys:
- return self._keys[kind]
-
- return "TemplateKey"
-
- # ---------------------------------------------------------------------------
- def _copyAndReplace(self, inFile, template, destination, key, name):
- outFile = os.path.join(destination, inFile.replace(key, name))
- logging.info("creating '%s'" % outFile)
- path = os.path.dirname(outFile)
- if not os.path.exists(path):
- os.makedirs(path)
-
- # Read file contents
- p = os.path.join(template, inFile)
- with open(p, "rb") as fp:
- contents = fp.read()
-
- # Replace template key with copy name
- if isinstance(name, bytes):
- # If replacement is just bytes, we can just do the replacement...
- contents = contents.replace(key, name)
- contents = contents.replace(key.upper(), name.upper())
-
- else:
- # ...else we have to try to guess the template file encoding in order to
- # convert it to unicode and back
- encoding, confidence = detectEncoding(contents)
-
- if encoding is not None:
- if confidence < 0.5:
- logging.warning("%s: encoding detection confidence is %f:"
- " copied file might be corrupt" % (p, confidence))
-
- contents = contents.decode(encoding)
- contents = contents.replace(key, name)
- contents = contents.replace(key.upper(), name.upper())
- contents = contents.encode(encoding)
-
- else:
- # Looks like a binary file; don't perform replacement
- pass
-
- # Write adjusted contents
- with open(outFile, "wb") as fp:
- fp.write(contents)
-
- # ---------------------------------------------------------------------------
- def copyTemplate(self, destination, category, kind, name, createInSubdirectory=True, requireEmptyDirectory=True):
- """Copy (instantiate) a template.
-
- :param destination: Directory in which to create the template copy.
- :type destination: :class:`str`
- :param category: Category of template to instantiate.
- :type category: :class:`str`
- :param kind: Name of template to instantiate.
- :type kind: :class:`str`
- :param name: Name for the instantiated template.
- :type name: :class:`str`
- :param createInSubdirectory: If True then files are copied to ``destination/name/``, else ``destination/``.
- :type name: :class:`bool`
- :param requireEmptyDirectory: If True then files are only copied if the target directory is empty.
- :type name: :class:`bool`
-
- :return:
- Path to the new instance (``os.path.join(destination, name)``).
- :rtype:
- :class:`unicode` if either ``destination`` and/or ``name`` is also
- :class:`unicode`, otherwise :class:`str`.
-
- :raises:
- * :exc:`~exceptions.KeyError` if the specified template is not found.
- * :exc:`~exceptions.IOError` if a subdirectory ``name`` already exists.
-
- This creates a copy of the specified template in ``destination``, with
- occurrences of the template's key replaced with ``name``. The new copy is
- in a subdirectory ``name``, which must not exist.
-
- .. note:: The replacement of the template key is case sensitive, however \
- the upper-case key is also replaced with the upper-case ``name``.
-
- .. seealso:: :meth:`.setKey`
- """
-
- templates = self._paths[category]
- if not kind.lower() in templates:
- raise KeyError("'%s' is not a known extension template" % kind)
-
- kind = kind.lower()
-
- if createInSubdirectory:
- destination = os.path.join(destination, name)
-
- if requireEmptyDirectory and os.path.exists(destination):
- raise OSError("create %s: refusing to overwrite"
- " existing directory '%s'" % (category, destination))
-
- template = templates[kind]
- key = self._getKey(kind)
-
- logging.info("copy template '%s' to '%s', replacing '%s' -> '%s'" %
- (template, destination, key, name))
- for f in _listSources(template):
- self._copyAndReplace(f, template, destination, key, name)
-
- return destination
-
- # ---------------------------------------------------------------------------
- def addCategoryPath(self, category, path):
- """Add templates for a particular category to the collection.
-
- :param category: Category of templates to add.
- :type category: :class:`str`
- :param path: Path to a directory containing templates.
- :type path: :class:`str`
-
- :raises:
- :exc:`~exceptions.KeyError` if ``category`` is not a known template
- category.
-
- This adds all templates found in ``path`` to ``category`` to the collection,
- where each subdirectory of ``path`` is a template. If ``path`` contains any
- templates whose names already exist in the ``category`` of the collection
- (case insensitive), the existing entries are replaced.
- """
-
- for entry in os.listdir(path):
- entryPath = os.path.join(path, entry)
- if os.path.isdir(entryPath):
- self._paths[category][entry.lower()] = entryPath
-
- # ---------------------------------------------------------------------------
- def addPath(self, basePath):
- """Add a template path to the collection.
-
- :param basePath: Path to a directory containing categorized templates.
- :type basePath: :class:`str`
+ """Template collection manager.
- This adds categorized templates to the collection. ``basePath`` should be
- a directory which contains one or more directories whose names match a
- known template category (case insensitive). Each such subdirectory is added
- to the collection via :meth:`.addCategoryPath`.
+ This class provides a template collection and operations for managing and
+ using that collection.
"""
- if not os.path.exists(basePath):
- return
+ # ---------------------------------------------------------------------------
+ def __init__(self):
+ self._paths = {}
+ self._keys = {}
- basePath = os.path.realpath(basePath)
+ for c in _templateCategories:
+ self._paths[c] = {}
- for entry in os.listdir(basePath):
- if _isTemplateCategory(entry, basePath):
- self.addCategoryPath(entry.lower(), os.path.join(basePath, entry))
+ # ---------------------------------------------------------------------------
+ def _getKey(self, kind):
+ if kind in self._keys:
+ return self._keys[kind]
- # ---------------------------------------------------------------------------
- def setKey(self, name, value):
- """Set template key for specified template.
+ return "TemplateKey"
- :param name: Name of template for which to set key.
- :type name: :class:`str`
- :param key: Key for specified template.
- :type name: :class:`str`
+ # ---------------------------------------------------------------------------
+ def _copyAndReplace(self, inFile, template, destination, key, name):
+ outFile = os.path.join(destination, inFile.replace(key, name))
+ logging.info("creating '%s'" % outFile)
+ path = os.path.dirname(outFile)
+ if not os.path.exists(path):
+ os.makedirs(path)
- This sets the template key for ``name`` to ``key``.
+ # Read file contents
+ p = os.path.join(template, inFile)
+ with open(p, "rb") as fp:
+ contents = fp.read()
- .. 'note' directive needs '\' to span multiple lines!
- .. note:: Template keys depend only on the template name, and not the \
- template category. As a result, two templates with the same name \
- in different categories will use the same key.
+ # Replace template key with copy name
+ if isinstance(name, bytes):
+ # If replacement is just bytes, we can just do the replacement...
+ contents = contents.replace(key, name)
+ contents = contents.replace(key.upper(), name.upper())
- .. seealso:: :meth:`.copyTemplate`
- """
-
- self._keys[name] = value
+ else:
+ # ...else we have to try to guess the template file encoding in order to
+ # convert it to unicode and back
+ encoding, confidence = detectEncoding(contents)
+
+ if encoding is not None:
+ if confidence < 0.5:
+ logging.warning("%s: encoding detection confidence is %f:"
+ " copied file might be corrupt" % (p, confidence))
+
+ contents = contents.decode(encoding)
+ contents = contents.replace(key, name)
+ contents = contents.replace(key.upper(), name.upper())
+ contents = contents.encode(encoding)
+
+ else:
+ # Looks like a binary file; don't perform replacement
+ pass
+
+ # Write adjusted contents
+ with open(outFile, "wb") as fp:
+ fp.write(contents)
+
+ # ---------------------------------------------------------------------------
+ def copyTemplate(self, destination, category, kind, name, createInSubdirectory=True, requireEmptyDirectory=True):
+ """Copy (instantiate) a template.
+
+ :param destination: Directory in which to create the template copy.
+ :type destination: :class:`str`
+ :param category: Category of template to instantiate.
+ :type category: :class:`str`
+ :param kind: Name of template to instantiate.
+ :type kind: :class:`str`
+ :param name: Name for the instantiated template.
+ :type name: :class:`str`
+ :param createInSubdirectory: If True then files are copied to ``destination/name/``, else ``destination/``.
+ :type name: :class:`bool`
+ :param requireEmptyDirectory: If True then files are only copied if the target directory is empty.
+ :type name: :class:`bool`
+
+ :return:
+ Path to the new instance (``os.path.join(destination, name)``).
+ :rtype:
+ :class:`unicode` if either ``destination`` and/or ``name`` is also
+ :class:`unicode`, otherwise :class:`str`.
+
+ :raises:
+ * :exc:`~exceptions.KeyError` if the specified template is not found.
+ * :exc:`~exceptions.IOError` if a subdirectory ``name`` already exists.
+
+ This creates a copy of the specified template in ``destination``, with
+ occurrences of the template's key replaced with ``name``. The new copy is
+ in a subdirectory ``name``, which must not exist.
+
+ .. note:: The replacement of the template key is case sensitive, however \
+ the upper-case key is also replaced with the upper-case ``name``.
+
+ .. seealso:: :meth:`.setKey`
+ """
- # ---------------------------------------------------------------------------
- @classmethod
- def categories(cls):
- """Get list of known template categories.
+ templates = self._paths[category]
+ if not kind.lower() in templates:
+ raise KeyError("'%s' is not a known extension template" % kind)
- :rtype: :class:`list` of :class:`str`.
+ kind = kind.lower()
- .. seealso:: :meth:`templates`, :meth:`.listTemplates`
- """
+ if createInSubdirectory:
+ destination = os.path.join(destination, name)
- return list(_templateCategories)
+ if requireEmptyDirectory and os.path.exists(destination):
+ raise OSError("create %s: refusing to overwrite"
+ " existing directory '%s'" % (category, destination))
+
+ template = templates[kind]
+ key = self._getKey(kind)
- # ---------------------------------------------------------------------------
- def templates(self, category=None):
- """Get collection of available templates.
+ logging.info("copy template '%s' to '%s', replacing '%s' -> '%s'" %
+ (template, destination, key, name))
+ for f in _listSources(template):
+ self._copyAndReplace(f, template, destination, key, name)
+
+ return destination
+
+ # ---------------------------------------------------------------------------
+ def addCategoryPath(self, category, path):
+ """Add templates for a particular category to the collection.
+
+ :param category: Category of templates to add.
+ :type category: :class:`str`
+ :param path: Path to a directory containing templates.
+ :type path: :class:`str`
+
+ :raises:
+ :exc:`~exceptions.KeyError` if ``category`` is not a known template
+ category.
+
+ This adds all templates found in ``path`` to ``category`` to the collection,
+ where each subdirectory of ``path`` is a template. If ``path`` contains any
+ templates whose names already exist in the ``category`` of the collection
+ (case insensitive), the existing entries are replaced.
+ """
+
+ for entry in os.listdir(path):
+ entryPath = os.path.join(path, entry)
+ if os.path.isdir(entryPath):
+ self._paths[category][entry.lower()] = entryPath
+
+ # ---------------------------------------------------------------------------
+ def addPath(self, basePath):
+ """Add a template path to the collection.
+
+ :param basePath: Path to a directory containing categorized templates.
+ :type basePath: :class:`str`
- :param category: Category of templates to query.
- :type name: :class:`str`
+ This adds categorized templates to the collection. ``basePath`` should be
+ a directory which contains one or more directories whose names match a
+ known template category (case insensitive). Each such subdirectory is added
+ to the collection via :meth:`.addCategoryPath`.
+ """
- :return:
- List of templates for the specified category, or a dictionary of such
- (keyed by category name) if ``category`` is ``None``.
- :rtype:
- :class:`list` of :class:`str`, or :class:`dict` of
- :class:`str` |rarr| (:class:`list` of :class:`str`).
+ if not os.path.exists(basePath):
+ return
- :raises:
- :exc:`~exceptions.KeyError` if ``category`` is not ``None`` or a known
- template category.
+ basePath = os.path.realpath(basePath)
- .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`,
- :meth:`.listTemplates`
- """
+ for entry in os.listdir(basePath):
+ if _isTemplateCategory(entry, basePath):
+ self.addCategoryPath(entry.lower(), os.path.join(basePath, entry))
- if category is None:
- result = {}
- for c in _templateCategories:
- result[c] = list(self._paths[c].keys())
- return result
+ # ---------------------------------------------------------------------------
+ def setKey(self, name, value):
+ """Set template key for specified template.
- else:
- return tuple(self._paths[category].keys())
+ :param name: Name of template for which to set key.
+ :type name: :class:`str`
+ :param key: Key for specified template.
+ :type name: :class:`str`
- # ---------------------------------------------------------------------------
- def listTemplates(self):
- """List available templates.
+ This sets the template key for ``name`` to ``key``.
- This displays a list of all available templates, using :func:`logging.info`,
- organized by category.
+ .. 'note' directive needs '\' to span multiple lines!
+ .. note:: Template keys depend only on the template name, and not the \
+ template category. As a result, two templates with the same name \
+ in different categories will use the same key.
- .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`,
- :meth:`.templates`
- """
+ .. seealso:: :meth:`.copyTemplate`
+ """
- for c in _templateCategories:
- logging.info("Available templates for category '%s':" % c)
+ self._keys[name] = value
- if len(self._paths[c]):
- for t in sorted(self._paths[c].keys()):
- logging.info(f" '{t}' ('{self._getKey(t)}')")
+ # ---------------------------------------------------------------------------
+ @classmethod
+ def categories(cls):
+ """Get list of known template categories.
- else:
- logging.info(" (none)")
+ :rtype: :class:`list` of :class:`str`.
- logging.info("")
+ .. seealso:: :meth:`templates`, :meth:`.listTemplates`
+ """
- # ---------------------------------------------------------------------------
- def addArguments(self, parser):
- """Add template manager |CLI| arguments to parser.
+ return list(_templateCategories)
- :param parser: Argument parser instance to which to add arguments.
- :type parser: :class:`argparse.ArgumentParser`
+ # ---------------------------------------------------------------------------
+ def templates(self, category=None):
+ """Get collection of available templates.
- This adds |CLI| arguments to the specified ``parser`` that may be used to
- interact with the template collection.
+ :param category: Category of templates to query.
+ :type name: :class:`str`
- .. 'note' directive needs '\' to span multiple lines!
- .. note:: The arguments use ``'<'`` and ``'>'`` to annotate optional \
- values. It is recommended to use :class:`.WizardHelpFormatter` \
- with the parser so that these will be displayed using the \
- conventional ``'['`` and ``']'``.
+ :return:
+ List of templates for the specified category, or a dictionary of such
+ (keyed by category name) if ``category`` is ``None``.
+ :rtype:
+ :class:`list` of :class:`str`, or :class:`dict` of
+ :class:`str` |rarr| (:class:`list` of :class:`str`).
- .. seealso:: :meth:`.parseArguments`
- """
+ :raises:
+ :exc:`~exceptions.KeyError` if ``category`` is not ``None`` or a known
+ template category.
- parser.add_argument("--templatePath", metavar="PATH",
- action="append",
- help="add additional template path for specified"
- " template category; if no category, expect that"
- " PATH contains subdirectories for one or more"
- " possible categories")
- parser.add_argument("--templateKey", metavar="TYPE=KEY", action="append",
- help="set template substitution key for specified"
- " template (default key: 'TemplateKey')")
-
- # ---------------------------------------------------------------------------
- def parseArguments(self, args):
- """Automatically add paths and keys from |CLI| arguments.
-
- :param args.templatePath: List of additional template paths.
- :type args.templatePath: :class:`list` of :class:`str`
- :param args.templateKey: List of user-specified template key mappings.
- :type args.templateKey: :class:`list` of :class:`str`
-
- This parses template-related command line arguments and updates the
- collection accordingly:
-
- * Additional template paths are provided in the form
- ``'[category=]path'``, and are added with either :meth:`.addPath` (if
- ``category`` is omitted) or :meth:`.addCategoryPath` (otherwise).
- * Template keys are provided in the form ``'name=value'``, and are
- registered using :meth:`.setKey`.
-
- If a usage error is found, the application is terminated by calling
- :func:`~.Utilities.die` with an appropriate error message.
-
- .. seealso:: :meth:`.parseArguments`, :meth:`.addPath`,
- :meth:`.addCategoryPath`, :meth:`.setKey`
- """
+ .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`,
+ :meth:`.listTemplates`
+ """
- # Add user-specified template paths
- if args.templatePath is not None:
- for tp in args.templatePath:
- tpParts = tp.split("=", 1)
+ if category is None:
+ result = {}
+ for c in _templateCategories:
+ result[c] = list(self._paths[c].keys())
+ return result
- if len(tpParts) == 1:
- if not os.path.exists(tp):
- die("template path '%s' does not exist" % tp)
- if not os.path.isdir(tp):
- die("template path '%s' is not a directory" % tp)
+ else:
+ return tuple(self._paths[category].keys())
- self.addPath(tp)
+ # ---------------------------------------------------------------------------
+ def listTemplates(self):
+ """List available templates.
- else:
- if tpParts[0].lower() not in _templateCategories:
- die(("'%s' is not a recognized template category" % tpParts[0],
- "recognized categories: %s" % ", ".join(_templateCategories)))
-
- if not os.path.exists(tpParts[1]):
- die("template path '%s' does not exist" % tpParts[1])
- if not os.path.isdir(tpParts[1]):
- die("template path '%s' is not a directory" % tpParts[1])
-
- self.addCategoryPath(tpParts[0].lower(),
- os.path.realpath(tpParts[1]))
-
- # Set user-specified template keys
- if args.templateKey is not None:
- for tk in args.templateKey:
- tkParts = tk.split("=")
- if len(tkParts) != 2:
- die("template key '%s' malformatted: expected 'NAME=KEY'" % tk)
-
- self.setKey(tkParts[0].lower(), tkParts[1])
+ This displays a list of all available templates, using :func:`logging.info`,
+ organized by category.
+
+ .. seealso:: :func:`~SlicerWizard.TemplateManager.categories`,
+ :meth:`.templates`
+ """
+
+ for c in _templateCategories:
+ logging.info("Available templates for category '%s':" % c)
+
+ if len(self._paths[c]):
+ for t in sorted(self._paths[c].keys()):
+ logging.info(f" '{t}' ('{self._getKey(t)}')")
+
+ else:
+ logging.info(" (none)")
+
+ logging.info("")
+
+ # ---------------------------------------------------------------------------
+ def addArguments(self, parser):
+ """Add template manager |CLI| arguments to parser.
+
+ :param parser: Argument parser instance to which to add arguments.
+ :type parser: :class:`argparse.ArgumentParser`
+
+ This adds |CLI| arguments to the specified ``parser`` that may be used to
+ interact with the template collection.
+
+ .. 'note' directive needs '\' to span multiple lines!
+ .. note:: The arguments use ``'<'`` and ``'>'`` to annotate optional \
+ values. It is recommended to use :class:`.WizardHelpFormatter` \
+ with the parser so that these will be displayed using the \
+ conventional ``'['`` and ``']'``.
+
+ .. seealso:: :meth:`.parseArguments`
+ """
+
+ parser.add_argument("--templatePath", metavar="PATH",
+ action="append",
+ help="add additional template path for specified"
+ " template category; if no category, expect that"
+ " PATH contains subdirectories for one or more"
+ " possible categories")
+ parser.add_argument("--templateKey", metavar="TYPE=KEY", action="append",
+ help="set template substitution key for specified"
+ " template (default key: 'TemplateKey')")
+
+ # ---------------------------------------------------------------------------
+ def parseArguments(self, args):
+ """Automatically add paths and keys from |CLI| arguments.
+
+ :param args.templatePath: List of additional template paths.
+ :type args.templatePath: :class:`list` of :class:`str`
+ :param args.templateKey: List of user-specified template key mappings.
+ :type args.templateKey: :class:`list` of :class:`str`
+
+ This parses template-related command line arguments and updates the
+ collection accordingly:
+
+ * Additional template paths are provided in the form
+ ``'[category=]path'``, and are added with either :meth:`.addPath` (if
+ ``category`` is omitted) or :meth:`.addCategoryPath` (otherwise).
+ * Template keys are provided in the form ``'name=value'``, and are
+ registered using :meth:`.setKey`.
+
+ If a usage error is found, the application is terminated by calling
+ :func:`~.Utilities.die` with an appropriate error message.
+
+ .. seealso:: :meth:`.parseArguments`, :meth:`.addPath`,
+ :meth:`.addCategoryPath`, :meth:`.setKey`
+ """
+
+ # Add user-specified template paths
+ if args.templatePath is not None:
+ for tp in args.templatePath:
+ tpParts = tp.split("=", 1)
+
+ if len(tpParts) == 1:
+ if not os.path.exists(tp):
+ die("template path '%s' does not exist" % tp)
+ if not os.path.isdir(tp):
+ die("template path '%s' is not a directory" % tp)
+
+ self.addPath(tp)
+
+ else:
+ if tpParts[0].lower() not in _templateCategories:
+ die(("'%s' is not a recognized template category" % tpParts[0],
+ "recognized categories: %s" % ", ".join(_templateCategories)))
+
+ if not os.path.exists(tpParts[1]):
+ die("template path '%s' does not exist" % tpParts[1])
+ if not os.path.isdir(tpParts[1]):
+ die("template path '%s' is not a directory" % tpParts[1])
+
+ self.addCategoryPath(tpParts[0].lower(),
+ os.path.realpath(tpParts[1]))
+
+ # Set user-specified template keys
+ if args.templateKey is not None:
+ for tk in args.templateKey:
+ tkParts = tk.split("=")
+ if len(tkParts) != 2:
+ die("template key '%s' malformatted: expected 'NAME=KEY'" % tk)
+
+ self.setKey(tkParts[0].lower(), tkParts[1])
diff --git a/Utilities/Scripts/SlicerWizard/Utilities.py b/Utilities/Scripts/SlicerWizard/Utilities.py
index 3b20bde3af7..e479cc1a7e3 100644
--- a/Utilities/Scripts/SlicerWizard/Utilities.py
+++ b/Utilities/Scripts/SlicerWizard/Utilities.py
@@ -9,48 +9,48 @@
# -----------------------------------------------------------------------------
def haveGit():
- """Return True if git is available.
+ """Return True if git is available.
- A side effect of `import git` is that it shows a popup window on
- macOS, asking the user to install XCode (if git is not installed already),
- therefore this method should only be called if git is actually needed.
- """
+ A side effect of `import git` is that it shows a popup window on
+ macOS, asking the user to install XCode (if git is not installed already),
+ therefore this method should only be called if git is actually needed.
+ """
- try:
- import git # noqa: F401
- _haveGit = True
+ try:
+ import git # noqa: F401
+ _haveGit = True
- except ImportError:
- _haveGit = False
+ except ImportError:
+ _haveGit = False
- return _haveGit
+ return _haveGit
try:
- from charset_normalizer import detect
- _haveCharDet = True
+ from charset_normalizer import detect
+ _haveCharDet = True
except ImportError:
- _haveCharDet = False
+ _haveCharDet = False
__all__ = [
- 'warn',
- 'die',
- 'inquire',
- 'initLogging',
- 'detectEncoding',
- 'buildProcessArgs',
- 'createEmptyRepo',
- 'SourceTreeDirectory',
- 'getRepo',
- 'getRemote',
- 'localRoot',
- 'vcsPrivateDirectory',
+ 'warn',
+ 'die',
+ 'inquire',
+ 'initLogging',
+ 'detectEncoding',
+ 'buildProcessArgs',
+ 'createEmptyRepo',
+ 'SourceTreeDirectory',
+ 'getRepo',
+ 'getRemote',
+ 'localRoot',
+ 'vcsPrivateDirectory',
]
_yesno = {
- "y": True,
- "n": False,
+ "y": True,
+ "n": False,
}
_logLevel = None
@@ -58,496 +58,496 @@ def haveGit():
# =============================================================================
class _LogWrapFormatter(logging.Formatter):
- # ---------------------------------------------------------------------------
- def __init__(self):
- super().__init__()
- try:
- self._width = int(os.environ['COLUMNS']) - 1
- except:
- self._width = 79
+ # ---------------------------------------------------------------------------
+ def __init__(self):
+ super().__init__()
+ try:
+ self._width = int(os.environ['COLUMNS']) - 1
+ except:
+ self._width = 79
- # ---------------------------------------------------------------------------
- def format(self, record):
- lines = super().format(record).split("\n")
- return "\n".join([textwrap.fill(l, self._width) for l in lines])
+ # ---------------------------------------------------------------------------
+ def format(self, record):
+ lines = super().format(record).split("\n")
+ return "\n".join([textwrap.fill(l, self._width) for l in lines])
# =============================================================================
class _LogReverseLevelFilter(logging.Filter):
- # ---------------------------------------------------------------------------
- def __init__(self, levelLimit):
- self._levelLimit = levelLimit
+ # ---------------------------------------------------------------------------
+ def __init__(self, levelLimit):
+ self._levelLimit = levelLimit
- # ---------------------------------------------------------------------------
- def filter(self, record):
- return record.levelno < self._levelLimit
+ # ---------------------------------------------------------------------------
+ def filter(self, record):
+ return record.levelno < self._levelLimit
# -----------------------------------------------------------------------------
def _log(func, msg):
- if sys.exc_info()[0] is not None:
- if _logLevel <= logging.DEBUG:
- logging.exception("")
+ if sys.exc_info()[0] is not None:
+ if _logLevel <= logging.DEBUG:
+ logging.exception("")
- if isinstance(msg, tuple):
- for m in msg:
- func(m)
+ if isinstance(msg, tuple):
+ for m in msg:
+ func(m)
- else:
- func(msg)
+ else:
+ func(msg)
# -----------------------------------------------------------------------------
def warn(msg):
- """Output a warning message (or messages), with exception if present.
+ """Output a warning message (or messages), with exception if present.
- :param msg: Message(s) to be output.
- :type msg: :class:`str` or sequence of :class:`str`
+ :param msg: Message(s) to be output.
+ :type msg: :class:`str` or sequence of :class:`str`
- This function outputs the specified message(s) using :func:`logging.warning`.
- If ``msg`` is a sequence, each message in the sequence is output, with a call
- to :func:`logging.warning` made for each message.
+ This function outputs the specified message(s) using :func:`logging.warning`.
+ If ``msg`` is a sequence, each message in the sequence is output, with a call
+ to :func:`logging.warning` made for each message.
- If there is a current exception, and debugging is enabled, the exception is
- reported prior to the other message(s) using :func:`logging.exception`.
+ If there is a current exception, and debugging is enabled, the exception is
+ reported prior to the other message(s) using :func:`logging.exception`.
- .. seealso:: :func:`.initLogging`.
- """
+ .. seealso:: :func:`.initLogging`.
+ """
- _log(logging.warning, msg)
+ _log(logging.warning, msg)
# -----------------------------------------------------------------------------
def die(msg, exitCode=1):
- """Output an error message (or messages), with exception if present.
+ """Output an error message (or messages), with exception if present.
- :param msg: Message(s) to be output.
- :type msg: :class:`str` or sequence of :class:`str`
- :param exitCode: Value to use as the exit code of the program.
- :type exitCode: :class:`int`
+ :param msg: Message(s) to be output.
+ :type msg: :class:`str` or sequence of :class:`str`
+ :param exitCode: Value to use as the exit code of the program.
+ :type exitCode: :class:`int`
- The output behavior (including possible report of an exception) of this
- function is the same as :func:`.warn`, except that :func:`logging.error` is
- used instead of :func:`logging.warning`. After output, the program is
- terminated by calling :func:`sys.exit` with the specified exit code.
- """
+ The output behavior (including possible report of an exception) of this
+ function is the same as :func:`.warn`, except that :func:`logging.error` is
+ used instead of :func:`logging.warning`. After output, the program is
+ terminated by calling :func:`sys.exit` with the specified exit code.
+ """
- _log(logging.error, msg)
- sys.exit(exitCode)
+ _log(logging.error, msg)
+ sys.exit(exitCode)
# -----------------------------------------------------------------------------
def inquire(msg, choices=_yesno):
- """Get multiple-choice input from the user.
-
- :param msg:
- Text of the prompt which the user will be shown.
- :type msg:
- :class:`str`
- :param choices:
- Map of possible choices to their respective return values.
- :type choices:
- :class:`dict`
-
- :returns:
- Value of the selected choice.
-
- This function presents a question (``msg``) to the user and asks them to
- select an option from a list of choices, which are presented in the manner of
- 'git add --patch' (i.e. the possible choices are shown between the prompt
- text and the final '?'). The prompt is repeated indefinitely until a valid
- selection is made.
-
- The ``choices`` are a :class:`dict`, with each key being a possible choice
- (using a single letter is recommended). The value for the selected key is
- returned to the caller.
-
- The default ``choices`` provides a yes/no prompt with a :class:`bool` return
- value.
- """
+ """Get multiple-choice input from the user.
+
+ :param msg:
+ Text of the prompt which the user will be shown.
+ :type msg:
+ :class:`str`
+ :param choices:
+ Map of possible choices to their respective return values.
+ :type choices:
+ :class:`dict`
+
+ :returns:
+ Value of the selected choice.
+
+ This function presents a question (``msg``) to the user and asks them to
+ select an option from a list of choices, which are presented in the manner of
+ 'git add --patch' (i.e. the possible choices are shown between the prompt
+ text and the final '?'). The prompt is repeated indefinitely until a valid
+ selection is made.
+
+ The ``choices`` are a :class:`dict`, with each key being a possible choice
+ (using a single letter is recommended). The value for the selected key is
+ returned to the caller.
+
+ The default ``choices`` provides a yes/no prompt with a :class:`bool` return
+ value.
+ """
- choiceKeys = list(choices.keys())
- msg = "{} {}? ".format(msg, ",".join(choiceKeys))
+ choiceKeys = list(choices.keys())
+ msg = "{} {}? ".format(msg, ",".join(choiceKeys))
- def throw(*args):
- raise ValueError()
+ def throw(*args):
+ raise ValueError()
- parser = argparse.ArgumentParser()
- parser.add_argument("choice", choices=choiceKeys)
- parser.error = throw
+ parser = argparse.ArgumentParser()
+ parser.add_argument("choice", choices=choiceKeys)
+ parser.error = throw
- while True:
- try:
- args = parser.parse_args(input(msg))
- if args.choice in choices:
- return choices[args.choice]
+ while True:
+ try:
+ args = parser.parse_args(input(msg))
+ if args.choice in choices:
+ return choices[args.choice]
- except:
- pass
+ except:
+ pass
# -----------------------------------------------------------------------------
def initLogging(logger, args):
- """Initialize logging.
+ """Initialize logging.
- :param args.debug: If ``True``, enable debug logging.
- :type args.debug: :class:`bool`
+ :param args.debug: If ``True``, enable debug logging.
+ :type args.debug: :class:`bool`
- This sets up the default logging object, with the following characteristics:
+ This sets up the default logging object, with the following characteristics:
- * Messages of :data:`~logging.WARNING` severity or greater will be sent to
- :data:`~sys.stderr`; other messages will be sent to :data:`~sys.stdout`.
- * The log level is set to :data:`~logging.DEBUG` if ``args.debug`` is
- ``True``, otherwise the log level is set to :data:`~logging.INFO`.
- * The log handlers will wrap their output according to the current terminal
- width (:envvar:`$COLUMNS`, if set, else 80).
- """
+ * Messages of :data:`~logging.WARNING` severity or greater will be sent to
+ :data:`~sys.stderr`; other messages will be sent to :data:`~sys.stdout`.
+ * The log level is set to :data:`~logging.DEBUG` if ``args.debug`` is
+ ``True``, otherwise the log level is set to :data:`~logging.INFO`.
+ * The log handlers will wrap their output according to the current terminal
+ width (:envvar:`$COLUMNS`, if set, else 80).
+ """
- global _logLevel
- _logLevel = logging.DEBUG if args.debug else logging.INFO
+ global _logLevel
+ _logLevel = logging.DEBUG if args.debug else logging.INFO
- # Create log output formatter
- f = _LogWrapFormatter()
+ # Create log output formatter
+ f = _LogWrapFormatter()
- # Create log output stream handlers
- lho = logging.StreamHandler(sys.stdout)
- lho.setLevel(_logLevel)
- lho.addFilter(_LogReverseLevelFilter(logging.WARNING))
- lho.setFormatter(f)
+ # Create log output stream handlers
+ lho = logging.StreamHandler(sys.stdout)
+ lho.setLevel(_logLevel)
+ lho.addFilter(_LogReverseLevelFilter(logging.WARNING))
+ lho.setFormatter(f)
- lhe = logging.StreamHandler(sys.stderr)
- lhe.setLevel(logging.WARNING)
- lhe.setFormatter(f)
+ lhe = logging.StreamHandler(sys.stderr)
+ lhe.setLevel(logging.WARNING)
+ lhe.setFormatter(f)
- # Set logging level and add handlers
- logger.addHandler(lho)
- logger.addHandler(lhe)
- logger.setLevel(_logLevel)
+ # Set logging level and add handlers
+ logger.addHandler(lho)
+ logger.addHandler(lhe)
+ logger.setLevel(_logLevel)
- # Turn of github debugging
- ghLogger = logging.getLogger("github")
- ghLogger.setLevel(logging.WARNING)
+ # Turn of github debugging
+ ghLogger = logging.getLogger("github")
+ ghLogger.setLevel(logging.WARNING)
# -----------------------------------------------------------------------------
def detectEncoding(data):
- """Attempt to determine the encoding of a byte sequence.
+ """Attempt to determine the encoding of a byte sequence.
- :param data: Input data on which to perform encoding detection.
- :type data: :class:`str`
+ :param data: Input data on which to perform encoding detection.
+ :type data: :class:`str`
- :return: Tuple of (encoding name, detection confidence).
- :rtype: :class:`tuple` of (:class:`str` or ``None``, :class:`float`)
+ :return: Tuple of (encoding name, detection confidence).
+ :rtype: :class:`tuple` of (:class:`str` or ``None``, :class:`float`)
- This function attempts to determine the character encoding of the input data.
- It returns a tuple with the most likely encoding (or ``None`` if the input
- data is not text) and the confidence of the detection.
+ This function attempts to determine the character encoding of the input data.
+ It returns a tuple with the most likely encoding (or ``None`` if the input
+ data is not text) and the confidence of the detection.
- This function uses the :mod:`chardet` module, if it is available. Otherwise,
- only ``'ascii'`` is detected, and ``None`` is returned for any non-ASCII
- input.
- """
+ This function uses the :mod:`chardet` module, if it is available. Otherwise,
+ only ``'ascii'`` is detected, and ``None`` is returned for any non-ASCII
+ input.
+ """
- if _haveCharDet:
- result = detect(data)
- return result["encoding"], result["confidence"]
+ if _haveCharDet:
+ result = detect(data)
+ return result["encoding"], result["confidence"]
- else:
- chars = ''.join(map(chr, list(range(7, 14)) + list(range(32, 128))))
- if len(data.translate(None, chars)):
- return None, 0.0
+ else:
+ chars = ''.join(map(chr, list(range(7, 14)) + list(range(32, 128))))
+ if len(data.translate(None, chars)):
+ return None, 0.0
- return "ascii", 1.0
+ return "ascii", 1.0
# -----------------------------------------------------------------------------
def buildProcessArgs(*args, **kwargs):
- """Build |CLI| arguments from Python-like arguments.
-
- :param prefix: Prefix for named options.
- :type prefix: :class:`str`
- :param args: Positional arguments.
- :type args: :class:`~collections.Sequence`
- :param kwargs: Named options.
- :type kwargs: :class:`dict`
-
- :return: Converted argument list.
- :rtype: :class:`list` of :class:`str`
-
- This function converts Python-style arguments, including named arguments, to
- a |CLI|-style argument list:
-
- .. code-block:: python
-
- >>> buildProcessArgs('p1', 'p2', None, 12, a=5, b=True, long_name='hello')
- ['-a', '5', '--long-name', 'hello', '-b', 'p1', 'p2', '12']
-
- Named arguments are converted to named options by adding ``'-'`` (if the name
- is one letter) or ``'--'`` (otherwise), and converting any underscores
- (``'_'``) to hyphens (``'-'``). If the value is ``True``, the option is
- considered a flag that does not take a value. If the value is ``False`` or
- ``None``, the option is skipped. Otherwise the stringified value is added
- following the option argument. Positional arguments --- except for ``None``,
- which is skipped --- are similarly stringified and added to the argument list
- following named options.
- """
+ """Build |CLI| arguments from Python-like arguments.
+
+ :param prefix: Prefix for named options.
+ :type prefix: :class:`str`
+ :param args: Positional arguments.
+ :type args: :class:`~collections.Sequence`
+ :param kwargs: Named options.
+ :type kwargs: :class:`dict`
+
+ :return: Converted argument list.
+ :rtype: :class:`list` of :class:`str`
+
+ This function converts Python-style arguments, including named arguments, to
+ a |CLI|-style argument list:
+
+ .. code-block:: python
+
+ >>> buildProcessArgs('p1', 'p2', None, 12, a=5, b=True, long_name='hello')
+ ['-a', '5', '--long-name', 'hello', '-b', 'p1', 'p2', '12']
+
+ Named arguments are converted to named options by adding ``'-'`` (if the name
+ is one letter) or ``'--'`` (otherwise), and converting any underscores
+ (``'_'``) to hyphens (``'-'``). If the value is ``True``, the option is
+ considered a flag that does not take a value. If the value is ``False`` or
+ ``None``, the option is skipped. Otherwise the stringified value is added
+ following the option argument. Positional arguments --- except for ``None``,
+ which is skipped --- are similarly stringified and added to the argument list
+ following named options.
+ """
- result = []
+ result = []
- for k, v in kwargs.items():
- if v is None or v is False:
- continue
+ for k, v in kwargs.items():
+ if v is None or v is False:
+ continue
- result += ["{}{}".format("-" if len(k) == 1 else "--", k.replace("_", "-"))]
+ result += ["{}{}".format("-" if len(k) == 1 else "--", k.replace("_", "-"))]
- if v is not True:
- result += ["%s" % v]
+ if v is not True:
+ result += ["%s" % v]
- return result + ["%s" % a for a in args if a is not None]
+ return result + ["%s" % a for a in args if a is not None]
# -----------------------------------------------------------------------------
def createEmptyRepo(path, tool=None):
- """Create a repository in an empty or nonexistent location.
-
- :param path:
- Location which should contain the newly created repository.
- :type path:
- :class:`str`
- :param tool:
- Name of the |VCS| tool to use to create the repository (e.g. ``'git'``). If
- ``None``, a default tool (git) is used.
- :type tool:
- :class:`str` or ``None``
+ """Create a repository in an empty or nonexistent location.
+
+ :param path:
+ Location which should contain the newly created repository.
+ :type path:
+ :class:`str`
+ :param tool:
+ Name of the |VCS| tool to use to create the repository (e.g. ``'git'``). If
+ ``None``, a default tool (git) is used.
+ :type tool:
+ :class:`str` or ``None``
- :raises:
- :exc:`~exceptions.Exception` if ``location`` exists and is not empty, or if
- the specified |VCS| tool is not supported.
+ :raises:
+ :exc:`~exceptions.Exception` if ``location`` exists and is not empty, or if
+ the specified |VCS| tool is not supported.
- This creates a new repository using the specified ``tool`` at ``location``,
- first creating ``location`` (and any parents) as necessary.
+ This creates a new repository using the specified ``tool`` at ``location``,
+ first creating ``location`` (and any parents) as necessary.
- This function is meant to be passed as the ``create`` argument to
- :func:`.getRepo`.
+ This function is meant to be passed as the ``create`` argument to
+ :func:`.getRepo`.
- .. note:: Only ``'git'`` repositories are supported at this time.
- """
+ .. note:: Only ``'git'`` repositories are supported at this time.
+ """
- # Check that the requested tool is supported
- if not haveGit() or tool not in (None, "git"):
- raise Exception("unable to create %r repository" % tool)
+ # Check that the requested tool is supported
+ if not haveGit() or tool not in (None, "git"):
+ raise Exception("unable to create %r repository" % tool)
- # Create a repository at the specified location
- if os.path.exists(path) and len(os.listdir(path)):
- raise Exception("refusing to create repository in non-empty directory")
+ # Create a repository at the specified location
+ if os.path.exists(path) and len(os.listdir(path)):
+ raise Exception("refusing to create repository in non-empty directory")
- os.makedirs(path)
- import git
- return git.Repo.init(path)
+ os.makedirs(path)
+ import git
+ return git.Repo.init(path)
# -----------------------------------------------------------------------------
class SourceTreeDirectory:
- """Abstract representation of a source tree directory.
-
- .. attribute:: root
+ """Abstract representation of a source tree directory.
- Location of the source tree.
+ .. attribute:: root
- .. attribute:: relative_directory
+ Location of the source tree.
- The relative path to the source directory.
- """
- # ---------------------------------------------------------------------------
+ .. attribute:: relative_directory
- def __init__(self, root, relative_directory):
+ The relative path to the source directory.
"""
- :param root: Location of the source tree.
- :type root: :class:`str`
+ # ---------------------------------------------------------------------------
- :param relative_directory: Relative directory.
- :type relative_directory: :class:`str`
+ def __init__(self, root, relative_directory):
+ """
+ :param root: Location of the source tree.
+ :type root: :class:`str`
- :raises:
- * :exc:`~exceptions.IOError` if the ``root/relative_directory`` does not exist.
- .
- """
- if not os.path.exists(os.path.join(root, relative_directory)):
- raise OSError("'root/relative_directory' does not exist")
- self.root = root
- self.relative_directory = relative_directory
+ :param relative_directory: Relative directory.
+ :type relative_directory: :class:`str`
+
+ :raises:
+ * :exc:`~exceptions.IOError` if the ``root/relative_directory`` does not exist.
+ .
+ """
+ if not os.path.exists(os.path.join(root, relative_directory)):
+ raise OSError("'root/relative_directory' does not exist")
+ self.root = root
+ self.relative_directory = relative_directory
# -----------------------------------------------------------------------------
def getRepo(path, tool=None, create=False):
- """Obtain a git repository for the specified path.
-
- :param path: Path to the repository.
- :type path: :class:`str`
- :param tool: Name of tool used to manage repository, e.g. ``'git'``.
- :type tool: :class:`str` or ``None``
- :param create: See description.
- :type create: :class:`callable` or :class:`bool`
-
- :returns:
- The repository instance, or ``None`` if no such repository exists.
- :rtype:
- :class:`git.Repo `, :class:`.Subversion.Repository`,
- or ``None``.
-
- This attempts to obtain a repository for the specified ``path``. If ``tool``
- is not ``None``, this will only look for a repository that is managed by the
- specified ``tool``; otherwise, all supported repository types will be
- considered.
-
- If ``create`` is callable, the specified function will be called to create
- the repository if one does not exist. Otherwise if ``bool(create)`` is
- ``True``, and ``tool`` is either ``None`` or ``'git'``, a repository is
- created using :meth:`git.Repo.init `. (Creation
- of other repository types is only supported at this time via a callable
- ``create``.)
-
- .. seealso:: :func:`.createEmptyRepo`
- """
-
- from . import Subversion
-
- # Try to obtain git repository
- if haveGit() and tool in (None, "git"):
- try:
- import git
- repo = git.Repo(path)
- return repo
+ """Obtain a git repository for the specified path.
+
+ :param path: Path to the repository.
+ :type path: :class:`str`
+ :param tool: Name of tool used to manage repository, e.g. ``'git'``.
+ :type tool: :class:`str` or ``None``
+ :param create: See description.
+ :type create: :class:`callable` or :class:`bool`
+
+ :returns:
+ The repository instance, or ``None`` if no such repository exists.
+ :rtype:
+ :class:`git.Repo `, :class:`.Subversion.Repository`,
+ or ``None``.
+
+ This attempts to obtain a repository for the specified ``path``. If ``tool``
+ is not ``None``, this will only look for a repository that is managed by the
+ specified ``tool``; otherwise, all supported repository types will be
+ considered.
+
+ If ``create`` is callable, the specified function will be called to create
+ the repository if one does not exist. Otherwise if ``bool(create)`` is
+ ``True``, and ``tool`` is either ``None`` or ``'git'``, a repository is
+ created using :meth:`git.Repo.init `. (Creation
+ of other repository types is only supported at this time via a callable
+ ``create``.)
+
+ .. seealso:: :func:`.createEmptyRepo`
+ """
- except:
- logging.debug("%r is not a git repository" % path)
+ from . import Subversion
- # Try to obtain subversion repository
- if tool in (None, "svn"):
- try:
- repo = Subversion.Repository(path)
- return repo
+ # Try to obtain git repository
+ if haveGit() and tool in (None, "git"):
+ try:
+ import git
+ repo = git.Repo(path)
+ return repo
- except:
- logging.debug("%r is not a svn repository" % path)
+ except:
+ logging.debug("%r is not a git repository" % path)
- # Specified path is not a supported / allowed repository; create a repository
- # if requested, otherwise return None
- if create:
- if callable(create):
- return create(path, tool)
+ # Try to obtain subversion repository
+ if tool in (None, "svn"):
+ try:
+ repo = Subversion.Repository(path)
+ return repo
- elif haveGit() and tool in (None, "git"):
- import git
- return git.Repo.init(path)
+ except:
+ logging.debug("%r is not a svn repository" % path)
- else:
- raise Exception("unable to create %r repository" % tool)
+ # Specified path is not a supported / allowed repository; create a repository
+ # if requested, otherwise return None
+ if create:
+ if callable(create):
+ return create(path, tool)
- return None
+ elif haveGit() and tool in (None, "git"):
+ import git
+ return git.Repo.init(path)
+ else:
+ raise Exception("unable to create %r repository" % tool)
-# -----------------------------------------------------------------------------
-def getRemote(repo, urls, create=None):
- """Get the remote matching a URL.
+ return None
- :param repo:
- repository instance from which to obtain the remote.
- :type repo:
- :class:`git.Repo `
- :param urls:
- A URL or list of URL's of the remote to obtain.
- :type urls:
- :class:`str` or sequence of :class:`str`
- :param create:
- What to name the remote when creating it, if it doesn't exist.
- :type create: :class:`str` or ``None``
- :returns:
- A matching or newly created :class:`git.Remote `, or
- ``None`` if no such remote exists.
+# -----------------------------------------------------------------------------
+def getRemote(repo, urls, create=None):
+ """Get the remote matching a URL.
+
+ :param repo:
+ repository instance from which to obtain the remote.
+ :type repo:
+ :class:`git.Repo `
+ :param urls:
+ A URL or list of URL's of the remote to obtain.
+ :type urls:
+ :class:`str` or sequence of :class:`str`
+ :param create:
+ What to name the remote when creating it, if it doesn't exist.
+ :type create: :class:`str` or ``None``
+
+ :returns:
+ A matching or newly created :class:`git.Remote `, or
+ ``None`` if no such remote exists.
- :raises:
- :exc:`~exceptions.Exception` if, when trying to create a remote, a remote
- with the specified name already exists.
+ :raises:
+ :exc:`~exceptions.Exception` if, when trying to create a remote, a remote
+ with the specified name already exists.
- This attempts to find a git remote of the specified repository whose upstream
- URL matches (one of) ``urls``. If no such remote exists and ``create`` is not
- ``None``, a new remote named ``create`` will be created using the first URL
- of ``urls``.
- """
+ This attempts to find a git remote of the specified repository whose upstream
+ URL matches (one of) ``urls``. If no such remote exists and ``create`` is not
+ ``None``, a new remote named ``create`` will be created using the first URL
+ of ``urls``.
+ """
- urls = list(urls)
+ urls = list(urls)
- for remote in repo.remotes:
- if remote.url in urls:
- return remote
+ for remote in repo.remotes:
+ if remote.url in urls:
+ return remote
- if create is not None:
- if not isinstance(create, str):
- raise TypeError("name of remote to create must be a string")
+ if create is not None:
+ if not isinstance(create, str):
+ raise TypeError("name of remote to create must be a string")
- if hasattr(repo.remotes, create):
- raise Exception("cannot create remote '%s':"
- " a remote with that name already exists" % create)
+ if hasattr(repo.remotes, create):
+ raise Exception("cannot create remote '%s':"
+ " a remote with that name already exists" % create)
- return repo.create_remote(create, urls[0])
+ return repo.create_remote(create, urls[0])
- return None
+ return None
# -----------------------------------------------------------------------------
def localRoot(repo):
- """Get top level local directory of a repository.
+ """Get top level local directory of a repository.
- :param repo:
- Repository instance.
- :type repo:
- :class:`git.Repo ` or
- :class:`.Subversion.Repository`.
+ :param repo:
+ Repository instance.
+ :type repo:
+ :class:`git.Repo ` or
+ :class:`.Subversion.Repository`.
- :return: Absolute path to the repository local root.
- :rtype: :class:`str`
+ :return: Absolute path to the repository local root.
+ :rtype: :class:`str`
- :raises: :exc:`~exceptions.Exception` if the local root cannot be determined.
+ :raises: :exc:`~exceptions.Exception` if the local root cannot be determined.
- This returns the local file system path to the top level of a repository
- working tree / working copy.
- """
+ This returns the local file system path to the top level of a repository
+ working tree / working copy.
+ """
- if hasattr(repo, "working_tree_dir"):
- return repo.working_tree_dir
+ if hasattr(repo, "working_tree_dir"):
+ return repo.working_tree_dir
- if hasattr(repo, "wc_root"):
- return repo.wc_root
+ if hasattr(repo, "wc_root"):
+ return repo.wc_root
- raise Exception("unable to determine repository local root")
+ raise Exception("unable to determine repository local root")
# -----------------------------------------------------------------------------
def vcsPrivateDirectory(repo):
- """Get |VCS| private directory of a repository.
+ """Get |VCS| private directory of a repository.
- :param repo:
- Repository instance.
- :type repo:
- :class:`git.Repo ` or
- :class:`.Subversion.Repository`.
+ :param repo:
+ Repository instance.
+ :type repo:
+ :class:`git.Repo ` or
+ :class:`.Subversion.Repository`.
- :return: Absolute path to the |VCS| private directory.
- :rtype: :class:`str`
+ :return: Absolute path to the |VCS| private directory.
+ :rtype: :class:`str`
- :raises:
- :exc:`~exceptions.Exception` if the private directory cannot be determined.
+ :raises:
+ :exc:`~exceptions.Exception` if the private directory cannot be determined.
- This returns the |VCS| private directory for a repository, e.g. the ``.git``
- or ``.svn`` directory.
- """
+ This returns the |VCS| private directory for a repository, e.g. the ``.git``
+ or ``.svn`` directory.
+ """
- if hasattr(repo, "git_dir"):
- return repo.git_dir
+ if hasattr(repo, "git_dir"):
+ return repo.git_dir
- if hasattr(repo, "svn_dir"):
- return repo.svn_dir
+ if hasattr(repo, "svn_dir"):
+ return repo.svn_dir
- raise Exception("unable to determine repository local private directory")
+ raise Exception("unable to determine repository local private directory")
diff --git a/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py b/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py
index fe88bcc8b7a..6ecd205970a 100644
--- a/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py
+++ b/Utilities/Scripts/SlicerWizard/WizardHelpFormatter.py
@@ -3,21 +3,21 @@
# =============================================================================
class WizardHelpFormatter(argparse.HelpFormatter):
- """Custom formatter for |CLI| arguments.
+ """Custom formatter for |CLI| arguments.
- This formatter overrides :class:`argparse.HelpFormatter` in order to replace
- occurrences of the '<' and '>' characters with '[' and ']', respectively.
- This is done to work around the formatter's wrapping, which tries to break
- metavars if they contain these characters and then becomes confused (read:
- raises an assertion).
- """
+ This formatter overrides :class:`argparse.HelpFormatter` in order to replace
+ occurrences of the '<' and '>' characters with '[' and ']', respectively.
+ This is done to work around the formatter's wrapping, which tries to break
+ metavars if they contain these characters and then becomes confused (read:
+ raises an assertion).
+ """
- # ---------------------------------------------------------------------------
- def _format_action_invocation(self, *args):
- text = super()._format_action_invocation(*args)
- return text.replace("<", "[").replace(">", "]")
+ # ---------------------------------------------------------------------------
+ def _format_action_invocation(self, *args):
+ text = super()._format_action_invocation(*args)
+ return text.replace("<", "[").replace(">", "]")
- # ---------------------------------------------------------------------------
- def _format_usage(self, *args):
- text = super()._format_usage(*args)
- return text.replace("<", "[").replace(">", "]")
+ # ---------------------------------------------------------------------------
+ def _format_usage(self, *args):
+ text = super()._format_usage(*args)
+ return text.replace("<", "[").replace(">", "]")
diff --git a/Utilities/Scripts/SlicerWizard/__version__.py b/Utilities/Scripts/SlicerWizard/__version__.py
index 2f2842e19b9..c09116cd3b8 100644
--- a/Utilities/Scripts/SlicerWizard/__version__.py
+++ b/Utilities/Scripts/SlicerWizard/__version__.py
@@ -1,8 +1,8 @@
__version_info__ = (
- 5,
- 1,
- 0,
- "dev0"
+ 5,
+ 1,
+ 0,
+ "dev0"
)
__version__ = ".".join(map(str, __version_info__))
diff --git a/Utilities/Scripts/SlicerWizard/doc/conf.py b/Utilities/Scripts/SlicerWizard/doc/conf.py
index 553f3506d7e..d22c4f8a1f7 100644
--- a/Utilities/Scripts/SlicerWizard/doc/conf.py
+++ b/Utilities/Scripts/SlicerWizard/doc/conf.py
@@ -29,40 +29,40 @@
# ===============================================================================
class WikidocRole:
- wiki_root = 'https://wiki.slicer.org/slicerWiki/index.php'
+ wiki_root = 'https://wiki.slicer.org/slicerWiki/index.php'
- # -----------------------------------------------------------------------------
- def __call__(self, role, rawtext, text, lineno, inliner,
- options={}, content=[]):
+ # -----------------------------------------------------------------------------
+ def __call__(self, role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
- roles.set_classes(options)
+ roles.set_classes(options)
- parts = utils.unescape(text).split(' ', 1)
- uri = '{}/Documentation/{}/{}'.format(self.wiki_root, self.wiki_doc_version,
- parts[0])
- text = parts[1]
+ parts = utils.unescape(text).split(' ', 1)
+ uri = '{}/Documentation/{}/{}'.format(self.wiki_root, self.wiki_doc_version,
+ parts[0])
+ text = parts[1]
- node = nodes.reference(rawtext, text, refuri=uri, **options)
- return [node], []
+ node = nodes.reference(rawtext, text, refuri=uri, **options)
+ return [node], []
# ===============================================================================
class ClassModuleClassDocumenter(autodoc.ClassDocumenter):
- # -----------------------------------------------------------------------------
- def resolve_name(self, *args):
- module, attrs = super().resolve_name(*args)
- module = module.split('.')
- if module[-1] == attrs[0]:
- del module[-1]
- return '.'.join(module), attrs
+ # -----------------------------------------------------------------------------
+ def resolve_name(self, *args):
+ module, attrs = super().resolve_name(*args)
+ module = module.split('.')
+ if module[-1] == attrs[0]:
+ del module[-1]
+ return '.'.join(module), attrs
# %%% Site customizations %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# -------------------------------------------------------------------------------
def setup(app):
- app.add_autodocumenter(ClassModuleClassDocumenter)
- app.add_role('wikidoc', WikidocRole())
+ app.add_autodocumenter(ClassModuleClassDocumenter)
+ app.add_role('wikidoc', WikidocRole())
# -------------------------------------------------------------------------------
@@ -75,7 +75,7 @@ def setup(app):
args, pargs = parser.parse_known_args()
for d in args.defs:
- setattr(args, *d.split('=', 1))
+ setattr(args, *d.split('=', 1))
setattr(WikidocRole, 'wiki_doc_version', args.wikidoc_version)
@@ -89,15 +89,15 @@ def setup(app):
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx']
intersphinx_mapping = {
- 'python': ('https://docs.python.org/%i.%i' % sys.version_info[:2], None),
- 'github': ('http://jacquev6.github.io/PyGithub/v1', None),
+ 'python': ('https://docs.python.org/%i.%i' % sys.version_info[:2], None),
+ 'github': ('http://jacquev6.github.io/PyGithub/v1', None),
}
try:
- import git
- intersphinx_mapping['git'] = ('https://pythonhosted.org/GitPython/%s' % git.__version__.split(' ')[0], None)
+ import git
+ intersphinx_mapping['git'] = ('https://pythonhosted.org/GitPython/%s' % git.__version__.split(' ')[0], None)
except:
- pass
+ pass
# The suffix of source filenames.
source_suffix = '.rst'
@@ -240,21 +240,21 @@ def setup(app):
# %%% Options for LaTeX output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
latex_elements = {
-# The paper size ('letterpaper' or 'a4paper').
-# 'papersize': 'letterpaper',
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
-# The font size ('10pt', '11pt' or '12pt').
-# 'pointsize': '10pt',
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
-# Additional stuff for the LaTeX preamble.
-# 'preamble': '',
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'SlicerWizard.tex', 'SlicerWizard Documentation',
- author, 'manual'),
+ ('index', 'SlicerWizard.tex', 'SlicerWizard Documentation',
+ author, 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -297,9 +297,9 @@ def setup(app):
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'SlicerWizard', 'SlicerWizard Documentation',
- author, 'SlicerWizard', 'One line description of project.',
- 'Miscellaneous'),
+ ('index', 'SlicerWizard', 'SlicerWizard Documentation',
+ author, 'SlicerWizard', 'One line description of project.',
+ 'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
diff --git a/Utilities/Scripts/genqrc.py b/Utilities/Scripts/genqrc.py
index 5a03198e938..706678b8c54 100755
--- a/Utilities/Scripts/genqrc.py
+++ b/Utilities/Scripts/genqrc.py
@@ -7,93 +7,93 @@
# -----------------------------------------------------------------------------
def writeFile(path, content):
- # Test if file already contains desired content
- if os.path.exists(path):
- try:
- with open(path) as f:
- if f.read() == content:
- return
+ # Test if file already contains desired content
+ if os.path.exists(path):
+ try:
+ with open(path) as f:
+ if f.read() == content:
+ return
- except:
- pass
+ except:
+ pass
- # Write file
- with open(path, "wt") as f:
- f.write(content)
+ # Write file
+ with open(path, "wt") as f:
+ f.write(content)
# -----------------------------------------------------------------------------
def addFile(path):
- name = os.path.basename(path)
- return [f" {path}"]
+ name = os.path.basename(path)
+ return [f" {path}"]
# -----------------------------------------------------------------------------
def buildContent(root, path):
- dirs = []
- out = [" " % path]
+ dirs = []
+ out = [" " % path]
- for entry in os.listdir(os.path.join(root, path)):
- full_entry = os.path.join(root, path, entry)
+ for entry in os.listdir(os.path.join(root, path)):
+ full_entry = os.path.join(root, path, entry)
- if os.path.isdir(full_entry):
- dirs.append(os.path.join(path, entry))
+ if os.path.isdir(full_entry):
+ dirs.append(os.path.join(path, entry))
- else:
- ext = os.path.splitext(entry)[1].lower()
+ else:
+ ext = os.path.splitext(entry)[1].lower()
- if ext == ".png" or ext == ".svg":
- out += addFile(full_entry)
+ if ext == ".png" or ext == ".svg":
+ out += addFile(full_entry)
- out += [" ", ""]
+ out += [" ", ""]
- for d in dirs:
- out += buildContent(root, d)
+ for d in dirs:
+ out += buildContent(root, d)
- return out
+ return out
# -----------------------------------------------------------------------------
def main(argv):
- parser = argparse.ArgumentParser(description="PythonQt Resource Compiler")
+ parser = argparse.ArgumentParser(description="PythonQt Resource Compiler")
- parser.add_argument("-o", dest="out_path", metavar="PATH", default="-",
- help="location to which to write the output .qrc file"
- " (default=stdout)")
- parser.add_argument("resource_directories", nargs="+",
- help="list of directories containing resource files")
+ parser.add_argument("-o", dest="out_path", metavar="PATH", default="-",
+ help="location to which to write the output .qrc file"
+ " (default=stdout)")
+ parser.add_argument("resource_directories", nargs="+",
+ help="list of directories containing resource files")
- args = parser.parse_args(argv)
+ args = parser.parse_args(argv)
- qrc_content = [
- "",
- "",
- "",
- "",
- ""]
+ qrc_content = [
+ "",
+ "",
+ "",
+ "",
+ ""]
- for path in args.resource_directories:
- path = os.path.dirname(os.path.join(path, '.')) # remove trailing '/'
- qrc_content += buildContent(os.path.dirname(path), os.path.basename(path))
+ for path in args.resource_directories:
+ path = os.path.dirname(os.path.join(path, '.')) # remove trailing '/'
+ qrc_content += buildContent(os.path.dirname(path), os.path.basename(path))
- qrc_content += [""]
+ qrc_content += [""]
- qrc_content = "\n".join(qrc_content) + "\n"
+ qrc_content = "\n".join(qrc_content) + "\n"
- if args.out_path == "-":
- sys.stdout.write(qrc_content)
+ if args.out_path == "-":
+ sys.stdout.write(qrc_content)
- else:
- writeFile(args.out_path, qrc_content)
+ else:
+ writeFile(args.out_path, qrc_content)
# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
if __name__ == "__main__":
- main(sys.argv[1:])
+ main(sys.argv[1:])
diff --git a/Utilities/Scripts/qrcc.py b/Utilities/Scripts/qrcc.py
index 8c6199b82cc..a31317964fe 100755
--- a/Utilities/Scripts/qrcc.py
+++ b/Utilities/Scripts/qrcc.py
@@ -30,69 +30,69 @@ def qCleanupResources():
def compileResources(in_path, out_file, args):
- # Determine command line for rcc
- if sys.platform.startswith("win") or sys.platform.startswith("cygwin"):
- # On Windows, rcc performs LF -> CRLF conversion when writing to stdout,
- # resulting in corrupt data that can cause Qt to crash when loading the
- # resources. To work around this, we must instead write to a temporary
- # file.
- if args.out_path == "-":
- import tempfile
- tmp_file, tmp_path = tempfile.mkstemp()
- os.close(tmp_file)
+ # Determine command line for rcc
+ if sys.platform.startswith("win") or sys.platform.startswith("cygwin"):
+ # On Windows, rcc performs LF -> CRLF conversion when writing to stdout,
+ # resulting in corrupt data that can cause Qt to crash when loading the
+ # resources. To work around this, we must instead write to a temporary
+ # file.
+ if args.out_path == "-":
+ import tempfile
+ tmp_file, tmp_path = tempfile.mkstemp()
+ os.close(tmp_file)
+
+ else:
+ tmp_path = args.out_path + ".rcctmp"
+
+ command = [args.rcc, "-binary", "-o", tmp_path, in_path]
else:
- tmp_path = args.out_path + ".rcctmp"
-
- command = [args.rcc, "-binary", "-o", tmp_path, in_path]
-
- else:
- tmp_path = None
- command = [args.rcc, "-binary", in_path]
+ tmp_path = None
+ command = [args.rcc, "-binary", in_path]
- # Run rcc
- proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sys.stderr)
- data, err = proc.communicate()
+ # Run rcc
+ proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sys.stderr)
+ data, err = proc.communicate()
- # Check that rcc ran successfully
- if proc.returncode != 0:
- sys.exit(proc.returncode)
+ # Check that rcc ran successfully
+ if proc.returncode != 0:
+ sys.exit(proc.returncode)
- # Read data, if using a temporary file (see above)
- if tmp_path is not None:
- with open(tmp_path, "rb") as tmp_file:
- data = tmp_file.read()
+ # Read data, if using a temporary file (see above)
+ if tmp_path is not None:
+ with open(tmp_path, "rb") as tmp_file:
+ data = tmp_file.read()
- os.remove(tmp_path)
+ os.remove(tmp_path)
- _data = base64.encodebytes(data).rstrip().decode()
+ _data = base64.encodebytes(data).rstrip().decode()
- # Write output script
- out_file.write(_header)
- out_file.write(_data)
- out_file.write(_footer)
+ # Write output script
+ out_file.write(_header)
+ out_file.write(_data)
+ out_file.write(_footer)
def main(argv):
- parser = argparse.ArgumentParser(description="PythonQt Resource Compiler")
+ parser = argparse.ArgumentParser(description="PythonQt Resource Compiler")
- parser.add_argument("--rcc", default="rcc",
- help="location of the Qt resource compiler executable")
- parser.add_argument("-o", "--output", dest="out_path", metavar="PATH",
- default="-",
- help="location to which to write the output Python"
- " script (default=stdout)")
- parser.add_argument("in_path", metavar="resource_script",
- help="input resource script to compile")
+ parser.add_argument("--rcc", default="rcc",
+ help="location of the Qt resource compiler executable")
+ parser.add_argument("-o", "--output", dest="out_path", metavar="PATH",
+ default="-",
+ help="location to which to write the output Python"
+ " script (default=stdout)")
+ parser.add_argument("in_path", metavar="resource_script",
+ help="input resource script to compile")
- args = parser.parse_args(argv)
+ args = parser.parse_args(argv)
- if args.out_path == "-":
- compileResources(args.in_path, sys.stdout, args)
- else:
- with open(args.out_path, "w") as f:
- compileResources(args.in_path, f, args)
+ if args.out_path == "-":
+ compileResources(args.in_path, sys.stdout, args)
+ else:
+ with open(args.out_path, "w") as f:
+ compileResources(args.in_path, f, args)
if __name__ == "__main__":
- main(sys.argv[1:])
+ main(sys.argv[1:])
diff --git a/Utilities/Templates/Modules/Scripted/TemplateKey.py b/Utilities/Templates/Modules/Scripted/TemplateKey.py
index 013f4b87500..78604a4ccd5 100644
--- a/Utilities/Templates/Modules/Scripted/TemplateKey.py
+++ b/Utilities/Templates/Modules/Scripted/TemplateKey.py
@@ -13,29 +13,29 @@
#
class TemplateKey(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "TemplateKey" # TODO: make this more human readable by adding spaces
- self.parent.categories = ["Examples"] # TODO: set categories (folders where the module shows up in the module selector)
- self.parent.dependencies = [] # TODO: add here list of module names that this module requires
- self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)"
- # TODO: update with short description of the module and a link to online module documentation
- self.parent.helpText = """
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "TemplateKey" # TODO: make this more human readable by adding spaces
+ self.parent.categories = ["Examples"] # TODO: set categories (folders where the module shows up in the module selector)
+ self.parent.dependencies = [] # TODO: add here list of module names that this module requires
+ self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)"
+ # TODO: update with short description of the module and a link to online module documentation
+ self.parent.helpText = """
This is an example of scripted loadable module bundled in an extension.
See more information in module documentation.
"""
- # TODO: replace with organization, grant and thanks
- self.parent.acknowledgementText = """
+ # TODO: replace with organization, grant and thanks
+ self.parent.acknowledgementText = """
This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab,
and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1.
"""
- # Additional initialization step after application startup is complete
- slicer.app.connect("startupCompleted()", registerSampleData)
+ # Additional initialization step after application startup is complete
+ slicer.app.connect("startupCompleted()", registerSampleData)
#
@@ -43,49 +43,49 @@ def __init__(self, parent):
#
def registerSampleData():
- """
- Add data sets to Sample Data module.
- """
- # It is always recommended to provide sample data for users to make it easy to try the module,
- # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed.
-
- import SampleData
- iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons')
-
- # To ensure that the source code repository remains small (can be downloaded and installed quickly)
- # it is recommended to store data sets that are larger than a few MB in a Github release.
-
- # TemplateKey1
- SampleData.SampleDataLogic.registerCustomSampleDataSource(
- # Category and sample name displayed in Sample Data module
- category='TemplateKey',
- sampleName='TemplateKey1',
- # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder.
- # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single".
- thumbnailFileName=os.path.join(iconsPath, 'TemplateKey1.png'),
- # Download URL and target file name
- uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95",
- fileNames='TemplateKey1.nrrd',
- # Checksum to ensure file integrity. Can be computed by this command:
- # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest())
- checksums='SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95',
- # This node name will be used when the data set is loaded
- nodeNames='TemplateKey1'
- )
-
- # TemplateKey2
- SampleData.SampleDataLogic.registerCustomSampleDataSource(
- # Category and sample name displayed in Sample Data module
- category='TemplateKey',
- sampleName='TemplateKey2',
- thumbnailFileName=os.path.join(iconsPath, 'TemplateKey2.png'),
- # Download URL and target file name
- uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97",
- fileNames='TemplateKey2.nrrd',
- checksums='SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97',
- # This node name will be used when the data set is loaded
- nodeNames='TemplateKey2'
- )
+ """
+ Add data sets to Sample Data module.
+ """
+ # It is always recommended to provide sample data for users to make it easy to try the module,
+ # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed.
+
+ import SampleData
+ iconsPath = os.path.join(os.path.dirname(__file__), 'Resources/Icons')
+
+ # To ensure that the source code repository remains small (can be downloaded and installed quickly)
+ # it is recommended to store data sets that are larger than a few MB in a Github release.
+
+ # TemplateKey1
+ SampleData.SampleDataLogic.registerCustomSampleDataSource(
+ # Category and sample name displayed in Sample Data module
+ category='TemplateKey',
+ sampleName='TemplateKey1',
+ # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder.
+ # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single".
+ thumbnailFileName=os.path.join(iconsPath, 'TemplateKey1.png'),
+ # Download URL and target file name
+ uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95",
+ fileNames='TemplateKey1.nrrd',
+ # Checksum to ensure file integrity. Can be computed by this command:
+ # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest())
+ checksums='SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95',
+ # This node name will be used when the data set is loaded
+ nodeNames='TemplateKey1'
+ )
+
+ # TemplateKey2
+ SampleData.SampleDataLogic.registerCustomSampleDataSource(
+ # Category and sample name displayed in Sample Data module
+ category='TemplateKey',
+ sampleName='TemplateKey2',
+ thumbnailFileName=os.path.join(iconsPath, 'TemplateKey2.png'),
+ # Download URL and target file name
+ uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97",
+ fileNames='TemplateKey2.nrrd',
+ checksums='SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97',
+ # This node name will be used when the data set is loaded
+ nodeNames='TemplateKey2'
+ )
#
@@ -93,196 +93,196 @@ def registerSampleData():
#
class TemplateKeyWidget(ScriptedLoadableModuleWidget, VTKObservationMixin):
- """Uses ScriptedLoadableModuleWidget base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent=None):
- """
- Called when the user opens the module the first time and the widget is initialized.
- """
- ScriptedLoadableModuleWidget.__init__(self, parent)
- VTKObservationMixin.__init__(self) # needed for parameter node observation
- self.logic = None
- self._parameterNode = None
- self._updatingGUIFromParameterNode = False
-
- def setup(self):
- """
- Called when the user opens the module the first time and the widget is initialized.
- """
- ScriptedLoadableModuleWidget.setup(self)
-
- # Load widget from .ui file (created by Qt Designer).
- # Additional widgets can be instantiated manually and added to self.layout.
- uiWidget = slicer.util.loadUI(self.resourcePath('UI/TemplateKey.ui'))
- self.layout.addWidget(uiWidget)
- self.ui = slicer.util.childWidgetVariables(uiWidget)
-
- # Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's
- # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's.
- # "setMRMLScene(vtkMRMLScene*)" slot.
- uiWidget.setMRMLScene(slicer.mrmlScene)
-
- # Create logic class. Logic implements all computations that should be possible to run
- # in batch mode, without a graphical user interface.
- self.logic = TemplateKeyLogic()
-
- # Connections
-
- # These connections ensure that we update parameter node when scene is closed
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
- self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
-
- # These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene
- # (in the selected parameter node).
- self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
- self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
- self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI)
- self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI)
- self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
-
- # Buttons
- self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
-
- # Make sure parameter node is initialized (needed for module reload)
- self.initializeParameterNode()
-
- def cleanup(self):
- """
- Called when the application closes and the module widget is destroyed.
- """
- self.removeObservers()
-
- def enter(self):
- """
- Called each time the user opens this module.
- """
- # Make sure parameter node exists and observed
- self.initializeParameterNode()
-
- def exit(self):
- """
- Called each time the user opens a different module.
- """
- # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module)
- self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
-
- def onSceneStartClose(self, caller, event):
- """
- Called just before the scene is closed.
- """
- # Parameter node will be reset, do not use it anymore
- self.setParameterNode(None)
-
- def onSceneEndClose(self, caller, event):
- """
- Called just after the scene is closed.
- """
- # If this module is shown while the scene is closed then recreate a new parameter node immediately
- if self.parent.isEntered:
- self.initializeParameterNode()
-
- def initializeParameterNode(self):
- """
- Ensure parameter node exists and observed.
- """
- # Parameter node stores all user choices in parameter values, node selections, etc.
- # so that when the scene is saved and reloaded, these settings are restored.
-
- self.setParameterNode(self.logic.getParameterNode())
-
- # Select default input nodes if nothing is selected yet to save a few clicks for the user
- if not self._parameterNode.GetNodeReference("InputVolume"):
- firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode")
- if firstVolumeNode:
- self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID())
-
- def setParameterNode(self, inputParameterNode):
- """
- Set and observe parameter node.
- Observation is needed because when the parameter node is changed then the GUI must be updated immediately.
- """
-
- if inputParameterNode:
- self.logic.setDefaultParameters(inputParameterNode)
-
- # Unobserve previously selected parameter node and add an observer to the newly selected.
- # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module
- # those are reflected immediately in the GUI.
- if self._parameterNode is not None:
- self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
- self._parameterNode = inputParameterNode
- if self._parameterNode is not None:
- self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
-
- # Initial GUI update
- self.updateGUIFromParameterNode()
-
- def updateGUIFromParameterNode(self, caller=None, event=None):
- """
- This method is called whenever parameter node is changed.
- The module GUI is updated to show the current state of the parameter node.
- """
-
- if self._parameterNode is None or self._updatingGUIFromParameterNode:
- return
-
- # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop)
- self._updatingGUIFromParameterNode = True
-
- # Update node selectors and sliders
- self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume"))
- self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume"))
- self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse"))
- self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold"))
- self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true")
-
- # Update buttons states and tooltips
- if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"):
- self.ui.applyButton.toolTip = "Compute output volume"
- self.ui.applyButton.enabled = True
- else:
- self.ui.applyButton.toolTip = "Select input and output volume nodes"
- self.ui.applyButton.enabled = False
-
- # All the GUI updates are done
- self._updatingGUIFromParameterNode = False
-
- def updateParameterNodeFromGUI(self, caller=None, event=None):
- """
- This method is called when the user makes any change in the GUI.
- The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
- """
-
- if self._parameterNode is None or self._updatingGUIFromParameterNode:
- return
-
- wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
-
- self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID)
- self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID)
- self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value))
- self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false")
- self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID)
-
- self._parameterNode.EndModify(wasModified)
-
- def onApplyButton(self):
- """
- Run processing when user clicks "Apply" button.
- """
- with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
-
- # Compute output
- self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(),
- self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked)
-
- # Compute inverted output (if needed)
- if self.ui.invertedOutputSelector.currentNode():
- # If additional output volume is selected then result with inverted threshold is written there
- self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(),
- self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False)
+ """Uses ScriptedLoadableModuleWidget base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self, parent=None):
+ """
+ Called when the user opens the module the first time and the widget is initialized.
+ """
+ ScriptedLoadableModuleWidget.__init__(self, parent)
+ VTKObservationMixin.__init__(self) # needed for parameter node observation
+ self.logic = None
+ self._parameterNode = None
+ self._updatingGUIFromParameterNode = False
+
+ def setup(self):
+ """
+ Called when the user opens the module the first time and the widget is initialized.
+ """
+ ScriptedLoadableModuleWidget.setup(self)
+
+ # Load widget from .ui file (created by Qt Designer).
+ # Additional widgets can be instantiated manually and added to self.layout.
+ uiWidget = slicer.util.loadUI(self.resourcePath('UI/TemplateKey.ui'))
+ self.layout.addWidget(uiWidget)
+ self.ui = slicer.util.childWidgetVariables(uiWidget)
+
+ # Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's
+ # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's.
+ # "setMRMLScene(vtkMRMLScene*)" slot.
+ uiWidget.setMRMLScene(slicer.mrmlScene)
+
+ # Create logic class. Logic implements all computations that should be possible to run
+ # in batch mode, without a graphical user interface.
+ self.logic = TemplateKeyLogic()
+
+ # Connections
+
+ # These connections ensure that we update parameter node when scene is closed
+ self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose)
+ self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose)
+
+ # These connections ensure that whenever user changes some settings on the GUI, that is saved in the MRML scene
+ # (in the selected parameter node).
+ self.ui.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
+ self.ui.outputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
+ self.ui.imageThresholdSliderWidget.connect("valueChanged(double)", self.updateParameterNodeFromGUI)
+ self.ui.invertOutputCheckBox.connect("toggled(bool)", self.updateParameterNodeFromGUI)
+ self.ui.invertedOutputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.updateParameterNodeFromGUI)
+
+ # Buttons
+ self.ui.applyButton.connect('clicked(bool)', self.onApplyButton)
+
+ # Make sure parameter node is initialized (needed for module reload)
+ self.initializeParameterNode()
+
+ def cleanup(self):
+ """
+ Called when the application closes and the module widget is destroyed.
+ """
+ self.removeObservers()
+
+ def enter(self):
+ """
+ Called each time the user opens this module.
+ """
+ # Make sure parameter node exists and observed
+ self.initializeParameterNode()
+
+ def exit(self):
+ """
+ Called each time the user opens a different module.
+ """
+ # Do not react to parameter node changes (GUI wlil be updated when the user enters into the module)
+ self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
+
+ def onSceneStartClose(self, caller, event):
+ """
+ Called just before the scene is closed.
+ """
+ # Parameter node will be reset, do not use it anymore
+ self.setParameterNode(None)
+
+ def onSceneEndClose(self, caller, event):
+ """
+ Called just after the scene is closed.
+ """
+ # If this module is shown while the scene is closed then recreate a new parameter node immediately
+ if self.parent.isEntered:
+ self.initializeParameterNode()
+
+ def initializeParameterNode(self):
+ """
+ Ensure parameter node exists and observed.
+ """
+ # Parameter node stores all user choices in parameter values, node selections, etc.
+ # so that when the scene is saved and reloaded, these settings are restored.
+
+ self.setParameterNode(self.logic.getParameterNode())
+
+ # Select default input nodes if nothing is selected yet to save a few clicks for the user
+ if not self._parameterNode.GetNodeReference("InputVolume"):
+ firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode")
+ if firstVolumeNode:
+ self._parameterNode.SetNodeReferenceID("InputVolume", firstVolumeNode.GetID())
+
+ def setParameterNode(self, inputParameterNode):
+ """
+ Set and observe parameter node.
+ Observation is needed because when the parameter node is changed then the GUI must be updated immediately.
+ """
+
+ if inputParameterNode:
+ self.logic.setDefaultParameters(inputParameterNode)
+
+ # Unobserve previously selected parameter node and add an observer to the newly selected.
+ # Changes of parameter node are observed so that whenever parameters are changed by a script or any other module
+ # those are reflected immediately in the GUI.
+ if self._parameterNode is not None:
+ self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
+ self._parameterNode = inputParameterNode
+ if self._parameterNode is not None:
+ self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self.updateGUIFromParameterNode)
+
+ # Initial GUI update
+ self.updateGUIFromParameterNode()
+
+ def updateGUIFromParameterNode(self, caller=None, event=None):
+ """
+ This method is called whenever parameter node is changed.
+ The module GUI is updated to show the current state of the parameter node.
+ """
+
+ if self._parameterNode is None or self._updatingGUIFromParameterNode:
+ return
+
+ # Make sure GUI changes do not call updateParameterNodeFromGUI (it could cause infinite loop)
+ self._updatingGUIFromParameterNode = True
+
+ # Update node selectors and sliders
+ self.ui.inputSelector.setCurrentNode(self._parameterNode.GetNodeReference("InputVolume"))
+ self.ui.outputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolume"))
+ self.ui.invertedOutputSelector.setCurrentNode(self._parameterNode.GetNodeReference("OutputVolumeInverse"))
+ self.ui.imageThresholdSliderWidget.value = float(self._parameterNode.GetParameter("Threshold"))
+ self.ui.invertOutputCheckBox.checked = (self._parameterNode.GetParameter("Invert") == "true")
+
+ # Update buttons states and tooltips
+ if self._parameterNode.GetNodeReference("InputVolume") and self._parameterNode.GetNodeReference("OutputVolume"):
+ self.ui.applyButton.toolTip = "Compute output volume"
+ self.ui.applyButton.enabled = True
+ else:
+ self.ui.applyButton.toolTip = "Select input and output volume nodes"
+ self.ui.applyButton.enabled = False
+
+ # All the GUI updates are done
+ self._updatingGUIFromParameterNode = False
+
+ def updateParameterNodeFromGUI(self, caller=None, event=None):
+ """
+ This method is called when the user makes any change in the GUI.
+ The changes are saved into the parameter node (so that they are restored when the scene is saved and loaded).
+ """
+
+ if self._parameterNode is None or self._updatingGUIFromParameterNode:
+ return
+
+ wasModified = self._parameterNode.StartModify() # Modify all properties in a single batch
+
+ self._parameterNode.SetNodeReferenceID("InputVolume", self.ui.inputSelector.currentNodeID)
+ self._parameterNode.SetNodeReferenceID("OutputVolume", self.ui.outputSelector.currentNodeID)
+ self._parameterNode.SetParameter("Threshold", str(self.ui.imageThresholdSliderWidget.value))
+ self._parameterNode.SetParameter("Invert", "true" if self.ui.invertOutputCheckBox.checked else "false")
+ self._parameterNode.SetNodeReferenceID("OutputVolumeInverse", self.ui.invertedOutputSelector.currentNodeID)
+
+ self._parameterNode.EndModify(wasModified)
+
+ def onApplyButton(self):
+ """
+ Run processing when user clicks "Apply" button.
+ """
+ with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True):
+
+ # Compute output
+ self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(),
+ self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked)
+
+ # Compute inverted output (if needed)
+ if self.ui.invertedOutputSelector.currentNode():
+ # If additional output volume is selected then result with inverted threshold is written there
+ self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(),
+ self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False)
#
@@ -290,61 +290,61 @@ def onApplyButton(self):
#
class TemplateKeyLogic(ScriptedLoadableModuleLogic):
- """This class should implement all the actual
- computation done by your module. The interface
- should be such that other python code can import
- this class and make use of the functionality without
- requiring an instance of the Widget.
- Uses ScriptedLoadableModuleLogic base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self):
- """
- Called when the logic class is instantiated. Can be used for initializing member variables.
- """
- ScriptedLoadableModuleLogic.__init__(self)
-
- def setDefaultParameters(self, parameterNode):
- """
- Initialize parameter node with default settings.
- """
- if not parameterNode.GetParameter("Threshold"):
- parameterNode.SetParameter("Threshold", "100.0")
- if not parameterNode.GetParameter("Invert"):
- parameterNode.SetParameter("Invert", "false")
-
- def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True):
- """
- Run the processing algorithm.
- Can be used without GUI widget.
- :param inputVolume: volume to be thresholded
- :param outputVolume: thresholding result
- :param imageThreshold: values above/below this threshold will be set to 0
- :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0
- :param showResult: show output volume in slice viewers
- """
-
- if not inputVolume or not outputVolume:
- raise ValueError("Input or output volume is invalid")
-
- import time
- startTime = time.time()
- logging.info('Processing started')
-
- # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module
- cliParams = {
- 'InputVolume': inputVolume.GetID(),
- 'OutputVolume': outputVolume.GetID(),
- 'ThresholdValue': imageThreshold,
- 'ThresholdType': 'Above' if invert else 'Below'
- }
- cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult)
- # We don't need the CLI module node anymore, remove it to not clutter the scene with it
- slicer.mrmlScene.RemoveNode(cliNode)
-
- stopTime = time.time()
- logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds')
+ """This class should implement all the actual
+ computation done by your module. The interface
+ should be such that other python code can import
+ this class and make use of the functionality without
+ requiring an instance of the Widget.
+ Uses ScriptedLoadableModuleLogic base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
+ """
+
+ def __init__(self):
+ """
+ Called when the logic class is instantiated. Can be used for initializing member variables.
+ """
+ ScriptedLoadableModuleLogic.__init__(self)
+
+ def setDefaultParameters(self, parameterNode):
+ """
+ Initialize parameter node with default settings.
+ """
+ if not parameterNode.GetParameter("Threshold"):
+ parameterNode.SetParameter("Threshold", "100.0")
+ if not parameterNode.GetParameter("Invert"):
+ parameterNode.SetParameter("Invert", "false")
+
+ def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showResult=True):
+ """
+ Run the processing algorithm.
+ Can be used without GUI widget.
+ :param inputVolume: volume to be thresholded
+ :param outputVolume: thresholding result
+ :param imageThreshold: values above/below this threshold will be set to 0
+ :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0
+ :param showResult: show output volume in slice viewers
+ """
+
+ if not inputVolume or not outputVolume:
+ raise ValueError("Input or output volume is invalid")
+
+ import time
+ startTime = time.time()
+ logging.info('Processing started')
+
+ # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module
+ cliParams = {
+ 'InputVolume': inputVolume.GetID(),
+ 'OutputVolume': outputVolume.GetID(),
+ 'ThresholdValue': imageThreshold,
+ 'ThresholdType': 'Above' if invert else 'Below'
+ }
+ cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult)
+ # We don't need the CLI module node anymore, remove it to not clutter the scene with it
+ slicer.mrmlScene.RemoveNode(cliNode)
+
+ stopTime = time.time()
+ logging.info(f'Processing completed in {stopTime-startTime:.2f} seconds')
#
@@ -352,65 +352,65 @@ def process(self, inputVolume, outputVolume, imageThreshold, invert=False, showR
#
class TemplateKeyTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
- """
- slicer.mrmlScene.Clear()
-
- def runTest(self):
- """Run as few or as many tests as needed here.
"""
- self.setUp()
- self.test_TemplateKey1()
-
- def test_TemplateKey1(self):
- """ Ideally you should have several levels of tests. At the lowest level
- tests should exercise the functionality of the logic with different inputs
- (both valid and invalid). At higher levels your tests should emulate the
- way the user would interact with your code and confirm that it still works
- the way you intended.
- One of the most important features of the tests is that it should alert other
- developers when their changes will have an impact on the behavior of your
- module. For example, if a developer removes a feature that you depend on,
- your test should break so they know that the feature is needed.
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting the test")
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear()
- # Get/create input data
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_TemplateKey1()
- import SampleData
- registerSampleData()
- inputVolume = SampleData.downloadSample('TemplateKey1')
- self.delayDisplay('Loaded test data set')
+ def test_TemplateKey1(self):
+ """ Ideally you should have several levels of tests. At the lowest level
+ tests should exercise the functionality of the logic with different inputs
+ (both valid and invalid). At higher levels your tests should emulate the
+ way the user would interact with your code and confirm that it still works
+ the way you intended.
+ One of the most important features of the tests is that it should alert other
+ developers when their changes will have an impact on the behavior of your
+ module. For example, if a developer removes a feature that you depend on,
+ your test should break so they know that the feature is needed.
+ """
+
+ self.delayDisplay("Starting the test")
+
+ # Get/create input data
+
+ import SampleData
+ registerSampleData()
+ inputVolume = SampleData.downloadSample('TemplateKey1')
+ self.delayDisplay('Loaded test data set')
- inputScalarRange = inputVolume.GetImageData().GetScalarRange()
- self.assertEqual(inputScalarRange[0], 0)
- self.assertEqual(inputScalarRange[1], 695)
+ inputScalarRange = inputVolume.GetImageData().GetScalarRange()
+ self.assertEqual(inputScalarRange[0], 0)
+ self.assertEqual(inputScalarRange[1], 695)
- outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode")
- threshold = 100
+ outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode")
+ threshold = 100
- # Test the module logic
+ # Test the module logic
- logic = TemplateKeyLogic()
+ logic = TemplateKeyLogic()
- # Test algorithm with non-inverted threshold
- logic.process(inputVolume, outputVolume, threshold, True)
- outputScalarRange = outputVolume.GetImageData().GetScalarRange()
- self.assertEqual(outputScalarRange[0], inputScalarRange[0])
- self.assertEqual(outputScalarRange[1], threshold)
+ # Test algorithm with non-inverted threshold
+ logic.process(inputVolume, outputVolume, threshold, True)
+ outputScalarRange = outputVolume.GetImageData().GetScalarRange()
+ self.assertEqual(outputScalarRange[0], inputScalarRange[0])
+ self.assertEqual(outputScalarRange[1], threshold)
- # Test algorithm with inverted threshold
- logic.process(inputVolume, outputVolume, threshold, False)
- outputScalarRange = outputVolume.GetImageData().GetScalarRange()
- self.assertEqual(outputScalarRange[0], inputScalarRange[0])
- self.assertEqual(outputScalarRange[1], inputScalarRange[1])
+ # Test algorithm with inverted threshold
+ logic.process(inputVolume, outputVolume, threshold, False)
+ outputScalarRange = outputVolume.GetImageData().GetScalarRange()
+ self.assertEqual(outputScalarRange[0], inputScalarRange[0])
+ self.assertEqual(outputScalarRange[1], inputScalarRange[1])
- self.delayDisplay('Test passed')
+ self.delayDisplay('Test passed')
diff --git a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py
index b83a23f99f5..83f1b8d58c5 100644
--- a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py
+++ b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKey.py
@@ -7,133 +7,133 @@
class SegmentEditorTemplateKey(ScriptedLoadableModule):
- """Uses ScriptedLoadableModule base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def __init__(self, parent):
- ScriptedLoadableModule.__init__(self, parent)
- self.parent.title = "SegmentEditorTemplateKey"
- self.parent.categories = ["Segmentation"]
- self.parent.dependencies = ["Segmentations"]
- self.parent.contributors = ["Andras Lasso (PerkLab)"]
- self.parent.hidden = True
- self.parent.helpText = "This hidden module registers the segment editor effect"
- self.parent.helpText += self.getDefaultModuleDocumentationLink()
- self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details."
- slicer.app.connect("startupCompleted()", self.registerEditorEffect)
-
- def registerEditorEffect(self):
- import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects
- instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None)
- effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py')
- instance.setPythonSource(effectFilename.replace('\\', '/'))
- instance.self().register()
-
-
-class SegmentEditorTemplateKeyTest(ScriptedLoadableModuleTest):
- """
- This is the test case for your scripted module.
- Uses ScriptedLoadableModuleTest base class, available at:
- https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
- """
-
- def setUp(self):
- """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """Uses ScriptedLoadableModule base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- slicer.mrmlScene.Clear(0)
- def runTest(self):
- """Run as few or as many tests as needed here.
- """
- self.setUp()
- self.test_TemplateKey1()
+ def __init__(self, parent):
+ ScriptedLoadableModule.__init__(self, parent)
+ self.parent.title = "SegmentEditorTemplateKey"
+ self.parent.categories = ["Segmentation"]
+ self.parent.dependencies = ["Segmentations"]
+ self.parent.contributors = ["Andras Lasso (PerkLab)"]
+ self.parent.hidden = True
+ self.parent.helpText = "This hidden module registers the segment editor effect"
+ self.parent.helpText += self.getDefaultModuleDocumentationLink()
+ self.parent.acknowledgementText = "Supported by NA-MIC, NAC, BIRN, NCIGT, and the Slicer Community. See https://www.slicer.org for details."
+ slicer.app.connect("startupCompleted()", self.registerEditorEffect)
+
+ def registerEditorEffect(self):
+ import qSlicerSegmentationsEditorEffectsPythonQt as qSlicerSegmentationsEditorEffects
+ instance = qSlicerSegmentationsEditorEffects.qSlicerSegmentEditorScriptedEffect(None)
+ effectFilename = os.path.join(os.path.dirname(__file__), self.__class__.__name__ + 'Lib/SegmentEditorEffect.py')
+ instance.setPythonSource(effectFilename.replace('\\', '/'))
+ instance.self().register()
+
- def test_TemplateKey1(self):
+class SegmentEditorTemplateKeyTest(ScriptedLoadableModuleTest):
"""
- Basic automated test of the segmentation method:
- - Create segmentation by placing sphere-shaped seeds
- - Run segmentation
- - Verify results using segment statistics
- The test can be executed from SelfTests module (test name: SegmentEditorTemplateKey)
+ This is the test case for your scripted module.
+ Uses ScriptedLoadableModuleTest base class, available at:
+ https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
"""
- self.delayDisplay("Starting test_TemplateKey1")
-
- import vtkSegmentationCorePython as vtkSegmentationCore
- import SampleData
- from SegmentStatistics import SegmentStatisticsLogic
-
- ##################################
- self.delayDisplay("Load master volume")
-
- masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
-
- ##################################
- self.delayDisplay("Create segmentation containing a few spheres")
-
- segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
- segmentationNode.CreateDefaultDisplayNodes()
- segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
-
- # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ]
- segmentGeometries = [
- ['Tumor', [[10, -6, 30, 28]]],
- ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]],
- ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]]
- for segmentGeometry in segmentGeometries:
- segmentName = segmentGeometry[0]
- appender = vtk.vtkAppendPolyData()
- for sphere in segmentGeometry[1]:
- sphereSource = vtk.vtkSphereSource()
- sphereSource.SetRadius(sphere[0])
- sphereSource.SetCenter(sphere[1], sphere[2], sphere[3])
- appender.AddInputConnection(sphereSource.GetOutputPort())
- segment = vtkSegmentationCore.vtkSegment()
- segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName))
- appender.Update()
- segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput())
- segmentationNode.GetSegmentation().AddSegment(segment)
-
- ##################################
- self.delayDisplay("Create segment editor")
-
- segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
- segmentEditorWidget.show()
- segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
- segmentEditorNode = slicer.vtkMRMLSegmentEditorNode()
- slicer.mrmlScene.AddNode(segmentEditorNode)
- segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)
- segmentEditorWidget.setSegmentationNode(segmentationNode)
- segmentEditorWidget.setMasterVolumeNode(masterVolumeNode)
-
- ##################################
- self.delayDisplay("Run segmentation")
- segmentEditorWidget.setActiveEffectByName("TemplateKey")
- effect = segmentEditorWidget.activeEffect()
- effect.setParameter("ObjectScaleMm", 3.0)
- effect.self().onApply()
-
- ##################################
- self.delayDisplay("Make segmentation results nicely visible in 3D")
- segmentationDisplayNode = segmentationNode.GetDisplayNode()
- segmentationDisplayNode.SetSegmentVisibility("Air", False)
- segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5)
-
- ##################################
- self.delayDisplay("Compute statistics")
-
- segStatLogic = SegmentStatisticsLogic()
- segStatLogic.computeStatistics(segmentationNode, masterVolumeNode)
-
- # Export results to table (just to see all results)
- resultsTableNode = slicer.vtkMRMLTableNode()
- slicer.mrmlScene.AddNode(resultsTableNode)
- segStatLogic.exportToTable(resultsTableNode)
- segStatLogic.showTable(resultsTableNode)
-
- self.delayDisplay("Check a few numerical results")
- self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16)
- self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010)
-
- self.delayDisplay('test_TemplateKey1 passed')
+ def setUp(self):
+ """ Do whatever is needed to reset the state - typically a scene clear will be enough.
+ """
+ slicer.mrmlScene.Clear(0)
+
+ def runTest(self):
+ """Run as few or as many tests as needed here.
+ """
+ self.setUp()
+ self.test_TemplateKey1()
+
+ def test_TemplateKey1(self):
+ """
+ Basic automated test of the segmentation method:
+ - Create segmentation by placing sphere-shaped seeds
+ - Run segmentation
+ - Verify results using segment statistics
+ The test can be executed from SelfTests module (test name: SegmentEditorTemplateKey)
+ """
+
+ self.delayDisplay("Starting test_TemplateKey1")
+
+ import vtkSegmentationCorePython as vtkSegmentationCore
+ import SampleData
+ from SegmentStatistics import SegmentStatisticsLogic
+
+ ##################################
+ self.delayDisplay("Load master volume")
+
+ masterVolumeNode = SampleData.downloadSample('MRBrainTumor1')
+
+ ##################################
+ self.delayDisplay("Create segmentation containing a few spheres")
+
+ segmentationNode = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLSegmentationNode')
+ segmentationNode.CreateDefaultDisplayNodes()
+ segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(masterVolumeNode)
+
+ # Segments are defined by a list of: name and a list of sphere [radius, posX, posY, posZ]
+ segmentGeometries = [
+ ['Tumor', [[10, -6, 30, 28]]],
+ ['Background', [[10, 0, 65, 22], [15, 1, -14, 30], [12, 0, 28, -7], [5, 0, 30, 54], [12, 31, 33, 27], [17, -42, 30, 27], [6, -2, -17, 71]]],
+ ['Air', [[10, 76, 73, 0], [15, -70, 74, 0]]]]
+ for segmentGeometry in segmentGeometries:
+ segmentName = segmentGeometry[0]
+ appender = vtk.vtkAppendPolyData()
+ for sphere in segmentGeometry[1]:
+ sphereSource = vtk.vtkSphereSource()
+ sphereSource.SetRadius(sphere[0])
+ sphereSource.SetCenter(sphere[1], sphere[2], sphere[3])
+ appender.AddInputConnection(sphereSource.GetOutputPort())
+ segment = vtkSegmentationCore.vtkSegment()
+ segment.SetName(segmentationNode.GetSegmentation().GenerateUniqueSegmentID(segmentName))
+ appender.Update()
+ segment.AddRepresentation(vtkSegmentationCore.vtkSegmentationConverter.GetSegmentationClosedSurfaceRepresentationName(), appender.GetOutput())
+ segmentationNode.GetSegmentation().AddSegment(segment)
+
+ ##################################
+ self.delayDisplay("Create segment editor")
+
+ segmentEditorWidget = slicer.qMRMLSegmentEditorWidget()
+ segmentEditorWidget.show()
+ segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
+ segmentEditorNode = slicer.vtkMRMLSegmentEditorNode()
+ slicer.mrmlScene.AddNode(segmentEditorNode)
+ segmentEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode)
+ segmentEditorWidget.setSegmentationNode(segmentationNode)
+ segmentEditorWidget.setMasterVolumeNode(masterVolumeNode)
+
+ ##################################
+ self.delayDisplay("Run segmentation")
+ segmentEditorWidget.setActiveEffectByName("TemplateKey")
+ effect = segmentEditorWidget.activeEffect()
+ effect.setParameter("ObjectScaleMm", 3.0)
+ effect.self().onApply()
+
+ ##################################
+ self.delayDisplay("Make segmentation results nicely visible in 3D")
+ segmentationDisplayNode = segmentationNode.GetDisplayNode()
+ segmentationDisplayNode.SetSegmentVisibility("Air", False)
+ segmentationDisplayNode.SetSegmentOpacity3D("Background", 0.5)
+
+ ##################################
+ self.delayDisplay("Compute statistics")
+
+ segStatLogic = SegmentStatisticsLogic()
+ segStatLogic.computeStatistics(segmentationNode, masterVolumeNode)
+
+ # Export results to table (just to see all results)
+ resultsTableNode = slicer.vtkMRMLTableNode()
+ slicer.mrmlScene.AddNode(resultsTableNode)
+ segStatLogic.exportToTable(resultsTableNode)
+ segStatLogic.showTable(resultsTableNode)
+
+ self.delayDisplay("Check a few numerical results")
+ self.assertEqual(round(segStatLogic.statistics["Tumor", "LM volume cc"]), 16)
+ self.assertEqual(round(segStatLogic.statistics["Background", "LM volume cc"]), 3010)
+
+ self.delayDisplay('test_TemplateKey1 passed')
diff --git a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py
index 59306dfd357..43c225703fd 100644
--- a/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py
+++ b/Utilities/Templates/Modules/ScriptedSegmentEditorEffect/SegmentEditorTemplateKeyLib/SegmentEditorEffect.py
@@ -10,123 +10,123 @@
class SegmentEditorEffect(AbstractScriptedSegmentEditorEffect):
- """This effect uses Watershed algorithm to partition the input volume"""
-
- def __init__(self, scriptedEffect):
- scriptedEffect.name = 'TemplateKey'
- scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment)
- scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation
- AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
-
- def clone(self):
- # It should not be necessary to modify this method
- import qSlicerSegmentationsEditorEffectsPythonQt as effects
- clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
- clonedEffect.setPythonSource(__file__.replace('\\', '/'))
- return clonedEffect
-
- def icon(self):
- # It should not be necessary to modify this method
- iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png')
- if os.path.exists(iconPath):
- return qt.QIcon(iconPath)
- return qt.QIcon()
-
- def helpText(self):
- return """Existing segments are grown to fill the image.
+ """This effect uses Watershed algorithm to partition the input volume"""
+
+ def __init__(self, scriptedEffect):
+ scriptedEffect.name = 'TemplateKey'
+ scriptedEffect.perSegment = False # this effect operates on all segments at once (not on a single selected segment)
+ scriptedEffect.requireSegments = True # this effect requires segment(s) existing in the segmentation
+ AbstractScriptedSegmentEditorEffect.__init__(self, scriptedEffect)
+
+ def clone(self):
+ # It should not be necessary to modify this method
+ import qSlicerSegmentationsEditorEffectsPythonQt as effects
+ clonedEffect = effects.qSlicerSegmentEditorScriptedEffect(None)
+ clonedEffect.setPythonSource(__file__.replace('\\', '/'))
+ return clonedEffect
+
+ def icon(self):
+ # It should not be necessary to modify this method
+ iconPath = os.path.join(os.path.dirname(__file__), 'SegmentEditorEffect.png')
+ if os.path.exists(iconPath):
+ return qt.QIcon(iconPath)
+ return qt.QIcon()
+
+ def helpText(self):
+ return """Existing segments are grown to fill the image.
The effect is different from the Grow from seeds effect in that smoothness of structures can be defined, which can prevent leakage.
To segment a single object, create a segment and paint inside and create another segment and paint outside on each axis.
"""
- def setupOptionsFrame(self):
-
- # Object scale slider
- self.objectScaleMmSlider = slicer.qMRMLSliderWidget()
- self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene)
- self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node
- self.objectScaleMmSlider.minimum = 0
- self.objectScaleMmSlider.maximum = 10
- self.objectScaleMmSlider.value = 2.0
- self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.')
- self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider)
- self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI)
-
- # Apply button
- self.applyButton = qt.QPushButton("Apply")
- self.applyButton.objectName = self.__class__.__name__ + 'Apply'
- self.applyButton.setToolTip("Accept previewed result")
- self.scriptedEffect.addOptionsWidget(self.applyButton)
- self.applyButton.connect('clicked()', self.onApply)
-
- def createCursor(self, widget):
- # Turn off effect-specific cursor for this effect
- return slicer.util.mainWindow().cursor
-
- def setMRMLDefaults(self):
- self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0)
-
- def updateGUIFromMRML(self):
- objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm")
- wasBlocked = self.objectScaleMmSlider.blockSignals(True)
- self.objectScaleMmSlider.value = abs(objectScaleMm)
- self.objectScaleMmSlider.blockSignals(wasBlocked)
-
- def updateMRMLFromGUI(self):
- self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value)
-
- def onApply(self):
-
- # Get list of visible segment IDs, as the effect ignores hidden segments.
- segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
- visibleSegmentIds = vtk.vtkStringArray()
- segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
- if visibleSegmentIds.GetNumberOfValues() == 0:
- logging.info("Smoothing operation skipped: there are no visible segments")
- return
-
- # This can be a long operation - indicate it to the user
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- # Allow users revert to this state by clicking Undo
- self.scriptedEffect.saveStateForUndo()
-
- # Export master image data to temporary new volume node.
- # Note: Although the original master volume node is already in the scene, we do not use it here,
- # because the master volume may have been resampled to match segmentation geometry.
- masterVolumeNode = slicer.vtkMRMLScalarVolumeNode()
- slicer.mrmlScene.AddNode(masterVolumeNode)
- masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID())
- slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode)
- # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels.
- mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
- slicer.mrmlScene.AddNode(mergedLabelmapNode)
- slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode)
-
- # Run segmentation algorithm
- import SimpleITK as sitk
- import sitkUtils
- # Read input data from Slicer into SimpleITK
- labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
- backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName()))
- # Run watershed filter
- featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm")))
- del backgroundImage
- f = sitk.MorphologicalWatershedFromMarkersImageFilter()
- f.SetMarkWatershedLine(False)
- f.SetFullyConnected(False)
- labelImage = f.Execute(featureImage, labelImage)
- del featureImage
- # Pixel type of watershed output is the same as the input. Convert it to int16 now.
- if labelImage.GetPixelID() != sitk.sitkInt16:
- labelImage = sitk.Cast(labelImage, sitk.sitkInt16)
- # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data.
- sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
- mergedLabelmapNode.GetImageData().Modified()
- mergedLabelmapNode.Modified()
-
- # Update segmentation from labelmap node and remove temporary nodes
- slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds)
- slicer.mrmlScene.RemoveNode(masterVolumeNode)
- slicer.mrmlScene.RemoveNode(mergedLabelmapNode)
-
- qt.QApplication.restoreOverrideCursor()
+ def setupOptionsFrame(self):
+
+ # Object scale slider
+ self.objectScaleMmSlider = slicer.qMRMLSliderWidget()
+ self.objectScaleMmSlider.setMRMLScene(slicer.mrmlScene)
+ self.objectScaleMmSlider.quantity = "length" # get unit, precision, etc. from MRML unit node
+ self.objectScaleMmSlider.minimum = 0
+ self.objectScaleMmSlider.maximum = 10
+ self.objectScaleMmSlider.value = 2.0
+ self.objectScaleMmSlider.setToolTip('Increasing this value smooths the segmentation and reduces leaks. This is the sigma used for edge detection.')
+ self.scriptedEffect.addLabeledOptionsWidget("Object scale:", self.objectScaleMmSlider)
+ self.objectScaleMmSlider.connect('valueChanged(double)', self.updateMRMLFromGUI)
+
+ # Apply button
+ self.applyButton = qt.QPushButton("Apply")
+ self.applyButton.objectName = self.__class__.__name__ + 'Apply'
+ self.applyButton.setToolTip("Accept previewed result")
+ self.scriptedEffect.addOptionsWidget(self.applyButton)
+ self.applyButton.connect('clicked()', self.onApply)
+
+ def createCursor(self, widget):
+ # Turn off effect-specific cursor for this effect
+ return slicer.util.mainWindow().cursor
+
+ def setMRMLDefaults(self):
+ self.scriptedEffect.setParameterDefault("ObjectScaleMm", 2.0)
+
+ def updateGUIFromMRML(self):
+ objectScaleMm = self.scriptedEffect.doubleParameter("ObjectScaleMm")
+ wasBlocked = self.objectScaleMmSlider.blockSignals(True)
+ self.objectScaleMmSlider.value = abs(objectScaleMm)
+ self.objectScaleMmSlider.blockSignals(wasBlocked)
+
+ def updateMRMLFromGUI(self):
+ self.scriptedEffect.setParameter("ObjectScaleMm", self.objectScaleMmSlider.value)
+
+ def onApply(self):
+
+ # Get list of visible segment IDs, as the effect ignores hidden segments.
+ segmentationNode = self.scriptedEffect.parameterSetNode().GetSegmentationNode()
+ visibleSegmentIds = vtk.vtkStringArray()
+ segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(visibleSegmentIds)
+ if visibleSegmentIds.GetNumberOfValues() == 0:
+ logging.info("Smoothing operation skipped: there are no visible segments")
+ return
+
+ # This can be a long operation - indicate it to the user
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ # Allow users revert to this state by clicking Undo
+ self.scriptedEffect.saveStateForUndo()
+
+ # Export master image data to temporary new volume node.
+ # Note: Although the original master volume node is already in the scene, we do not use it here,
+ # because the master volume may have been resampled to match segmentation geometry.
+ masterVolumeNode = slicer.vtkMRMLScalarVolumeNode()
+ slicer.mrmlScene.AddNode(masterVolumeNode)
+ masterVolumeNode.SetAndObserveTransformNodeID(segmentationNode.GetTransformNodeID())
+ slicer.vtkSlicerSegmentationsModuleLogic.CopyOrientedImageDataToVolumeNode(self.scriptedEffect.masterVolumeImageData(), masterVolumeNode)
+ # Generate merged labelmap of all visible segments, as the filter expects a single labelmap with all the labels.
+ mergedLabelmapNode = slicer.vtkMRMLLabelMapVolumeNode()
+ slicer.mrmlScene.AddNode(mergedLabelmapNode)
+ slicer.vtkSlicerSegmentationsModuleLogic.ExportSegmentsToLabelmapNode(segmentationNode, visibleSegmentIds, mergedLabelmapNode, masterVolumeNode)
+
+ # Run segmentation algorithm
+ import SimpleITK as sitk
+ import sitkUtils
+ # Read input data from Slicer into SimpleITK
+ labelImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
+ backgroundImage = sitk.ReadImage(sitkUtils.GetSlicerITKReadWriteAddress(masterVolumeNode.GetName()))
+ # Run watershed filter
+ featureImage = sitk.GradientMagnitudeRecursiveGaussian(backgroundImage, float(self.scriptedEffect.doubleParameter("ObjectScaleMm")))
+ del backgroundImage
+ f = sitk.MorphologicalWatershedFromMarkersImageFilter()
+ f.SetMarkWatershedLine(False)
+ f.SetFullyConnected(False)
+ labelImage = f.Execute(featureImage, labelImage)
+ del featureImage
+ # Pixel type of watershed output is the same as the input. Convert it to int16 now.
+ if labelImage.GetPixelID() != sitk.sitkInt16:
+ labelImage = sitk.Cast(labelImage, sitk.sitkInt16)
+ # Write result from SimpleITK to Slicer. This currently performs a deep copy of the bulk data.
+ sitk.WriteImage(labelImage, sitkUtils.GetSlicerITKReadWriteAddress(mergedLabelmapNode.GetName()))
+ mergedLabelmapNode.GetImageData().Modified()
+ mergedLabelmapNode.Modified()
+
+ # Update segmentation from labelmap node and remove temporary nodes
+ slicer.vtkSlicerSegmentationsModuleLogic.ImportLabelmapToSegmentationNode(mergedLabelmapNode, segmentationNode, visibleSegmentIds)
+ slicer.mrmlScene.RemoveNode(masterVolumeNode)
+ slicer.mrmlScene.RemoveNode(mergedLabelmapNode)
+
+ qt.QApplication.restoreOverrideCursor()