Operating system: Windows 10
Slicer version: 4.10.2
Hello Slicer Community,
I’m experiencing strange behavior when exporting RTStructs. Prior to Export, I have about 170 segments each representing a different biological structure as shown here:
I then export these segments to RTStruct using two methods both which show the same strange behavior:
(1) Right clicking on Study and selecting export
(2) programatically using the DicomRtImportExportPlugin module.
When I import the resulting RTStruct and image volume, I see that most or all all of the contours still exist as shown in the 3D window, but only 16 segments are found in the Subject hierarchy.
It seems that somehow segments from the above photo are being grouped into segments in the below photo. I would expect that in the second photo, I would still have all ~170 segments listed on the left.
I looked into the rtstruct file and there were indeed only 16 elements in the (3006, 0039) ROI Contour Sequence tag. Also, I opened the RTStruct in a different viewer, and all the contours are there but only grouped into 16 elements.
This all tells me that the export is not working correctly. Can anybody advise how to properly export all ~170 segments? I would need to do it using the DicomRtImportExportPlugin module.
A bit more information, I’m not sure if this is relevant but I am creating the image and segmentation nodes via the python interpreter. I’m wondering if the way that I’m defining the segments is impacting the export? Here is the code I’m using to define the segmentation nodes, segments, and perform the export:
def createSegNode2(segmentationNode,imageNode,contours,name):
# Create segmentation node where we will store segments
#segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
#segmentationNode.CreateDefaultDisplayNodes()
# set up contour objects
contoursPolyData = vtk.vtkPolyData()
contourPoints = vtk.vtkPoints()
contourLines = vtk.vtkCellArray()
contoursPolyData.SetLines(contourLines)
contoursPolyData.SetPoints(contourPoints)
contour_count=-1
for contour in contours:
contour_count=contour_count+1
with open(constants.SLICER_STATUS_LOC,"w") as file:
file.write("For %s working on contour %d of %d: %s" % (name,contour_count,len(contours),str(contour)))
startPointIndex = contourPoints.GetNumberOfPoints()
contourLine = vtk.vtkPolyLine()
linePointIds = contourLine.GetPointIds()
for point in contour["points_rc"]:
point_ras=RCS2RAS(point+[0],imageNode)
point_ras[2]=contour["z_pos_mm"]
linePointIds.InsertNextId(contourPoints.InsertNextPoint(point_ras))
linePointIds.InsertNextId(startPointIndex) # make the contour line closed
contourLines.InsertNextCell(contourLine)
segment = slicer.vtkSegment()
segment.SetName("_".join([name.split("_")[0]]+[name.split("_")[-1]]+name.split("_")[1:-1]))
#segment.SetColor(segmentColor)
segment.AddRepresentation('Planar contour', contoursPolyData)
segmentationNode.GetSegmentation().AddSegment(segment)
# define image and segmentation nodes
imageNode=slicer.util.loadVolume(header["series_folder"]+files[0],properties={"show":False},returnNode=True)[1]
segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
segmentationNode.CreateDefaultDisplayNodes()
segmentationNode.SetReferenceImageGeometryParameterFromVolumeNode(imageNode)
# contours is a dictionary with contour names as keys and contour points in RCS
# here we loop through each contour and create a segmentation node. For this example, contours has about 170 key/value pairs, each representing a different structure
for seg in contours.keys():
createSegNode2(segmentationNode,imageNode,contours[seg],seg)
# now place imageNode and segmentationNode under a new patient/study
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(),header["PatientID"])
studyItemID = shNode.CreateStudyItem(patientItemID,header["StudyInstanceUID"])
volumeImgShItemID = shNode.GetItemByDataNode(imageNode)
shNode.SetItemParent(volumeImgShItemID, studyItemID)
volumeSegShItemID = shNode.GetItemByDataNode(segmentationNode)
shNode.SetItemParent(volumeSegShItemID, studyItemID)
# create the dicom exporter object and the exportables
exporter = DicomRtImportExportPlugin.DicomRtImportExportPluginClass()
exportables = exporter.examineForExport(volumeSegShItemID)+exporter.examineForExport(volumeImgShItemID)
# perform the export
m=exporter.export(exportables)
@Sunderlandkyl I have a hunch that the export function has not been updated correctly after the implementation of the shared labelmaps, and the 170 segments are stored in 16 shared labelmaps that are not separated when doing the contour extraction during export.
Thanks all for taking a look. If there is a quick short-term fix that would also be valuable since I was supposed to send my RTStructs to a colleague over the weekend. If that’s not possible, I totally understand.
I understand this is a complicated problem and sincerely appreciate the help.
This was a plastimatch bug. I have pushed a fix. @rmd, can you please test the Preview version? You might need to wait for Sunday’s build to see the fix.
Also FYI, the structure names are stored in the ROIObservationLabel, which regrettably, has VR of “Short String.” That means you are limited to 16 characters. You may wish to rename your structures to something shorter!
It is shocking to see just how bad these first-generation DICOM RT data structures are (not just the general idea of using planar contours for representing segmentations but little details like this, too). @gcsharp do you see any uptake of second-generation radiation therapy IODs in current commercial systems?
Sadly no. There seem to be two obstacles. First, the current state of affairs is “just good enough” that things mostly work OK. Second, all the vendors need to support first-generation DICOM-RT anyway. It is hard to see a path forward.
I went and double checked this. There are two places the name gets written, the other one is ROI Name which does allow LO (Long string). So you might be ok with your naming convention.