Handling Node Names with Square Brackets and Filtering Nodes of RTDose Type Exported from TPS in 3DSlicer

Hello Dear Developers and Users,

I am facing two issues:

First issue: Running getNode on a node whose name contains square brackets with a number inside.

I noticed that if a node’s name contains square brackets with a number or expression inside (for example [11]), I get the following error when using the Python Interactor. Let’s assume that when reading a DICOM RTDose file using the DICOM Module, I see a node with the name:

2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) [11]: Beam setup 1

in the Data Module. Now, if I try to read it with getNode, the following error occurs:

>>> rtDose = getNode('2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) [11]: Beam setup 1');
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/sn/Slicer-5.0.2-linux-amd64/bin/Python/slicer/util.py", line 1436, in getNode
    raise MRMLNodeNotFoundException("could not find nodes in the scene by name or id '%s'" % (pattern if (isinstance(pattern, str)) else ""))
slicer.util.MRMLNodeNotFoundException: could not find nodes in the scene by name or id '2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) [11]: Beam setup 1'
>>> 

However, if I manually rename the node to (removing 11 from [11]):

2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) : Beam setup 1

then the error does not occur, and I can get the node with this code:

>>> rtDose = getNode('2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) []: Beam setup 1');
>>>

With this explanation, how can I read a node with a name that has this property using getNode?
The figure below shows this error.

Second issue: Filtering nodes in a ComboBox based on their type.
Is it possible to have a ComboBox where only nodes of the type RTDose are listed, and other nodes, such as those related to CT images, are not shown?

In the following figure, I have two nodes with the names of 2: Unnamed Series and 2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) []: Beam setup 1, both of which are of the type vtkMRMLScalarVolumeNode. However, for example, in the Isodose module, I can only see the node of type RTDOSE, not the other one.

Please guide me.
Best regards.
Shahrokh

First issue:

I confirm that getNode does not work with square brackets in the latest. I think it used to work, because I regularly accessed beam nodes and they have these brackets in the name. This may be due to the new translation features? Not sure.

For the time being you can use slicer.mrmlScene.GetFirstNodeByName('MRHead [11]')

Second issue:

Please look at the comboboxes in Isodose or DoseVolumeHistogram modules in SlicerRT, they can filter to show only RTDose volumes.

Thank you very much for your answers and guidance. I understand that the Isodose or DoseVolumeHistogram modules have the filtering feature based on the type of node. I was wondering which property has been used in the RTDose type nodes that allows these modules to list only this type of node?

Best regards.
Shahrokh

Unfortunately, I still haven’t been able to implement the UI that is similar to the comboboxes in the DoseVolumeHistogram or Isodose modules, where they can filter and only show RTDose volumes. However, I found some interesting points. One of these is the presence of an Attribute called DicomRtImport.DoseVolume with a value of 1 in RTDose volumes. Based on this, I can identify and separate the nodes with this attribute from the ones in the Data module using the following commands:

import slicer
listVolumeNodes = list(slicer.mrmlScene.GetNodesByClass('vtkMRMLScalarVolumeNode'))
strClass = 'vtkMRMLScalarVolumeNode'

for nodeNumber in range(len(listVolumeNodes)):
 nodeID = strClass + str(nodeNumber+1)
 print(nodeID)
 slicer.mrmlScene.GetNodeByID(nodeID).GetAttributeNames()

strAttributeDoseVolume = 'DicomRtImport.DoseVolume'

for nodeNumber in range(len(listVolumeNodes)):
 nodeID = strClass + str(nodeNumber+1)
 if strAttributeDoseVolume == slicer.mrmlScene.GetNodeByID(nodeID).GetAttributeNames()[0]:
  print('Attribute Node: ', slicer.mrmlScene.GetNodeByID(nodeID).GetAttributeNames())
  nodeDoseVolume = slicer.mrmlScene.GetNodeByID(nodeID)

However, I couldn’t reach my goal in the UI, which is to only show RTDose Volume in the ComboBox. Still, the code I have written so far is as follows:
import os



import unittest
from __main__ import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
import qt
parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Parameters"
parametersCollapsibleButton.show()
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
inputSelector0 = slicer.qMRMLNodeComboBox()
inputSelector0.nodeTypes = ["vtkMRMLScalarVolumeNode"]
inputSelector0.addEnabled = False
inputSelector0.removeEnabled = False
inputSelector0.noneEnabled = False
inputSelector0.showHidden = False
inputSelector0.showChildNodeTypes = False
inputSelector0.setMRMLScene( slicer.mrmlScene )
#inputSelector0.setCurrentNodeID(slicer.mrmlScene.GetNodeByID(nodeID))
#inputSelector0.setCurrentNode(slicer.mrmlScene.GetNodeByID(nodeID))
#inputSelector0.setCurrentNodeIndex(slicer.mrmlScene.GetNodeByID(nodeID))
#inputSelector0.setNodeTypeLabel(slicer.mrmlScene.GetNodeByID(nodeID))
inputSelector0.setToolTip( "Pick the input to the algorithm." )
parametersFormLayout.addRow("Dose Volume: ", inputSelector0)
#parametersFormLayout.addRow("Dose Volume: ", inputSelector0.setCurrentNodeID(nodeID))

