Last active
April 8, 2022 22:24
-
-
Save mikebind/b3e01fdc57d7d3d0cef72cb43cb59a75 to your computer and use it in GitHub Desktop.
Excerpt of 3D Slicer module logic code to essentially export the "Red" slice view image to a single slice DICOM file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SomeModuleLogic(ScriptedLoadableModuleLogic): | |
def exportRedSliceAsDicom(self, patientNameSH, xRangeMm=150, yRangeMm=150, zRangeMm=0.5, outputFolderPath=None, dicomTagDict=None): | |
""" This function creates an ROI centered on the center of the Red slice view, and | |
with axes aligned with that slice view's slice, and with thickness specified by zRangeMm. | |
This ROI is then used to crop the red slice's current background image volume to slices | |
in this orientation (I choose the zRange such that this results in a single slice by | |
setting it to close to the minimum voxel side length), and export the result to DICOM. | |
In order to export to DICOM, a patient and study need to be created in the subject hierarchy, | |
these are done the subject name given in patientNameSH and with study name "SingleSliceExport". | |
The DICOM file(s) are exported to the supplied outputFolderPath. DICOM tags in the export | |
can be suppled via a dictionary with tag names as keys and tag values as values (this has not | |
been extensively tested, but should probably work and would be a way to force things like UIDs | |
to desired vaues). If no tag dictionary is supplied, then by default the DICOM tags "PatientName" | |
and "StudyDescription" are set to patientNameSH just as a simple and minimal way to keep track. | |
""" | |
redSliceNode = slicer.mrmlScene.GetNodeByID('vtkMRMLSliceNodeRed') | |
sliceTransformMatrix = redSliceNode.GetSliceToRAS() | |
roi = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLMarkupsROINode', 'SliceExportROI') | |
roiTransformMatrix = vtk.vtkMatrix4x4() | |
roiTransformMatrix.DeepCopy(sliceTransformMatrix) | |
# The DICOM viewer I was using ended up showing most of the slices I was extracting as | |
# flipped LR compared to the view in Slicer (though some were flipped on both axes) | |
# There are lots of reasons this could happen, and this is not proof of a bug in either | |
# Slicer or the viewer (the angle from which an image is viewed is not fully | |
# prescribed in the DICOM header, only it's location in space). I just wanted a quick | |
# hack fix which resulted in images which ended up oriented the same. Through trial | |
# and error, I found that inverting the second axis of the ROI did this for my image | |
# orientation. Your mileage may vary. IMPORTNANT NOTE: Inverting a single axis is NOT appropriate | |
# if you are exporting more than one slice!! Changing a single axis direction in 3D space | |
# is equivalent to a mirror reflection, and the "handedness" of any non-mirror symmetric | |
# 3D structure will be inverted compared to reality. It's OK for a single slice because | |
# there can't be any "handedness" to a 2D structure (a reflection is equivalent to looking at | |
# the same plane from the other side). | |
for row in range(4): | |
for col in [1]: # invert second axis | |
roiTransformMatrix.SetElement(row, col, -1 * roiTransformMatrix.GetElement(row,col)) | |
# Position and orient ROI | |
roi.SetAndObserveObjectToNodeMatrix(roiTransformMatrix) | |
# Size ROI | |
roi.SetSize([xRangeMm, yRangeMm, zRangeMm]) | |
# Get Volume Node to crop (background volume of red slice) | |
sliceLogic = slicer.app.applicationLogic().GetSliceLogic(redSliceNode) | |
compositeNode = sliceLogic.GetSliceCompositeNode() | |
volNodeID = compositeNode.GetBackgroundVolumeID() | |
volNode = slicer.mrmlScene.GetNodeByID(volNodeID) | |
# Crop using ROI | |
croppedNode = self.cropVolumeUsingROI(volNode, roi, interpolation_mode='linear', isotropic=True) | |
# Export to DICOM | |
patientItemID, studyItemID = self.createSubjectHierarchyPatientAndStudy(patientNameSH,'SingleSliceExport') | |
self.putImageVolumeUnderStudy(croppedNode, studyItemID) | |
if dicomTagDict is None: | |
dicomTagDict = {"PatientName": patientNameSH, "StudyDescription": patientNameSH} | |
# "StudyID" seems to be automatically overwritten to "SLICER10001" for some reason | |
self.exportImageVolumeToDICOM(croppedNode, os.path.join(outputFolderPath, patientNameSH), tagDictionary=dicomTagDict) | |
# Clean up | |
slicer.mrmlScene.RemoveNode(roi) | |
return | |
def exportImageVolumeToDICOM(self, volNode, outputDirectory=None, tagDictionary={"PatientID":"anon", "StudyID":"testStudy"}): | |
""" Export a supplied scalar volume node to DICOM, setting DICOM tags using a dictionary, | |
and saving to a supplied outputDirectory. | |
""" | |
if outputDirectory is None: | |
# Ask user where to output (TODO) | |
slicer.util.errorDisplay('No output directory specified') | |
return | |
import DICOMScalarVolumePlugin | |
exporter = DICOMScalarVolumePlugin.DICOMScalarVolumePluginClass() | |
# Get image volume's subject hierarchy ID | |
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) | |
volumeShItemID = shNode.GetItemByDataNode(volNode) | |
# Break the volume into individual exportable slices (?) | |
exportables = exporter.examineForExport(volumeShItemID) | |
# For each slice, set the needed tags and the output directory | |
for exp in exportables: | |
# Set output folder | |
exp.directory = outputDirectory | |
for tagName, tagValue in tagDictionary.items(): | |
exp.setTag(tagName, tagValue) | |
# Do the export | |
exporter.export(exportables) | |
def putImageVolumeUnderStudy(self, volNode, studySubjectHierarchyID): | |
""" Place an image volume under a dicom study in the slicer subject hierarchy | |
""" | |
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) | |
volumeSubjectHierarchyID = shNode.GetItemByDataNode(volNode) | |
shNode.SetItemParent(volumeSubjectHierarchyID, studySubjectHierarchyID) | |
def createSubjectHierarchyPatientAndStudy(self, subjectNameInSH, studyNameInSH): | |
""" | |
""" | |
shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene) | |
# set IDs. Note: these IDs are not specifying DICOM tags, but only the names that appear in the hierarchy tree | |
patientItemID = shNode.CreateSubjectItem(shNode.GetSceneItemID(), subjectNameInSH) | |
studyItemID = shNode.CreateStudyItem(patientItemID, studyNameInSH) | |
return patientItemID, studyItemID | |
def cropVolumeUsingROI(self, input_volume_node, roi_node, output_volume_node=None, | |
interpolation_mode='linear', fill_value=0, isotropic=True): | |
""" Run Crop Volume using input ROI | |
""" | |
cropVolumeNode = slicer.vtkMRMLCropVolumeParametersNode() | |
cropVolumeNode.SetScene(slicer.mrmlScene) | |
cropVolumeNode.SetName("MyCropVolumeParametersNode") | |
cropVolumeNode.SetIsotropicResampling(isotropic) | |
if interpolation_mode=='linear': | |
interp_mode = cropVolumeNode.InterpolationLinear | |
elif interpolation_mode=='nn': | |
interp_mode = cropVolumeNode.InterpolationNearestNeighbor | |
cropVolumeNode.SetInterpolationMode(interp_mode) | |
cropVolumeNode.SetFillValue(fill_value) | |
cropVolumeNode.SetROINodeID(roi_node.GetID()) # roi | |
slicer.mrmlScene.AddNode(cropVolumeNode) | |
if output_volume_node is None: | |
output_volume_node = slicer.mrmlScene.AddNewNodeByClass('vtkMRMLScalarVolumeNode', input_volume_node.GetName() + '_roiCropped') | |
cropVolumeNode.SetInputVolumeNodeID(input_volume_node.GetID()) # input | |
cropVolumeNode.SetOutputVolumeNodeID(output_volume_node.GetID()) # output | |
slicer.modules.cropvolume.logic().Apply(cropVolumeNode) # do the crop | |
slicer.mrmlScene.RemoveNode(cropVolumeNode) | |
return output_volume_node |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment