I am writing our dynamic PET volumes into a Legacy PET Enhanced IOD.
When reading in this file after writing, pydicom does fine but my checking tools (Offis: dcm2xml and DVTk) show the following,
<element tag="0020,9171" vr="??" vm="1" len="46" name="Unknown Tag & Data" binary="hidden"></element>
<element tag="0020,9170" vr="??" vm="1" len="808" name="Unknown Tag & Data" binary="hidden"></element>
I'm not able to read these data on other imaging analysis platforms (e.g. Matlab, Others). Really stuck here, could use some suggestions.
Code is below.
def writeLegacyPETDICOM(inFilename,
FrameStartTime = None,
FrameDuration = None,
StudyDescription = None,
SeriesDescription = None,
SeriesNumber = None,
HalfLife = None,
RescaleSlope = False,
RescaleIntercept = False):
'''
Parameters
----------
inFilename : TYPE
DESCRIPTION.
FrameStartTime : TYPE, optional
DESCRIPTION. The default is None.
FrameDuration : TYPE, optional
DESCRIPTION. The default is None.
StudyDescription : TYPE, optional
DESCRIPTION. The default is None.
SeriesNumber : TYPE, optional
DESCRIPTION. The default is None.
HalfLife : TYPE, optional
DESCRIPTION. The default is None.
RescaleSlope : TYPE, optional
DESCRIPTION. The default is None.
RescaleIntercept : TYPE, optional
DESCRIPTION. The default is None.
Returns
-------
None.
'''
# gather header from first frame to assist with setting data
hdr = getVol(inFilename)[1]
suffix = '.dcm'
filename_little_endian = tempfile.NamedTemporaryFile(suffix = suffix).name
# filename_little_endian = os.path.join(os.getcwd(),str(uuid.uuid4().hex) + suffix)
# filename_big_endian = tempfile.NamedTemporaryFile(suffix = suffix).name
print("Setting file meta information...")
# populate required values
file_meta = Dataset()
# Patient Module
file_meta.FileMetaInformationGroupLength = 128 #?? is this the preamble size in bytes?
file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.128.1'
file_meta.MediaStorageSOPInstanceUID = pyd.uid.generate_uid()
file_meta.ImplementationClassUID = pyd.uid.generate_uid()
print("Setting dataset values...")
# Create the FileDataset instance (initially no data elements, but file_meta
# supplied)
ds = FileDataset(filename_little_endian, {},
file_meta=file_meta, preamble=b"\0" * 128)
# Set the transfer syntax
ds.file_meta.TransferSyntaxUID = pyd.uid.ImplicitVRLittleEndian
# ds.is_little_endian = True
# ds.is_implicit_VR = True
dt = datetime.datetime.now()
ds.SpecificCharacterSet = 'ISO_IR 100'
ds.ImageType = ['ORIGINAL', 'PRIMARY', 'DYNAMIC', 'NONE']
ds.SOPClassUID = '1.2.840.10008.5.1.4.1.1.128.1'
ds.SOPInstanceUID = pyd.uid.generate_uid()
ds.StudyDate = dt.strftime('%Y%m%d')
ds.SeriesDate = dt.strftime('%Y%m%d')
ds.ContentDate = dt.strftime('%Y%m%d')
ds.StudyTime = dt.strftime('%H%M%S')
ds.SeriesTime = dt.strftime('%H%M%S')
ds.ContentTime = dt.strftime('%H%M%S')
ds.Modality = 'PT'
ds.Manufacturer = 'Unknown'
ds.InstitutionName = ''
ds.InstitutionAddress = ''
ds.ReferringPhysicianName = ''
ds.StationName = ''
ds.StudyDescription = hdr['studyDescription'] if StudyDescription is None else StudyDescription #'N-13 Dynamic Stress XCAT Simulation'
ds.SeriesDescription = hdr['seriesDescription'] if SeriesDescription is None else SeriesDescription
ds.InstitutionalDepartmentName = ''
ds.ManufacturerModelsName = 'Unknown'
ds.PatientName = ''
ds.PatientID = ''
ds.PatientAge = '040Y'
ds.PatientSize = ''
ds.PatientWeight = '70'
ds.DeviceSerialNumber = 'Unknown'
ds.SoftwareVersions = 'Unknown'
ds.DateOfLastCalibration = ['20130418', '20130121']
ds.TimeOfLastCalibration = ['123624.875000', '172738.000000']
ds.ContentQualification = 'PRODUCT'
ds.CardiacSynchronizationTechnique = 'NONE'
ds.RespiratoryMotionCompensationTechnique = 'NONE'
ds.StudyInstanceUID = pyd.uid.generate_uid()
ds.SeriesInstanceUID = pyd.uid.generate_uid()
ds.StudyID = ''
ds.SeriesNumber = '501' if SeriesNumber is None else SeriesNumber
ds.InstanceNumber = '1'
ds.FrameOfReferenceUID = pyd.uid.generate_uid()
ds.ImageContents = ''
# dimension organization sequence
DimensionOrganizationSequenceUID = pyd.uid.generate_uid(prefix = None)
ds.DimensionOrganizationSequence = Sequence([Dataset()])
ds.DimensionOrganizationSequence[0].DimensionOrganizationUID = DimensionOrganizationSequenceUID
# end of sequence
# dimention index sequence
ds.DimensionIndexSequence = Sequence([Dataset(),Dataset(),Dataset()])
ds.DimensionIndexSequence[0].DimensionOrganizationUID = DimensionOrganizationSequenceUID
ds.DimensionIndexSequence[0].DimensionIndexPointer = Tag(0x020,0x9056)
ds.DimensionIndexSequence[0].FunctionalGroupPointer = Tag(0x020,0x9111)
ds.DimensionIndexSequence[1].DimensionOrganizationUID = DimensionOrganizationSequenceUID
ds.DimensionIndexSequence[1].DimensionIndexPointer = Tag(0x020,0x9057)
ds.DimensionIndexSequence[1].FunctionalGroupPointer = Tag(0x020,0x9111)
ds.DimensionIndexSequence[2].DimensionOrganizationUID = DimensionOrganizationSequenceUID
ds.DimensionIndexSequence[2].DimensionIndexPointer = Tag(0x020,0x9128)
ds.DimensionIndexSequence[2].FunctionalGroupPointer = Tag(0x020,0x9111)
# end of sequence
ds.SamplesPerPixel = 1
ds.PhotometricInterpretation = 'MONOCHROME2'
ds.NumberOfFrames = str(hdr['frames'] * hdr['dim'][2])
ds.Rows = int(hdr['dim'][0])
ds.Columns = int(hdr['dim'][1])
ds.BitsAllocated = 16
ds.BitsStored = 16
ds.HighBit = 15
ds.PixelRepresentation = 1
ds.AquisitionContextSequence = Sequence([Dataset()])
ds.PresentationLUTShape = 'IDENTITY'
# Shared Functional Groups Sequence
ds.SharedFunctionalGroupsSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].ReferencedImageSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].DerivationImageSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].PETFrameTypeSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].PETFrameTypeSequence[0].FrameType = ['ORIGINAL', 'PRIMARY', 'DYNAMIC', 'NONE']
ds.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence[0].ImageOrientationPatient = ['1', '0', '0', '0', '1', '0']
#Unassigned Shared Converted Arrtributes Sequence
ds.SharedFunctionalGroupsSequence[0].add_new([0x020,0x9170], 'SQ', Sequence([Dataset()]))
# ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].Manufacturer = 'XCAT 2.0'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].CorrectedImage = ['NORM', 'DTIM', 'ATTN', 'SCAT', 'DECY']
# Radiopharmaceutical Information Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartTime = dt.strftime('%H%M%S')
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideTotalDose = '481000000'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideHalfLife = str(hdr['halflife']) if HalfLife is None else HalfLife
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclidePositronFraction = '1.0'
# Radionuclide Code Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].CodeValue = 'C-107A1'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].CodingSchemeDesignator = 'SNM3'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].CodeMeaning = 'N^13^[^13^Nitrogen]'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].MappingResource = 'DCMR'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].ContextGroupVersion = '20020904000000.000000'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
RadiopharmaceuticalInformationSequence[0].RadionuclideCodeSequence[0].ContextIdentifier = '4020'
# Unassigned Shared Converted Arrtributes Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].NumberOfSlices = int(hdr['dim'][2])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].NumberOfTimeSlices = int(hdr['frames'])
# Patient Orientation Code Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].PatientOrientationCodeSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].CodeValue = 'F-10450'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].CodingSchemeDesignator = '99SDM'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].CodeMeaning = 'recumbent'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].MappingResource = 'DCMR'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].ContextGroupVersion = '20020904000000.000000'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].ContextIdentifier = '19'
# Patient Orientation Modifier Code Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].CodeValue = 'F-10340'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].CodingSchemeDesignator = '99SDM'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].CodeMeaning = 'supine'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].MappingResource = 'DCMR'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].ContextGroupVersion = '20020904000000.000000'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientOrientationCodeSequence[0].PatientOrientationModifierCodeSequence[0].ContextIdentifier = '20'
# Patient Gantry Relationship Code Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].CodeValue = 'F-10470'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].CodingSchemeDesignator = '99SDM'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].CodeMeaning = 'headfirst'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].MappingResource = 'DCMR'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].ContextGroupVersion = '20020904000000.000000'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].\
PatientGantryRelationshipCodeSequence[0].ContextIdentifier = '21'
# Unassigned Shared Converted Arrtributes Sequence
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].SeriesType = ['DYNAMIC', 'IMAGE']
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].Units = 'BQML'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].RandomsCorrectionMethod = 'DLYD'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].AttenuationCorrectionMethod = 'measured,AC_CT_STRESS'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].DecayCorrection = 'START'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].ReconstructionMethod = 'OSEM3D 3i16s'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].ScatterCorrectionMethod = 'Model-based'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].AxialAcceptance = '38'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].AxialMash = ['5', '6']
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].DecayFactor = '1.0'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].DoseCalibrationFactor = '1.0'
ds.SharedFunctionalGroupsSequence[0].UnassignedSharedConvertedAttributesSequence[0].ScatterFractionFactor = '0.45'
# Pixel Measures Sequence
ds.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence = Sequence([Dataset()])
ds.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence[0].SliceThickness = str(hdr['pixdim'][2])
ds.SharedFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing = [str(hdr['pixdim'][0]), str(hdr['pixdim'][1])]
ds.SharedFunctionalGroupsSequence[0].FrameVOILUTSequence = Sequence([Dataset()])
# Create Pixel Array and Sequences for PerFrameFunctionGroupSequence
Rows, Columns, Slices, Temporal = hdr['dim']
pixels = np.zeros((Slices*hdr['frames'], Rows, Columns))
# Per Frame Functional Groups Sequence
ds.PerFrameFunctionalGroupsSequence = Sequence(None)
mx = 0
if RescaleSlope:
# get the global maximum
for gg in range(hdr['frames']):
vol = getVol(inFilename, frame = gg)[0]
if vol.max() > mx: mx = vol.max()
scl_slope = mx / 32700
else:
scl_slope = 1.0
TotSliceNum = 0
for frameNum in range(hdr['frames']):
# get volume and put into pixels array
vol,hdr = getVol(inFilename, frame = frameNum, calibrate = False)
start = frameNum * Slices
end = start + Slices
# flip data in z-direction
volflipz = np.zeros(vol.shape)
for jj in range(vol.shape[2]):
volflipz[:,:,jj] = vol[:,:,-jj]
pixels[start:end,:,:] = np.moveaxis(volflipz,2,0) / scl_slope
for sliceNum in range(Slices):
dsTmp = Dataset()
dsTmp.FrameContentSequence = Sequence([Dataset()])
dsTmp.FrameContentSequence[0].StackID = '1'
dsTmp.FrameContentSequence[0].InStackPositionNumber = sliceNum + 1 # slice number 0 to 50, int
dsTmp.FrameContentSequence[0].TemporalPositionIndex = frameNum + 1 # frame number
dsTmp.FrameContentSequence[0].DimensionIndexValues = [1, sliceNum + 1, frameNum + 1] # [StackID, InStackPosition,TemporalPosition]
dsTmp.PlanePositionSequence = Sequence([Dataset()])
dsTmp.PlanePositionSequence[0].ImagePositionPatient = ['-156.08316400801', '-293.41428002869', str(-float(hdr['pixdim'][2]) * sliceNum - 100.5)] # last item needs to be index by slice thcikness
dsTmp.UnassignedPerFrameConvertedAttributesSequence = Sequence([Dataset()])
dsTmp.UnassignedPerFrameConvertedAttributesSequence[0].AcquisitionTime = ''
dsTmp.UnassignedPerFrameConvertedAttributesSequence[0].ActualFrameDuration = str(int(1000 * hdr['duration'])) if FrameDuration is None else str(int(FrameDuration[frameNum] * 1000)) # in milliseconds
dsTmp.UnassignedPerFrameConvertedAttributesSequence[0].FrameReferenceTime = str(1000 * hdr['duration']) if FrameStartTime is None else str(FrameStartTime[frameNum] * 1000) # start time in milliseconds ?
dsTmp.UnassignedPerFrameConvertedAttributesSequence[0].ImageIndex = 1 # 1 to total number of slices for all temporal frames combined
dsTmp.PixelValueTransformationSequence = Sequence([Dataset()])
dsTmp.PixelValueTransformationSequence[0].RescaleIntercept = str(hdr['scl_inter']) #if not RescaleIntercept else str(RescaleIntercept)
dsTmp.PixelValueTransformationSequence[0].RescaleSlope = str(hdr['scl_slope']) if not RescaleSlope else str(scl_slope)
dsTmp.PixelValueTransformationSequence[0].RescaleType = 'US'
# add to Per-Frame Functional Groups Sequence
ds.PerFrameFunctionalGroupsSequence.append(dsTmp)
# set pixel data and save
print('Rescaling images by: {}'.format(scl_slope))
ds.PixelData = pixels.astype(np.int16).tobytes()
ds.save_as(filename_little_endian, write_like_original = True)
# copy file to inFilename Directory
drive, path = os.path.splitdrive(inFilename)
path, name = os.path.split(path)
outFilename = os.path.join(drive, path,filename_little_endian.split(os.path.sep)[-1])
shutil.move(filename_little_endian,outFilename)