Source code for cerr.dcm_export.rtstruct_iod

# Module to export structures to RTSTRUCT DICOM
#
# APA, 11/13/2023

from datetime import datetime
import numpy as np
from pydicom.uid import generate_uid
from pydicom.dataset import Dataset, FileDataset, FileMetaDataset
from pydicom.sequence import Sequence
from random import randint
from cerr.dcm_export import iod_helper
from cerr.dataclasses import scan as scn

org_root = '1.3.6.1.4.1.9590.100.1.2.'

[docs] def getDcmTagVals(structNumV, planC, seriesOpts = {}): assocScanNum = scn.getScanNumFromUID(planC.structure[structNumV[0]].assocScanUID,planC) pat_tags = {"PatientName": planC.scan[assocScanNum].scanInfo[0].patientName, "PatientID": planC.scan[assocScanNum].scanInfo[0].patientID, "PatientBirthDate": planC.scan[assocScanNum].scanInfo[0].patientBirthDate, "PatientSex": planC.scan[assocScanNum].scanInfo[0].patientSex, "PatientAge": "", #planC.scan[assocScanNum].scanInfo[0].patientAge "PatientSize": planC.scan[assocScanNum].scanInfo[0].patientSize, "PatientWeight": planC.scan[assocScanNum].scanInfo[0].patientWeight } study_tags = {"StudyDate": planC.scan[assocScanNum].scanInfo[0].studyDate, "StudyTime": planC.scan[assocScanNum].scanInfo[0].studyTime, "StudyDescription": planC.scan[assocScanNum].scanInfo[0].studyDescription, "StudyInstanceUID": planC.scan[assocScanNum].scanInfo[0].studyInstanceUID, "StudyID": "" #planC.scan[assocScanNum].scanInfo[0].studyID } if "seriesDescription" in seriesOpts: seriesDescription = seriesOpts['seriesDescription'] else: seriesDescription = "Generated by pyCERR" dt = datetime.now() if "SeriesDate" in seriesOpts: SeriesDate = seriesOpts['SeriesDate'] else: SeriesDate = dt.strftime("%Y%m%d") if "SeriesTime" in seriesOpts: SeriesTime = seriesOpts['SeriesTime'] else: SeriesTime = dt.strftime("%H%M%S.%f") series_tags = { 'Modality': 'RTSTRUCT', 'SeriesDate': SeriesDate, 'SeriesTime': SeriesTime, 'SeriesDescription': seriesDescription, 'SeriesInstanceUID': generate_uid(prefix=org_root), 'SeriesNumber': str(randint(9010, 9900)) } equiqmt_tags = {"Manufacturer": "", "ManufacturerModelName": "", "InstitutionName": "" } content_tags = {"ContentDescription": "", "ContentLabel": ""} struct_set_tags = {"InstanceNumber": "", "StructureSetDescription": "", "StructureSetLabel": "" } return pat_tags, study_tags, series_tags, equiqmt_tags, content_tags, struct_set_tags
[docs] def getRefFORSeq(structNumV, planC): # 3006,0010 Referenced Frame of Reference Sequence # 0020,0052 Frame of Reference UID # 3006,0012 RT Referenced Study Sequence # 0008,1150 Referenced SOP Class UID # 0008,1155 Referenced SOP Instance UID # 3006,0014 RT Referenced Series Sequence # 0020,000E Series Instance UID # 3006,0016 Contour Image Sequence # 0008,1150 Referenced SOP Class UID # 0008,1155 Referenced SOP Instance UID # 0008,1160 Referenced Frame Number (currently not implemented) refFORSeq = Sequence() assocScanNum = scn.getScanNumFromUID(planC.structure[structNumV[0]].assocScanUID,planC) dsRefFOR = Dataset() dsRefFOR.FrameOfReferenceUID = planC.scan[assocScanNum].scanInfo[0].frameOfReferenceUID dsRefFOR.RTReferencedStudySequence = Sequence() dsStudy = Dataset() dsStudy.ReferencedSOPClassUID = planC.scan[assocScanNum].scanInfo[0].sopClassUID # add to import dsStudy.ReferencedSOPInstanceUID = planC.scan[assocScanNum].scanInfo[0].studyInstanceUID dsStudy.RTReferencedSeriesSequence = Sequence() dsSeries = Dataset() dsSeries.SeriesInstanceUID = planC.scan[assocScanNum].scanInfo[0].seriesInstanceUID dsSeries.ContourImageSequence = Sequence() for sInfo in planC.scan[assocScanNum].scanInfo: dsContourImage = Dataset() dsContourImage.ReferencedSOPClassUID = sInfo.sopClassUID dsContourImage.ReferencedSOPInstanceUID = sInfo.sopInstanceUID dsSeries.ContourImageSequence.append(dsContourImage) dsStudy.RTReferencedSeriesSequence.append(dsSeries) dsRefFOR.RTReferencedStudySequence.append(dsStudy) refFORSeq.append(dsRefFOR) return refFORSeq
[docs] def getStructSetROISeq(structNumV, planC): # 3006,0020 Structure Set ROI Sequence # 3006,0022 ROI Number # 3006,0024 Referenced Frame of Reference UID # 3006,0026 ROI Name # 3006,0028 ROI Description # 3006,002C ROI Volume # 3006,0036 ROI Generation Algorithm # 3006,0038 ROI Generation Description assocScanNum = scn.getScanNumFromUID(planC.structure[structNumV[0]].assocScanUID,planC) strSetROISeq = Sequence() roiCount = 0 for iStr in structNumV: roiCount += 1 dsStrSet = Dataset() dsStrSet.ROINumber = roiCount dsStrSet.ReferencedFrameOfReferenceUID = planC.scan[assocScanNum].scanInfo[0].frameOfReferenceUID dsStrSet.ROIName = planC.structure[iStr].structureName dsStrSet.ROIDescription = "" dsStrSet.ROIGenerationAlgorithm = planC.structure[iStr].roiGenerationAlgorithm dsStrSet.ROIGenerationDescription = planC.structure[iStr].roiGenerationDescription strSetROISeq.append(dsStrSet) return strSetROISeq
[docs] def convertCerrToDcmCoords(pointsM,planC): return pointsM
[docs] def getROIContourSeq(structNumV, planC): # 3006, 0039 ROI Contour Sequence #3006,0084 Referenced ROI Number #3006,002A ROI Display Color #3006,0048 Contour Sequence # 3006,0048 Contour Number # 3006,0049 Attached Contours # 3006,0016 Contour Image Sequence # 0008,1150 Referenced SOP Class UID # 0008,1155 Referenced SOP Instance UID # 0008,1160 Referenced Frame Number # 3006,0042 Contour Geometric Type # 3006,0044 Contour Slab Thickness # 3006,0045 Contour Offset Vector # 3006,0046 Number of Contour Points # 3006,0050 Contour Data assocScanNum = scn.getScanNumFromUID(planC.structure[structNumV[0]].assocScanUID,planC) Image2VirtualPhysicalTransM = planC.scan[assocScanNum].Image2VirtualPhysicalTransM Image2PhysicalTransM = planC.scan[assocScanNum].Image2PhysicalTransM transM = np.matmul(Image2PhysicalTransM, np.linalg.inv(Image2VirtualPhysicalTransM)) transM[:,:3] = transM[:,:3] * 10 # cm to mm roiContourSeq = Sequence() roiCount = 0 for iStr in structNumV: roiCount += 1 dsROIContour = Dataset() dsROIContour.ReferencedROINumber = roiCount #planC.structure[iStr].roiNumber if len(planC.structure[0].structureColor) > 0: dsROIContour.ROIDisplayColor = planC.structure[iStr].structureColor contourSeq = Sequence() for slcNum in range(len(planC.scan[assocScanNum].scanInfo)): if not hasattr(planC.structure[iStr].contour[slcNum],'segments'): continue for seg in planC.structure[iStr].contour[slcNum].segments: contourImgSeq = Sequence() dsContour = Dataset() dsImg = Dataset() dsImg.ReferencedSOPClassUID = planC.scan[assocScanNum].scanInfo[slcNum].sopClassUID dsImg.ReferencedSOPInstanceUID = planC.scan[assocScanNum].scanInfo[slcNum].sopInstanceUID contourImgSeq.append(dsImg) dsImg.ContourImageSequence = contourImgSeq geom_type = 'CLOSED_PLANAR' if seg.points.shape[0] == 1: geom_type = 'POINT' dsContour.ContourGeometricType = geom_type dsContour.NumberOfContourPoints = seg.points.shape[0] #pointsArray = convertCerrToDcmCoords(seg.points,planC) tempPtsM = np.hstack((seg.points, np.ones((seg.points.shape[0], 1)))) #tempPtsM = np.matmul(np.linalg.inv(Image2VirtualPhysicalTransM), tempPtsM.T) #tempPtsM = np.matmult(Image2PhysicalTransM,tempPtsM) tempPtsM = np.matmul(transM, tempPtsM.T) dsContour.ContourData = tempPtsM[:3,:].flatten(order = "F").tolist() contourSeq.append(dsContour) dsROIContour.ContourSequence = contourSeq roiContourSeq.append(dsROIContour) return roiContourSeq
[docs] def getROIObservSeq(structNumV, planC): #3006,0080 RT Observation Sequence # 3006,0082 Observation Number # 3006,0084 Referenced ROI Number # 3006,0085 ROI Observation Label # 3006,0088 ROI Observation Description # 3006,0030 RT Related ROI Sequence # 30060084 Referenced ROI Number # 30060033 RT ROI Relationship # 3006,0086 RT ROI Identification Code Sequence # 3006,00A0 Related RT ROI Observations Sequence # 3006,0082 Observation Number # 3006,00A4 RT ROI Interpreted Type # 3006,00A6 ROI Interpreter # 300A,00E1 Material ID # 3006,00B0 ROI Physical Properties Sequence # 3006,00B2 ROI Physical Property # 3006 00B4 ROI Physical Property Value rtObsSeq = Sequence() roiCount = 0 for iStr in structNumV: roiCount += 1 dsRtObs = Dataset() dsRtObs.ObservationNumber = roiCount dsRtObs.ReferencedROINumber = roiCount dsRtObs.ROIObservationLabel = planC.structure[iStr].structureName dsRtObs.RTROIInterpretedType = planC.structure[iStr].ROIInterpretedType dsRtObs.ROIInterpreter = '' rtObsSeq.append(dsRtObs) return rtObsSeq
[docs] def create(structNumV, filePath, planC, seriesOpts = {}): # seriesDate = 'YYYYMMDD' e.g. '20240315' # seriesTime = 'HHMMSS.MS' e.g. '171354.838973' # seriesDescription = 'string describing RTSTRUCT series' # seriesOpts = {'seriesDescription': seriesDescription, # 'seriesDate': seriesDate, # 'seriesTime': seriesTime} # Make sure that all structures in structNumV are associated with the same sane # Get related UIDs for structNumV pat_tags, study_tags, series_tags, equiqmt_tags, content_tags, struct_set_tags = \ getDcmTagVals(structNumV, planC, seriesOpts) # Initialize RTSTRUCT series # ds = create_reg_dataset(base_series_data, filePath) file_meta = iod_helper.getFileMeta('RTSTRUCT') ds = FileDataset(filePath, {}, file_meta=file_meta, preamble=b"\0" * 128) ds = iod_helper.addSOPCommonTags(ds) # Add Patient tags ds = iod_helper.addPatientTags(ds, pat_tags) # Add Study tags ds = iod_helper.addStudyTags(ds, study_tags) # Add Series tags ds = iod_helper.addSeriesTags(ds, series_tags) # Add Equipment tags ds = iod_helper.addEquipmentTags(ds, equiqmt_tags) # Add Content tags ds = iod_helper.addContentTags(ds, content_tags) # Add Structure Set tags ds = iod_helper.addStructureSetTags(ds, struct_set_tags) ds.ReferencedFrameOfReferenceSequence = getRefFORSeq(structNumV, planC) ds.StructureSetROISequence = getStructSetROISeq(structNumV, planC) ds.ROIContourSequence = getROIContourSeq(structNumV, planC) ds.RTROIObservationsSequence = getROIObservSeq(structNumV, planC) print("Writing RTSTRUCT file ...", filePath) ds.save_as(filePath) print("File saved.")