From c975d1f39a6d9b5c34c152a3c337fecd08158488 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Mon, 11 Mar 2024 08:13:58 -0400 Subject: [PATCH] PET BIDS (https://github.com/rordenlab/dcm2niix/issues/802) --- BIDS/README.md | 47 +++++++++++++++++++++++-------------- console/nii_dicom.cpp | 37 +++++++++++++++++++++++++++++ console/nii_dicom.h | 6 ++--- console/nii_dicom_batch.cpp | 10 ++++---- 4 files changed, 75 insertions(+), 25 deletions(-) diff --git a/BIDS/README.md b/BIDS/README.md index c48d5a43..f6f0dbc6 100644 --- a/BIDS/README.md +++ b/BIDS/README.md @@ -78,7 +78,7 @@ These fields are present regardless of modality (e.g. MR, CT, PET). | Field | Unit | Comments | Defined By | |--------------------------|------|---------------------|------------| -| BodyPartExamined | | DICOM tag 0018,0015 | D | +| BodyPart | | DICOM tag 0018,0015 | B | | PatientPosition | | DICOM tag 0020,0032 | D | | ProcedureStepDescription | | DICOM tag 0040,0254 | D | | SoftwareVersions | | DICOM tag 0020,1020 | B | @@ -107,6 +107,9 @@ These fields contain personally identifiable information. By default dcm2niix wi | PatientID | | DICOM tag 0010,0020 | D | | AccessionNumber | | DICOM tag 0008,0050 | D | | PatientBirthDate | | DICOM tag 0010,0030 | D | +| PatientSex | | DICOM tag 0010,0040 | D | +| PatientAge | | DICOM tag 0010,1010 | D | +| PatientSize | | DICOM tag 0010,1020 | D | | PatientWeight | kg | DICOM tag 0010,1030 | D | | AcquisitionDateTime | | DICOM tag 0008,002A | D | @@ -181,23 +184,31 @@ PET fields extracted from [DICOM tags](http://dicom.nema.org/medical/dicom/curre The term ECAT in the comments suggests that values are defined by the [ECAT7](http://www.turkupetcentre.net/petanalysis/format_image_ecat.html) format. Therefore, these fields will not be populated for DICOM data. -| Field | Unit | Comments | Defined By | -|------------------------------|------|-----------------------------|------------| -| Radiopharmaceutical | | DICOM tag 0018,0031 or ECAT | D | -| RadionuclidePositronFraction | f | DICOM tag 0018,1076 | D | -| RadionuclideTotalDose | MBq | DICOM tag 0018,1074 | D | -| RadionuclideHalfLife | s | DICOM tag 0018,1075 | D | -| DoseCalibrationFactor | | DICOM tag 0054,1322 | D | -| IsotopeHalfLife | | ECAT | D | -| Dosage | | ECAT | D | -| ConvolutionKernel | | DICOM tag 0018,1210 | D | -| Units | | DICOM tag 0054,1001 | D | -| DecayCorrection | | DICOM tag 0054,1102 | D | -| AttenuationCorrectionMethod | | DICOM tag 0054,1101 | D | -| ReconstructionMethod | | DICOM tag 0054,1103 | D | -| DecayFactor | | DICOM tag 0054,1321 | D | -| FrameTimesStart | s | DICOM tags 0008,0022 | D | -| FrameDuration | s | DICOM tag 0018,1242 | D | +| Field | Unit | Comments | Defined By | +|------------------------------|------|----------------------------------|------------| +| IsotopeHalfLife | | ECAT | D | +| Dosage | | ECAT | D | +| FrameTimesStart | s | DICOM tag 0008,0022 | D | +| TracerRadionuclide | | DICOM tag 0008,0100 or 0008,0104 | B | +| Radiopharmaceutical | | DICOM tag 0018,0031 or ECAT | D | +| InjectedRadioactivity | MBq | DICOM tag 0018,1074 | B | +| RadionuclideHalfLife | s | DICOM tag 0018,1075 | D | +| RadionuclidePositronFraction | f | DICOM tag 0018,1076 | D | +| ConvolutionKernel | | DICOM tag 0018,1210 | D | +| Units | | DICOM tag 0054,1001 | B | +| AttenuationCorrectionMethod | | DICOM tag 0054,1101 | B | +| DecayCorrection | | DICOM tag 0054,1102 | D | +| ReconstructionMethod | | DICOM tag 0054,1103 | B | +| DecayCorrectionFactor | | DICOM tag 0054,1321 | B | +| DoseCalibrationFactor | | DICOM tag 0054,1322 | B | +| ScatterFraction | | DICOM tag 0054,1323 | B | +| FrameDuration | s | DICOM tag 0018,1242 | B | + +n.b. ConvolutionKernel (0018,1210) can be parsed to BIDS `ReconFilterType` and `ReconFilterSize`. However, manufacturer variations (Siemens: `XYZGAUSSIAN3.00` GE `Rad: \ rectangle \ 4.000000 mm \ Ax: \ rectangle \ 8.500000 mm` require complex logic). + +n.b. ReconstructionMethod (0054,1103) can be parsed to ReconMethodName, ReconMethodParameterLabels, ReconMethodParameterUnits and ReconMethodParameterValues. However, this requires maniufacturer specific logic (e.g. Siemens `OP-OSEM4i21s`, GE `3D Kinahan - Rogers` + +For Philips scanners, [Source Isotope Name](https://dicom.innolitics.com/ciods/rt-brachy-treatment-record/rt-brachy-session-record/30080100/300a0226) may be a good source for TracerRadionuclide ## Manufacturer Fields diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 361b3a26..8acc262a 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -786,6 +786,7 @@ struct TDICOMdata clear_dicom_data() { strcpy(d.convolutionKernel, ""); strcpy(d.parallelAcquisitionTechnique, ""); strcpy(d.imageOrientationText, ""); + strcpy(d.tracerRadionuclide, ""); strcpy(d.unitsPT, ""); strcpy(d.decayCorrection, ""); strcpy(d.attenuationCorrectionMethod, ""); @@ -833,6 +834,7 @@ struct TDICOMdata clear_dicom_data() { d.shimGradientZ = -33333;//impossible value for UINT16 strcpy(d.prescanReuseString, ""); d.decayFactor = 0.0; + d.scatterFraction = 0.0; d.percentSampling = 0.0; d.phaseFieldofView = 0.0; d.dwellTime = 0; @@ -936,6 +938,7 @@ struct TDICOMdata clear_dicom_data() { d.numberOfImagesInGridUIH = 0; d.phaseEncodingRC = '?'; d.patientSex = '?'; + d.patientSize = 0.0; d.patientWeight = 0.0; strcpy(d.patientBirthDate, ""); strcpy(d.patientAge, ""); @@ -4362,6 +4365,8 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kInstitutionName 0x0008 + (0x0080 << 16) #define kInstitutionAddress 0x0008 + (0x0081 << 16) #define kReferringPhysicianName 0x0008 + (0x0090 << 16) +#define kTracerRadionuclide1 0x0008 + (0x0100 << 16) //SH +#define kTracerRadionuclide2 0x0008 + (0x0104 << 16) //LO #define kStationName 0x0008 + (0x1010 << 16) #define kSeriesDescription 0x0008 + (0x103E << 16) // '0008' '103E' 'LO' 'SeriesDescription' #define kInstitutionalDepartmentName 0x0008 + (0x1040 << 16) @@ -4376,6 +4381,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kPatientBirthDate 0x0010 + (0x0030 << 16) #define kPatientSex 0x0010 + (0x0040 << 16) #define kPatientAge 0x0010 + (0x1010 << 16) +#define kPatientSize 0x0010 + (0x1020 << 16) //DS #define kPatientWeight 0x0010 + (0x1030 << 16) #define kAnatomicalOrientationType 0x0010 + (0x2210 << 16) #define kDeidentificationMethod 0x0012 + (0x0063 << 16) //[DICOMANON, issue 383 @@ -4581,6 +4587,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD #define kReconstructionMethod 0x0054 + (0x1103 << 16) //LO #define kFrameReferenceTime 0x0054 + (0x1300 << 16) //DS #define kDecayFactor 0x0054 + (0x1321 << 16) //DS +#define kScatterFraction 0x0054 + (0x1323 << 16) //DS //ftp://dicom.nema.org/MEDICAL/dicom/2014c/output/chtml/part03/sect_C.8.9.4.html //If ImageType is REPROJECTION we slice direction is reversed - need example to test // #define kSeriesType 0x0054+(0x1000 << 16 ) @@ -5656,9 +5663,35 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD case kPatientAge: dcmStr(lLength, &buffer[lPos], d.patientAge); break; + case kPatientSize: + d.patientSize = dcmStrFloat(lLength, &buffer[lPos]); + break; case kPatientWeight: d.patientWeight = dcmStrFloat(lLength, &buffer[lPos]); break; +/* n.b. check GeneralElectricAdvance-NIMH example dataset: need to speficy SQ + (0018,0031) LO [FDG -- fluorodeoxyglucose ] # 26,1 Radiopharmaceutical + (0018,1071) DS [0 ] # 2,1 Radiopharmaceutical Volume + (0018,1072) TM [092345.00 ] # 10,1 Radiopharmaceutical Start Time + (0018,1074) DS [ 75850000] # 14,1 Radionuclide Total Dose + (0018,1075) DS [6588] # 4,1 Radionuclide Half Life + (0018,1076) DS [0.97000002861023] # 16,1 Radionuclide Positron Fraction + (0054,0300) SQ (Sequence with undefined length) # u/l,1 Radionuclide Code Sequence + (fffe,e000) na (Item with defined length) + (0008,0100) SH [C-111A1 ] # 8,1 Code Value + (0008,0102) SH [99SDM ] # 6,1 Coding Scheme Designator + (0008,0104) LO [18F ] # 4,1 Code Meaning + (fffe,e0dd) + (0054,0304) SQ (Sequence with undefined length) # u/l,1 Radiopharmaceutical Code Sequence + (fffe,e000) na (Item with defined length) + (0008,0100) SH [Y-X1743 ] # 8,1 Code Value + (0008,0102) SH [99SDM ] # 6,1 Coding Scheme Designator + (0008,0104) LO [FDG -- fluorodeoxyglucose ] # 26,1 Code Meaning + (fffe,e0dd) + case kTracerRadionuclide1: + case kTracerRadionuclide2: + dcmStr(lLength, &buffer[lPos], d.tracerRadionuclide); + break;*/ case kStationName: dcmStr(lLength, &buffer[lPos], d.stationName); break; @@ -6645,6 +6678,10 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD case kDecayFactor: d.decayFactor = dcmStrFloat(lLength, &buffer[lPos]); break; + case kScatterFraction: + d.scatterFraction = dcmStrFloat(lLength, &buffer[lPos]); + printf("SF%g\n", d.scatterFraction); //for each slice? + break; case kIconImageSequence: if (lLength > 8) break; //issue638: we will skip entire icon if there is an explicit length diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 8008468a..ccc92240 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20240229" +#define kDCMdate "v1.0.20240308" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic @@ -241,13 +241,13 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; uint32_t coilCrc, seriesUidCrc, instanceUidCrc; int overlayStart[kMaxOverlay]; int postLabelDelay, shimGradientX, shimGradientY, shimGradientZ, phaseNumber, spoiling, mtState, partialFourierDirection, interp3D, aslFlags, durationLabelPulseGE, epiVersionGE, internalepiVersionGE, maxEchoNumGE, rawDataRunNumber, numberOfTR, numberOfImagesInGridUIH, numberOfDiffusionT2GE, numberOfDiffusionDirectionGE, tensorFileGE, diffCyclingModeGE, phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, frequencyEncodingSteps, phaseEncodingStepsOutOfPlane, echoTrainLength, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, locationsInAcquisitionConflict, compressionScheme; - float compressedSensingFactor, xRayTubeCurrent, exposureTimeMs, numberOfExcitations, numberOfArms, numberOfPointsPerArm, groupDelay, decayFactor, percentSampling,waterFatShift, numberOfAverages, patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, accelFactOOP, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4], velocityEncodeScaleGE; + float compressedSensingFactor, xRayTubeCurrent, exposureTimeMs, numberOfExcitations, numberOfArms, numberOfPointsPerArm, groupDelay, decayFactor, scatterFraction, percentSampling,waterFatShift, numberOfAverages, patientSize, patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, accelFactOOP, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4], velocityEncodeScaleGE; float orient[7], patientPosition[4], patientPositionLast[4], xyzMM[4], stackOffcentre[4]; float rtia_timerGE, radionuclidePositronFraction, radionuclideTotalDose, radionuclideHalfLife, doseCalibrationFactor; //PET ISOTOPE MODULE ATTRIBUTES (C.8-57) float frameReferenceTime, frameDuration, ecat_isotope_halflife, ecat_dosage; float pixelPaddingValue; // used for both FloatPixelPaddingValue (0028, 0122) and PixelPaddingValue (0028, 0120); NaN if not present. double imagingFrequency, acquisitionDuration, triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; - char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[kDICOMStr], decayCorrection[kDICOMStr], attenuationCorrectionMethod[kDICOMStr],reconstructionMethod[kDICOMStr], transferSyntax[kDICOMStr]; + char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[kDICOMStr], tracerRadionuclide[kDICOMStr], decayCorrection[kDICOMStr], attenuationCorrectionMethod[kDICOMStr],reconstructionMethod[kDICOMStr], transferSyntax[kDICOMStr]; char prescanReuseString[kDICOMStr], imageOrientationText[kDICOMStr], pulseSequenceName[kDICOMStr], coilElements[kDICOMStr], coilName[kDICOMStr], phaseEncodingDirectionDisplayedUIH[kDICOMStr], imageBaseName[kDICOMStr], stationName[kDICOMStr], softwareVersions[kDICOMStr], deviceSerialNumber[kDICOMStr], institutionName[kDICOMStr], referringPhysicianName[kDICOMStr], instanceUID[kDICOMStr], seriesInstanceUID[kDICOMStr], studyInstanceUID[kDICOMStr], bodyPartExamined[kDICOMStr], procedureStepDescription[kDICOMStr], imageTypeText[kDICOMStr], imageType[kDICOMStr], institutionalDepartmentName[kDICOMStr], manufacturersModelName[kDICOMStr], patientID[kDICOMStr], patientOrient[kDICOMStr], patientName[kDICOMStr], accessionNumber[kDICOMStr], seriesDescription[kDICOMStr], studyID[kDICOMStr], sequenceName[kDICOMStr], protocolName[kDICOMStr],sequenceVariant[kDICOMStr],scanningSequence[kDICOMStr], patientBirthDate[kDICOMStr], patientAge[kDICOMStr], studyDate[kDICOMStr],studyTime[kDICOMStr]; char deepLearningText[kDICOMStrLarge], scanOptions[kDICOMStrLarge], institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 11cba2d4..3ac8913b 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -1371,6 +1371,7 @@ tse3d: T2*/ } if (d.patientSex != '?') fprintf(fp, "\t\"PatientSex\": \"%c\",\n", d.patientSex); + json_Float(fp, "\t\"PatientSize\": %g,\n", d.patientSize); json_Float(fp, "\t\"PatientWeight\": %g,\n", d.patientWeight); //d.patientBirthDate //convert from DICOM YYYYMMDD to JSON //d.patientAge //4-digit Age String: nnnD, nnnW, nnnM, nnnY; @@ -1385,7 +1386,7 @@ tse3d: T2*/ } if (d.isQuadruped) json_Bool(fp, "\t\"Quadruped\": %s,\n", true); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP] - json_Str(fp, "\t\"BodyPartExamined\": \"%s\",\n", d.bodyPartExamined); + json_Str(fp, "\t\"BodyPart\": \"%s\",\n", d.bodyPartExamined); //renamed to match BIDS https://bids-specification.readthedocs.io/en/stable/modality-specific-files/positron-emission-tomography.html json_Str(fp, "\t\"PatientPosition\": \"%s\",\n", d.patientOrient); // 0018,5100 = PatientPosition in DICOM json_Str(fp, "\t\"ProcedureStepDescription\": \"%s\",\n", d.procedureStepDescription); json_Str(fp, "\t\"SoftwareVersions\": \"%s\",\n", d.softwareVersions); @@ -1497,19 +1498,20 @@ tse3d: T2*/ if ((d.isRealIsPhaseMapHz) && ((d.manufacturer == kMANUFACTURER_GE) || (d.isHasReal))) fprintf(fp, "\t\"Units\": \"Hz\",\n"); // //PET ISOTOPE MODULE ATTRIBUTES + json_Str(fp, "\t\"TracerRadionuclide\": \"%s\",\n", d.tracerRadionuclide); json_Str(fp, "\t\"Radiopharmaceutical\": \"%s\",\n", d.radiopharmaceutical); json_Float(fp, "\t\"RadionuclidePositronFraction\": %g,\n", d.radionuclidePositronFraction); - json_Float(fp, "\t\"RadionuclideTotalDose\": %g,\n", d.radionuclideTotalDose); + json_Float(fp, "\t\"InjectedRadioactivity\": %g,\n", d.radionuclideTotalDose); //renamed https://bids-specification.readthedocs.io/en/stable/glossary.html#objects.metadata.InjectedRadioactivity json_Float(fp, "\t\"RadionuclideHalfLife\": %g,\n", d.radionuclideHalfLife); json_Float(fp, "\t\"DoseCalibrationFactor\": %g,\n", d.doseCalibrationFactor); json_Float(fp, "\t\"IsotopeHalfLife\": %g,\n", d.ecat_isotope_halflife); json_Float(fp, "\t\"Dosage\": %g,\n", d.ecat_dosage); json_Str(fp, "\t\"ConvolutionKernel\": \"%s\",\n", d.convolutionKernel); json_Str(fp, "\t\"Units\": \"%s\",\n", d.unitsPT); //https://github.com/bids-standard/bids-specification/pull/773 - json_Str(fp, "\t\"DecayCorrection\": \"%s\",\n", d.decayCorrection); + json_Str(fp, "\t\"DecayCorrectionFactor\": \"%s\",\n", d.decayCorrection); //renamed https://bids-specification.readthedocs.io/en/stable/glossary.html#objects.metadata.DecayCorrectionFactor json_Str(fp, "\t\"AttenuationCorrectionMethod\": \"%s\",\n", d.attenuationCorrectionMethod); json_Str(fp, "\t\"ReconstructionMethod\": \"%s\",\n", d.reconstructionMethod); - //json_Float(fp, "\t\"DecayFactor\": %g,\n", d.decayFactor); + json_Float(fp, "\t\"ScatterFraction\": %g,\n", d.scatterFraction); if (dti4D->decayFactor[0] >= 0.0) { //see BEP009 PET https://docs.google.com/document/d/1mqMLnxVdLwZjDd4ZiWFqjEAmOmfcModA_R535v3eQs0 fprintf(fp, "\t\"DecayFactor\": [\n"); for (int i = 0; i < h->dim[4]; i++) {