Screenshot:

As you can see from the commands above, I used various “Get” functions from slicer.mrmlScene, but none of them gave the desired result.
As you suggested, I studied the module code from DoseVolumeHistogram on GitHub. Although it seemed like the nodeType was assigned the value vtkMRMLScalarVolumeNode, I couldn’t figure out what other condition was added to make the combobox work correctly.

In any case, I will keep reading through this code to see if I can find a solution to my issue. I believe that when someone is looking for an answer to their problem on GitHub, StackOverflow, or in forums, they inevitably come across many other problems and solutions. Still, I would be happy if you could guide me.

Best regards.
Shahrokh

  1. getNode behavior for some special characters

getNode is only intended for quick access to nodes for testing and troubleshooting. For convenience, it uses searching with wildcards in filename format (e.g., you can ask getNode('something*')), but it also means that some special characters (such as *, square brackets, etc.) need to be escaped. It has many other limitations, such as it only returns the first node by that name (while in scene you often have several nodes by the exact same name). So, it is a convenient shorthand for simple cases, use it when it works for you.

If you have several nodes by the same name or the node name contain special characters, I would recommend to use:

  • getNode with the node ID as argument (it does not contain special character and it is unique within a scene)
  • getFirstNodeByClassByName function, which allows you to select the right node even when there are various other nodes of a different class by the same name. It also comes without filename search pattern matching, so you don’t need to worry about special characters. For example, try: getFirstNodeByClassByName("vtkMRMLScalarVolumeNode", "2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) [11]: Beam setup 1"
  1. Filtering in node selector

As @cpinter suggested, you can have a look how it is done in DoseVolumeHistogram module:

In the future, please post unrelated questions as two separate topics. Thank you!

First of all, I would like to thank you for your excellent guidance and support. The point you mentioned at the end, I will definitely take into consideration.

I have learned some interesting points from the existing code, such as vtkSlicerRtCommon.cxx. For example, I learned how to determine whether a volume node is of type RTDose based on the attribute DicomRtImport.DoseVolume. To achieve this, I implemented a function called doseVolumeFilter:

def doseVolumeFilter(listVolumeNodes):
 strClass = 'vtkMRMLScalarVolumeNode'
 strAttributeDoseVolume = 'DicomRtImport.DoseVolume'
 for nodeNumber in range(len(listVolumeNodes)):
  nodeID = strClass + str(nodeNumber+1)
  if strAttributeDoseVolume == slicer.mrmlScene.GetNodeByID(nodeID).GetAttributeNames()[0]:
   attributesDoseNode = slicer.mrmlScene.GetNodeByID(nodeID).GetAttributeNames()
   nodeDoseVolume = slicer.mrmlScene.GetNodeByID(nodeID)
   return nodeID, nodeDoseVolume, attributesDoseNode

Execution of this function:

listVolumeNodes = list(slicer.mrmlScene.GetNodesByClass('vtkMRMLScalarVolumeNode'))
returns_doseVolumeFilter=doseVolumeFilter(listVolumeNodes)
print("node IDs:", returns_doseVolumeFilter[0])
print("Dose node ID:", returns_doseVolumeFilter[1])
print("Attribute Dose node:", returns_doseVolumeFilter[2])

I should mention, however, that after writing this function, I noticed that the IsDoseVolumeNode function in the vtkSlicerRtCommon.cxx file does the same thing.

Execution of this funtion:

import vtkSlicerRtCommonPython as vtkSlicerRtCommon
rtDose = slicer.util.getFirstNodeByClassByName("vtkMRMLScalarVolumeNode", "2: RTDOSE: DOSIsoft:RTDOSE:Phase #1 Dosi Dosi 1: Beam setup 1 (GY) [11]: Beam setup 1")
vtkSlicerRtCommon.vtkSlicerRtCommon().IsDoseVolumeNode(rtDose)
#Output: True

Anyway, I am trying hard to write a combobox that only lists volumes of type RTDose. However, I haven’t been successful yet. As shown in the following lines:

import os
import unittest
from __main__ import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
import qt

parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Parameters"
parametersCollapsibleButton.show()
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
inputSelector0 = slicer.qMRMLNodeComboBox()
inputSelector0.setMRMLScene( slicer.mrmlScene )
inputSelector0.enabled
inputSelector0.setToolTip( "Pick the input to the algorithm." )
parametersFormLayout.addRow("Dose Volume: ", inputSelector0)

The result of executing these commands is a combobox that lists different types of nodes. In these lines, the nodeTypes is not specified, as following figure:

Based on line 65 of the vtkSlicerRtCommon.cxx file, I thought perhaps an attribute named DICOMRTIMPORT_DOSE_VOLUME_IDENTIFIER_ATTRIBUTE_NAME with the value DicomRtImport.DoseVolume should be added to the combobox.

Accordingly, I executed the following code:

import os
import unittest
from __main__ import vtk, qt, ctk, slicer
from slicer.ScriptedLoadableModule import *
import qt

parametersCollapsibleButton = ctk.ctkCollapsibleButton()
parametersCollapsibleButton.text = "Parameters"
parametersCollapsibleButton.show()
parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton)
inputSelector0 = slicer.qMRMLNodeComboBox()
inputSelector0.nodeTypes = ["vtkMRMLScalarVolumeNode"]
inputSelector0.addAttribute("vtkMRMLScalarVolumeNode", "DICOMRTIMPORT_DOSE_VOLUME_IDENTIFIER_ATTRIBUTE_NAME", "DicomRtImport.DoseVolume")
inputSelector0.setMRMLScene( slicer.mrmlScene )
inputSelector0.enabled
inputSelector0.setToolTip( "Pick the input to the algorithm." )
parametersFormLayout.addRow("Dose Volume: ", inputSelector0)

In these commands, the nodeTypes and addAttribute methods were used. However, as you can see in the figure below, contrary to my expectation, the node list in the combobox is empty.

Based on line 271 of the qSlicerIsodoseModuleWidget.cxx file, it occurred to me that perhaps the filtering should be done through the inputSelector0.connect function, as dear Andras Lasso has pointed out.

I think the issue lies in the implementation of this function. In my module, named RTDoseSelector.py, I implemented this function as follows:

    # connections
    self.inputSelector0.connect("currentNodeChanged(vtkMRMLNode*)", self.onSelect)

If possible, could you please guide me? In my opinion, acquiring the ability to filter nodes based on their type and attribute is very important. The attribute is particularly significant because the difference between RTDose and other volumeNode types lies in its attribute.

Best regards.
Shahrokh

Excuse me, I think that in order for the combobox to have filtering capability (to pass volumes with the attribute equal to DicomRtImport.DoseVolume and reject other volumes, i.e., not display them in the list of combobox), it should use the concept of signal, slot, and connect. This understanding of mine is based on line 271 from the file qSlicerIsodoseModuleWidget.cxx.

Is my understanding correct?
Best regards.
Shahrokh

Connection of signals and slots are nor related to filtering. The issue in the code above is that DICOMRTIMPORT_DOSE_VOLUME_IDENTIFIER_ATTRIBUTE_NAME is not a string literal. It is a variable name.

Instead of using the variable name as input, you need to use the variable value.

Thank you so much for the guidance. I’m sorry, but I’ve encountered a new error again.

>>> import qt
>>> inputSelector0.addAttribute(qt.QString(["vtkMRMLScalarVolumeNode"]), qt.QString(["DICOMRTIMPORT_DOSE_VOLUME_IDENTIFIER_ATTRIBUTE_NAME"]), qt.QString(["DicomRtImport.DoseVolume"]))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: module 'qt' has no attribute 'QString'
>>> inputSelector0.addAttribute(qt.QStringListModel(["vtkMRMLScalarVolumeNode"]), qt.QStringListModel(["DICOMRTIMPORT_DOSE_VOLUME_IDENTIFIER_ATTRIBUTE_NAME"]), qt.QStringListModel(["DicomRtImport.DoseVolume"]))
Traceback (most recent call last):
  File "<console>", line 1, in <module>
ValueError: Could not find matching overload for given arguments:
(QStringListModel (QStringListModel at: 0x7660300), QStringListModel (QStringListModel at: 0x8a71630), QStringListModel (QStringListModel at: 0x91629f0))
 The following slots are available:
addAttribute(QString nodeType, QString attributeName, QVariant attributeValue) -> void
addAttribute(QString nodeType, QString attributeName) -> void

>>> 

Best regards.
Shahrokh

Thank you very much for the support and assistance you are providing. You have guided me very well, but I was not able to understand the issue.
line corrected:

...
inputSelector0 = slicer.qMRMLNodeComboBox()
inputSelector0.nodeTypes = ["vtkMRMLScalarVolumeNode"]
inputSelector0.addAttribute("vtkMRMLScalarVolumeNode", "DicomRtImport.DoseVolume")
...

However, as shown in the image below, the Combobox only contains nodes of type RTDose.

Once again, I sincerely appreciate your guidance and support.
Best regards.
Shahrokh