From ece7add2b7be70f06729e9475199399d3bb56ff3 Mon Sep 17 00:00:00 2001 From: Ningfei Li Date: Thu, 27 Apr 2023 21:26:03 +0200 Subject: [PATCH 01/49] Remove unused CMake args. --- SuperBuild/External-CLOUDFLARE-ZLIB.cmake | 1 - SuperBuild/External-OPENJPEG.cmake | 1 - 2 files changed, 2 deletions(-) diff --git a/SuperBuild/External-CLOUDFLARE-ZLIB.cmake b/SuperBuild/External-CLOUDFLARE-ZLIB.cmake index 4c6a6acb..74168530 100644 --- a/SuperBuild/External-CLOUDFLARE-ZLIB.cmake +++ b/SuperBuild/External-CLOUDFLARE-ZLIB.cmake @@ -11,7 +11,6 @@ ExternalProject_Add(zlib ${OSX_ARCHITECTURES} # Compiler settings -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} - -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${DEP_INSTALL_DIR} ) diff --git a/SuperBuild/External-OPENJPEG.cmake b/SuperBuild/External-OPENJPEG.cmake index 1d99ac28..a311fa0b 100644 --- a/SuperBuild/External-OPENJPEG.cmake +++ b/SuperBuild/External-OPENJPEG.cmake @@ -12,7 +12,6 @@ ExternalProject_Add(openjpeg ${OSX_ARCHITECTURES} # Compiler settings -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} - # Not used -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} # Install directories -DCMAKE_INSTALL_PREFIX:PATH=${DEP_INSTALL_DIR} ) From 591661dc8c5df59f25649570cbf1bcd22add43de Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 9 May 2023 08:16:28 -0400 Subject: [PATCH 02/49] Multiple segments (https://github.com/rordenlab/dcm2niix/issues/706) --- console/nii_dicom.cpp | 23 +++++++++++++++++++++-- console/nii_dicom.h | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index a14ea39c..84fcd5c5 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4529,6 +4529,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD // #define kSeriesType 0x0054+(0x1000 << 16 ) #define kDoseCalibrationFactor 0x0054 + (0x1322 << 16) #define kPETImageIndex 0x0054 + (0x1330 << 16) +#define kReferencedSegmentNumber 0x0062 + (0x000B << 16) //US #define kPEDirectionDisplayedUIH 0x0065 + (0x1005 << 16) //SH #define kDiffusion_bValueUIH 0x0065 + (0x1009 << 16) //FD #define kParallelInformationUIH 0x0065 + (0x100D << 16) //SH @@ -4645,6 +4646,8 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD int temporalPositionIndex = 0; int minTemporalPositionIndex = 65535; int maxTemporalPositionIndex = 0; + int minReferencedSegmentNumber = 65535; + int maxReferencedSegmentNumber = 0; //int temporalPositionIdentifier = 0; int locationsInAcquisitionPhilips = 0; int imagesInAcquisition = 0; @@ -4819,7 +4822,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD if (sqDepth < 0) sqDepth = 0; //should not happen, but protect for faulty anonymization //if we leave the folder MREchoSequence 0018,9114 - if ((nDimIndxVal > 0) && ((d.manufacturer == kMANUFACTURER_CANON) || (d.manufacturer == kMANUFACTURER_BRUKER) || (d.manufacturer == kMANUFACTURER_PHILIPS)) && (sqDepth00189114 >= sqDepth)) { + if ((nDimIndxVal > 0) && ((d.manufacturer == kMANUFACTURER_UNKNOWN) || (d.manufacturer == kMANUFACTURER_CANON) || (d.manufacturer == kMANUFACTURER_BRUKER) || (d.manufacturer == kMANUFACTURER_PHILIPS)) && (sqDepth00189114 >= sqDepth)) { sqDepth00189114 = -1; //triggered //printf("slice %d---> 0020,9157 = %d %d %d\n", inStackPositionNumber, d.dimensionIndexValues[0], d.dimensionIndexValues[1], d.dimensionIndexValues[2]); // d.aslFlags = kASL_FLAG_PHILIPS_LABEL; kASL_FLAG_PHILIPS_LABEL @@ -5590,6 +5593,8 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD d.isXA10A = true; if ((slen > 4) && (strstr(d.softwareVersions, "XA30") != NULL)) d.isXA10A = true; + if ((slen > 4) && (strstr(d.softwareVersions, "XA31") != NULL)) + d.isXA10A = true; if ((slen < 5) || (strstr(d.softwareVersions, "XA10") == NULL)) break; d.isXA10A = true; @@ -5989,6 +5994,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD minTemporalPositionIndex = temporalPositionIndex; break; case kDimensionIndexPointer: + if (dimensionIndexPointerCounter >= MAX_NUMBER_OF_DIMENSIONS) break; dimensionIndexPointer[dimensionIndexPointerCounter++] = dcmAttributeTag(&buffer[lPos], d.isLittleEndian); break; case kFrameContentSequence: @@ -6189,6 +6195,14 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD case kPETImageIndex: PETImageIndex = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); break; + case kReferencedSegmentNumber: { //US issue706 + int segn = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + if (segn < minReferencedSegmentNumber) + minReferencedSegmentNumber = segn; + if (segn > maxReferencedSegmentNumber) + maxReferencedSegmentNumber = segn; + break; + } case kPEDirectionDisplayedUIH: if (d.manufacturer != kMANUFACTURER_UIH) break; @@ -7553,6 +7567,11 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD d.xyzDim[3] = d.xyzDim[3] / numberOfDynamicScans; d.xyzDim[4] = numberOfDynamicScans; } + int nSegment = maxReferencedSegmentNumber - minReferencedSegmentNumber + 1; + if ((nSegment > 1) && (d.xyzDim[4] < 2) && (d.xyzDim[3] > 1) && ((d.xyzDim[3] % nSegment) == 0)) { //issue706 + d.xyzDim[3] = d.xyzDim[3] / nSegment; + d.xyzDim[4] = nSegment; + } if ((maxInStackPositionNumber > 1) && (d.xyzDim[4] < 2) && (d.xyzDim[3] > 1) && ((d.xyzDim[3] % maxInStackPositionNumber) == 0)) { d.xyzDim[4] = d.xyzDim[3] / maxInStackPositionNumber; d.xyzDim[3] = maxInStackPositionNumber; @@ -7664,7 +7683,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD int stackPositionItem = 0; if (dimensionIndexPointerCounter > 0) for (size_t i = 0; i < dimensionIndexPointerCounter; i++) - if (dimensionIndexPointer[i] == kInStackPositionNumber) + if ((dimensionIndexPointer[i] == kInStackPositionNumber) || (dimensionIndexPointer[i] == kImagePositionPatient)) //issue706 stackPositionItem = i; if ((d.manufacturer == kMANUFACTURER_CANON) && (nVariableItems == 1) && (d.xyzDim[4] > 1)) { //WARNING: Canon CANON V6.0SP2001* (0018,9005) = "AX fMRI" strangely sets TemporalPositionIndex(0020,9128) as 1 for all volumes: (0020,9157) and (0020,9128) are INCORRECT! diff --git a/console/nii_dicom.h b/console/nii_dicom.h index aa86ef41..8788e9d8 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.20230411" +#define kDCMdate "v1.0.20230509" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic From d0fc89758d6fb634f36695ef03066c6e0f1a5b1b Mon Sep 17 00:00:00 2001 From: Qianqian Fang Date: Wed, 10 May 2023 14:11:29 -0400 Subject: [PATCH 03/49] Replace JNIfTI by JNIFTI in makefile macros --- console/CMakeLists.txt | 4 ++-- console/main_console.cpp | 4 ++-- console/makefile | 2 +- console/nii_dicom_batch.cpp | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt index aa52bd7c..79fcd38b 100644 --- a/console/CMakeLists.txt +++ b/console/CMakeLists.txt @@ -91,9 +91,9 @@ set(DCM2NIIX_SRCS nii_dicom_batch.cpp) -option(USE_JNIfTI "Build with JNIfTI support" ON) +option(USE_JNIFTI "Build with JNIfTI support" ON) if(USE_JNIFTI) - add_definitions(-DmyEnableJNIfTI) + add_definitions(-DmyEnableJNIFTI) set(DCM2NIIX_SRCS ${DCM2NIIX_SRCS} cJSON.cpp base64.cpp) endif() diff --git a/console/main_console.cpp b/console/main_console.cpp index 539fd276..4e54a939 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -80,7 +80,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { printf(" -ba : anonymize BIDS (y/n, default %c)\n", bool2Char(opts.isAnonymizeBIDS)); printf(" -c : comment stored in NIfTI aux_file (up to 24 characters e.g. '-c VIP', empty to anonymize e.g. 0020,4000 e.g. '-c \"\"')\n"); printf(" -d : directory search depth. Convert DICOMs in sub-folders of in_folder? (0..9, default %d)\n", opts.dirSearchDepth); -#ifdef myEnableJNIfTI +#ifdef myEnableJNIFTI printf(" -e : export as NRRD (y) or MGH (o) or JSON/JNIfTI (j) or BJNIfTI (b) instead of NIfTI (y/n/o/j/b, default n)\n"); #else printf(" -e : export as NRRD (y) or MGH (o) instead of NIfTI (y/n/o/j/b, default n)\n"); @@ -370,7 +370,7 @@ int main(int argc, const char *argv[]) { opts.saveFormat = kSaveFormatJNII; if ((argv[i][0] == 'b') || (argv[i][0] == 'B') || (argv[i][0] == '4')) opts.saveFormat = kSaveFormatBNII; - #ifndef myEnableJNIfTI + #ifndef myEnableJNIFTI if ((opts.saveFormat == kSaveFormatJNII) || (opts.saveFormat == kSaveFormatBNII)) { printf("Recompile for JNIfTI support.\n"); return EXIT_SUCCESS; diff --git a/console/makefile b/console/makefile index 1f35e23c..33131075 100644 --- a/console/makefile +++ b/console/makefile @@ -25,7 +25,7 @@ endif #run "JNIfTI=0 make" to disable JNIFTI build JSFLAGS= ifneq "$(JNIfTI)" "0" - JSFLAGS=-DmyEnableJNIfTI base64.cpp cJSON.cpp + JSFLAGS=-DmyEnableJNIFTI base64.cpp cJSON.cpp endif ifneq ($(OS),Windows_NT) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index f913f2ff..21e8387b 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -38,7 +38,7 @@ #endif #include "nii_dicom.h" #include "nii_ortho.h" -#ifdef myEnableJNIfTI +#ifdef myEnableJNIFTI #include "base64.h" #include "cJSON.h" #endif @@ -3583,7 +3583,7 @@ void nii_createDummyFilename(char *niiFilename, struct TDCMopts opts) { strcat(niiFilename, ".nhdr'"); else strcat(niiFilename, ".nrrd'"); - #ifdef myEnableJNIfTI + #ifdef myEnableJNIFTI } else if (opts.saveFormat == kSaveFormatJNII) { strcat(niiFilename, ".jnii'"); } else if (opts.saveFormat == kSaveFormatBNII) { @@ -4390,7 +4390,7 @@ int nii_saveNRRD(char *niiFilename, struct nifti_1_header hdr, unsigned char *im enum TZipMethod {zmZlib, zmGzip, zmBase64}; -#ifdef myEnableJNIfTI +#ifdef myEnableJNIFTI #ifdef Z_DEFLATED int zmat_run(const size_t inputsize, unsigned char *inputstr, size_t *outputsize, unsigned char **outputbuf, const int zipid, int *ret, const int iscompress){ @@ -4956,12 +4956,12 @@ int nii_savejnii(char *niiFilename, struct nifti_1_header hdr, unsigned char *im cJSON_Delete(root); return EXIT_SUCCESS; } // nii_savejnii() -#endif //#ifdef myEnableJNIfTI +#endif //#ifdef myEnableJNIFTI int nii_saveForeign(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d, struct TDTI4D *dti4D, int numDTI) { if (opts.saveFormat == kSaveFormatMGH) return nii_saveMGH(niiFilename, hdr, im, opts, d, dti4D, numDTI); - #ifdef myEnableJNIfTI + #ifdef myEnableJNIFTI else if (opts.saveFormat == kSaveFormatJNII || opts.saveFormat == kSaveFormatBNII) return nii_savejnii(niiFilename, hdr, im, opts, d, dti4D, numDTI); #endif From c8277760e165a9abbf0fde3259da209cbd2817d2 Mon Sep 17 00:00:00 2001 From: Ningfei Li Date: Wed, 31 May 2023 23:17:54 +0200 Subject: [PATCH 04/49] CI: Fix artifact file name on mac. --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index a1aa35b6..e0a05a46 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -78,8 +78,8 @@ for: - mkdir -p build/bin - lipo -create -output build/bin/dcm2niix intel/bin/dcm2niix apple/bin/dcm2niix - strip -Sx build/bin/* - - 7z a dcm2niix_macos.zip ./build/bin/* &>/dev/null - - appveyor PushArtifact dcm2niix_macos.zip + - 7z a dcm2niix_mac.zip ./build/bin/* &>/dev/null + - appveyor PushArtifact dcm2niix_mac.zip deploy: - provider: GitHub From 3f60989ed614541ad0a75d1a18546d35bb8c1755 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Sat, 10 Jun 2023 07:26:02 -0400 Subject: [PATCH 05/49] Echo Train Length (https://github.com/rordenlab/dcm2niix/issues/717) --- console/nii_dicom.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 84fcd5c5..35311a45 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -5735,9 +5735,13 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD case kMRAcquisitionPhaseEncodingStepsOutOfPlane: d.phaseEncodingStepsOutOfPlane = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); break; - case kGradientEchoTrainLength: - d.echoTrainLength = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + case kGradientEchoTrainLength: { + int etl = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); + if (etl < 2) //issue 717 + break; + d.echoTrainLength = etl; break; + } case kNumberOfImagesInMosaic: if (d.manufacturer == kMANUFACTURER_SIEMENS) numberOfImagesInMosaic = dcmInt(lLength, &buffer[lPos], d.isLittleEndian); From 570ee85b4cd5d3a740730d49af8d07f4058d474d Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Sat, 10 Jun 2023 16:16:08 -0400 Subject: [PATCH 06/49] Ignore icons with compressed transfer syntax (https://github.com/rordenlab/dcm2niix/issues/713) --- console/nii_dicom.cpp | 13 ++++++++++--- console/nii_dicom.h | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 35311a45..07764988 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4975,14 +4975,15 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD nDimIndxVal = -1; //we need DimensionIndexValues } //record dimensionIndexValues slice information } //groupElement == kItemDelimitationTag : delimit item exits folder + uint32_t itemTagLength = 0; if (groupElement == kItemTag) { - uint32_t slen = dcmInt(4, &buffer[lPos], d.isLittleEndian); + itemTagLength = dcmInt(4, &buffer[lPos], d.isLittleEndian); uint32_t kUndefinedLen = 0xFFFFFFFF; - if (slen != kUndefinedLen) { + if (itemTagLength != kUndefinedLen) { nNestPos++; if (nNestPos >= kMaxNestPost) nNestPos = kMaxNestPost - 1; - nestPos[nNestPos] = slen + lFileOffset + lPos; + nestPos[nNestPos] = itemTagLength + lFileOffset + lPos; } lLength = 4; sqDepth++; @@ -5037,6 +5038,9 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD lLength = 0; //Do not skip kItemTag - required to determine nesting of Philips Enhanced } } //if explicit else implicit VR + if ((lLength == 0xFFFFFFFF) && (vr[0] == 'O') && (vr[1] == 'B') && (isIconImageSequence)) { + lLength = 0; //encapuslated data of unspecified length + } //issue713 if (lLength == 0xFFFFFFFF) { lLength = 8; //SQ (Sequences) use 0xFFFFFFFF [4294967295] to denote unknown length //09032018 - do not count these as SQs: Horos does not count even groups @@ -5047,6 +5051,9 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD vr[1] = 'Q'; //} } + if ((groupElement == kItemTag) && (vr[0] == 'O') && (vr[1] == 'B') && (isIconImageSequence)) + lLength += itemTagLength; //issue713 + //printf("issue713::%c%c %04x,%04x %d@%lu\n", vr[0], vr[1], groupElement & 65535, groupElement >> 16, lLength, lPos); if ((groupElement == kItemTag) && (isEncapsulatedData)) { //use this to find image fragment for compressed datasets, e.g. JPEG transfer syntax d.imageBytes = dcmInt(4, &buffer[lPos], d.isLittleEndian); lPos = lPos + 4; diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 8788e9d8..4431f31d 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.20230509" +#define kDCMdate "v1.0.20230609" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic From 14c66a1fd2a80c1346d0fccb08db14ecc263db66 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Sun, 25 Jun 2023 07:21:16 -0400 Subject: [PATCH 07/49] dissociate REVSLICEORDER from SLICEORDER (https://github.com/rordenlab/dcm2niix/issues/723) --- console/nii_dicom.h | 2 +- console/nii_dicom_batch.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 4431f31d..d88105f6 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.20230609" +#define kDCMdate "v1.0.20230623" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 21e8387b..a65fc18e 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -994,7 +994,7 @@ int geProtocolBlock(const char *filename, int geOffset, int geLength, int isVerb // if ((pUnCmp[0] == '<') && (pUnCmp[1] == '?')) printWarning("New XML-based GE Protocol Block is not yet supported: please report issue on dcm2niix Github page\n"); - char keyStrSO[] = "SLICEORDER"; + char keyStrSO[] = "\nSLICEORDER"; *sliceOrder = readKeyN1(keyStrSO, (char *)pUnCmp, unCmpSz); char keyStrVO[] = "VIEWORDER"; *viewOrder = readKey(keyStrVO, (char *)pUnCmp, unCmpSz); From d7ed903d237de1f6748dd52e73c35ac014ebed96 Mon Sep 17 00:00:00 2001 From: "Brice Fernandez (100026991)" Date: Thu, 6 Jul 2023 15:08:58 +0200 Subject: [PATCH 08/49] This Fix corrects the TotalReadoutTime and EffectiveEchoSpacing fields in the BIDS (.json) sidecar in cose of image interpolation. TotalReadoutTime and EffectiveEchoSpacing were correct when there was no interpolation (i.e. AcquisitionMatrixPE = ReconMatrixPE). However, when interpolation was used, this values were incorrect. When interpolation was used, the estimated fieldmap given by TOPUP was ReconMatrixPE/AcquisitionMatrixPE times a measured fieldmap. Also, the same interpolated images corrected with a B0Map based method via FSL's epi_reg were clearly under-corrected. It turned out that the problem was comming from TotalReadoutTime and EffectiveEchoSpacing which were not taking ReconMatrixPE correctly into account. This is a Fix for that. GE/README.md was modified to clarified how TotalReadoutTime and EffectiveEchoSpacing are now computed. Some intermediate variable were also introduced and this described. This should help he final user to better understand. console/nii_dicom_batch.cpp was modified to implement the changes on TotalReadoutTime and EffectiveEchoSpacing. The intermediate variables described in README were also added into the json BIDS sidecar (i.e. EchoSpacingMicroSecondsGE, NotPhysicalNumberOfAcquiredPELinesGE, NotPhysicalTotalReadOutTimeGE). As the TotalReadoutTime definition is not intuitive this should help the user to understand what is going on. On branch dev-fix-TotalReadoutTimeGE-with-interpolation Changes to be committed: modified: GE/README.md modified: console/nii_dicom_batch.cpp --- GE/README.md | 17 +++++++++++++---- console/nii_dicom_batch.cpp | 12 +++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/GE/README.md b/GE/README.md index 7efbdc92..59e244f0 100644 --- a/GE/README.md +++ b/GE/README.md @@ -50,15 +50,24 @@ Some sequences allow the user to interpolate images in plane (e.g. saving a 2D 6 ## Total Readout Time -One often wants to determine [echo spacing, bandwidth](https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth) and total read-out time for EPI data so they can be undistorted. Specifically, we are interested in FSL's definition of total read-out time, which may differ from the actual read-out time. FSL expects “the time from the middle of the first echo to the middle of the last echo, as it would have been had partial k-space not been used”. So total read-out time is influenced by parallel acceleration factor, bandwidth, number of EPI lines, but not partial Fourier. For GE data we can use the Acquisition Matrix (0018,1310) in the phase-encoding direction, the in-plane acceleration ASSET R factor (the reciprocal of this is stored as the first element of 0043,1083) and the Effective Echo Spacing (0043,102C). While GE does not tell us the [partial Fourier fraction](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html), is does reveal if it is present with the ScanOptions (0018,1022) reporting [PFF](http://dicomlookup.com/lookup.asp?sw=Ttable&q=C.8-4) (in my experience, GE does not populate [(0018,9081)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9081))). While partial Fourier does not impact FSL's totalReadoutTime directly, it can interact with the number of lines acquired when combined with parallel imaging (the `Round_factor` 2 (Full Fourier) or 4 (Partial Fourier)). +One often wants to determine [echo spacing, bandwidth](https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth) and total read-out time for EPI data so they can be undistorted. Specifically, we are interested in FSL's definition of total read-out time, which may differ from the actual read-out time. FSL expects “the time from the middle of the first echo to the middle of the last echo, as it would have been had partial k-space not been used”. So total read-out time is influenced by parallel acceleration factor, bandwidth, number of EPI lines, but not partial Fourier. For GE data we can use the Acquisition Matrix (0018,1310) in the phase-encoding direction, the in-plane acceleration ASSET R factor (the reciprocal of this is stored as the first element of 0043,1083) and the Effective Echo Spacing (0043,102C). Note that the Effective Echo Spacing (0043,102C) in GE DICOMS is defined as the time between two consecutives acquired phase encoding lines divided by the number of shots (usually, this is equal to 1 for fMRI and Diffusion). +While GE does not tell us the [partial Fourier fraction](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html), is does reveal if it is present with the ScanOptions (0018,1022) reporting [PFF](http://dicomlookup.com/lookup.asp?sw=Ttable&q=C.8-4) (in my experience, GE does not populate [(0018,9081)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9081))). While partial Fourier does not impact FSL's totalReadoutTime directly, it can interact with the number of lines acquired when combined with parallel imaging (the `Round_factor` 2 (Full Fourier) or 4 (Partial Fourier)). -The formula for FSL's definition of TotalReadoutTime (in seconds) is: +Let `NotPhysicalNumberOfAcquiredPELinesGE` be the number of acquired phase encoding lines if there was no partial Fourier and `NotPhysicalTotalReadOutTimeGE` be the physical total read-out time if there was no partial Fourier. Please, note that these two intermadiate variables does not take partial Fourier into account. These two variables can be computed as ``` -TotalReadoutTime = ( ( ceil ((1/Round_factor) * PE_AcquisitionMatrix / Asset_R_factor ) * Round_factor) - 1 ] * EchoSpacing * 0.000001 -EffectiveEchoSpacing = TotalReadoutTime/ (reconMatrixPE - 1) +NotPhysicalNumberOfAcquiredPELinesGE = (ceil((1/Round_factor) * PE_AcquisitionMatrix / Asset_R_factor) * Round_factor) +NotPhysicalTotalReadOutTimeGE = (NotPhysicalNumberOfAcquiredPELinesGE - 1) * EchoSpacing * 0.000001 ``` +Then, the formula for FSL's definition of `EffectiveEchoSpacing` and `TotalReadoutTime` (in seconds) are: + +``` +EffectiveEchoSpacing = NotPhysicalTotalReadOutTimeGE / (PE_AcquisitionMatrix - 1) +TotalReadoutTime = EffectiveEchoSpacing * (ReconMatrixPE - 1) +``` +When there is no image interpolation (i.e. `ReconMatrixPE = PE_AcquisitionMatrix`) the `TotalReadoutTime` has the same value as `NotPhysicalTotalReadOutTimeGE`. In other words, `NotPhysicalTotalReadOutTimeGE` is the `TotalReadoutTime` without any image interpolation. + Consider an example: ``` diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index a65fc18e..2d0b7fc2 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -2001,10 +2001,16 @@ tse3d: T2*/ float roundFactor = 2.0; if (d.isPartialFourier) roundFactor = 4.0; - float totalReadoutTime = ((ceil(1 / roundFactor * d.phaseEncodingLines / d.accelFactPE) * roundFactor) - 1.0) * d.effectiveEchoSpacingGE * 0.000001; - //printf("ASSET= %g PE_AcquisitionMatrix= %d ESP= %d TotalReadoutTime= %g\n", d.accelFactPE, d.phaseEncodingLines, d.effectiveEchoSpacingGE, totalReadoutTime); + float NotPhysicalNumberOfAcquiredPELinesGE = (ceil(1 / roundFactor * d.phaseEncodingLines / d.accelFactPE) * roundFactor); + float NotPhysicalTotalReadOutTimeGE = ( NotPhysicalNumberOfAcquiredPELinesGE - 1.0) * d.effectiveEchoSpacingGE * 0.000001; + // printf("ASSET= %g PE_AcquisitionMatrix= %d ESP= %d TotalReadoutTimeGE= %g NumKyLineGE= %d\n", + // d.accelFactPE, d.phaseEncodingLines, d.effectiveEchoSpacingGE, NotPhysicalTotalReadOutTimeGE, (int)NotPhysicalNumberOfAcquiredPELinesGE); //json_Float(fp, "\t\"TotalReadoutTime\": %g,\n", totalReadoutTime); - effectiveEchoSpacing = totalReadoutTime / (reconMatrixPE - 1); + effectiveEchoSpacing = NotPhysicalTotalReadOutTimeGE / (d.phaseEncodingLines - 1); + // if this is considered acceptable, meaningful intermediate variables can be written, this might help the end-user. + fprintf(fp, "\t\"EchoSpacingMicroSecondsGE\": %d,\n", d.effectiveEchoSpacingGE); + fprintf(fp, "\t\"NotPhysicalNumberOfAcquiredPELinesGE\": %d,\n", (int)(NotPhysicalNumberOfAcquiredPELinesGE)); + json_Float(fp, "\t\"NotPhysicalTotalReadOutTimeGE\": %g,\n", NotPhysicalTotalReadOutTimeGE); } json_Float(fp, "\t\"EffectiveEchoSpacing\": %g,\n", effectiveEchoSpacing); // Calculate true echo spacing (should match what Siemens reports on the console) From c2b6d1a851de4391b84a4a2b1e8f8954ffc41ae4 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 6 Jul 2023 11:18:03 -0400 Subject: [PATCH 09/49] Bump version --- console/nii_dicom.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index d88105f6..e9e6aee6 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.20230623" +#define kDCMdate "v1.0.20230706" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic From 2abb179fffaace55f79de219da56653c541119b3 Mon Sep 17 00:00:00 2001 From: Yujing Huang Date: Thu, 6 Jul 2023 12:29:38 -0400 Subject: [PATCH 10/49] dcm2niix_fswrapper and libdcm2niixfs.a 1. save dicom file list in MRIFSSTRUCT, return vector of MRIFSSTRUCT for multi-echo scans if isForceStackSameSeries = 0 (-m n) 2. change output file format to "%4s.%d" from "%4s.%p" to be consistent with Freesurfer dcmunpack 3. remove space, [, ] from scan description 4. implement isDumpNotConvert option to retrieve dicom info only 5. dcm2niix_fswrapper::dicomDump() - output information of series to be converted, and seriesNum-dicomflst.txt in the same directory as series-info.dat; dcm2niix_fswrapper::seriesInfoDump() - print information for given dicom series 6. don't use std::max() from (fix Freesurfer ubuntu18 compiler error) --- console/dcm2niix_fswrapper.cpp | 232 ++++++++++++++++++++++++++++++++- console/dcm2niix_fswrapper.h | 12 +- console/nii_dicom.cpp | 54 +++++++- console/nii_dicom.h | 5 +- console/nii_dicom_batch.cpp | 122 ++++++++++++++--- console/nii_dicom_batch.h | 48 ++++--- console/nii_foreign.h | 4 +- 7 files changed, 436 insertions(+), 41 deletions(-) diff --git a/console/dcm2niix_fswrapper.cpp b/console/dcm2niix_fswrapper.cpp index e8eedfc6..1286fa37 100644 --- a/console/dcm2niix_fswrapper.cpp +++ b/console/dcm2niix_fswrapper.cpp @@ -1,4 +1,5 @@ #include +#include #include "nii_dicom.h" #include "dcm2niix_fswrapper.h" @@ -52,7 +53,7 @@ numSeries = 0 */ // set TDCMopts defaults, overwrite settings to output in mgz orientation -void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir, bool createBIDS) +void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir, bool createBIDS, int ForceStackSameSeries) { memset(&tdcmOpts, 0, sizeof(tdcmOpts)); setDefaultOpts(&tdcmOpts, NULL); @@ -62,7 +63,9 @@ void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir, bo if (niioutdir != NULL) strcpy(tdcmOpts.outdir, niioutdir); - strcpy(tdcmOpts.filename, "%4s.%p"); + // dcmunpack actually uses seriesDescription, set FName = `printf %04d.$descr $series` + // change it from "%4s.%p" to "%4s.%d" + strcpy(tdcmOpts.filename, "%4s.%d"); // set the options for freesurfer mgz orientation tdcmOpts.isRotate3DAcq = false; @@ -70,7 +73,7 @@ void dcm2niix_fswrapper::setOpts(const char* dcmindir, const char* niioutdir, bo tdcmOpts.isIgnoreSeriesInstanceUID = true; tdcmOpts.isCreateBIDS = createBIDS; tdcmOpts.isGz = false; - tdcmOpts.isForceStackSameSeries = 1; // merge 2D slice '-m y' + tdcmOpts.isForceStackSameSeries = ForceStackSameSeries; // merge 2D slice '-m y', tdcmOpts.isForceStackSameSeries = 1 tdcmOpts.isForceStackDCE = false; //tdcmOpts.isForceOnsetTimes = false; } @@ -107,6 +110,12 @@ MRIFSSTRUCT* dcm2niix_fswrapper::getMrifsStruct(void) return nii_getMrifsStruct(); } +// interface to nii_getMrifsStructVector() +std::vector* dcm2niix_fswrapper::getMrifsStructVector(void) +{ + return nii_getMrifsStructVector(); +} + // return nifti header saved in MRIFSSTRUCT nifti_1_header* dcm2niix_fswrapper::getNiiHeader(void) { @@ -121,11 +130,226 @@ const unsigned char* dcm2niix_fswrapper::getMRIimg(void) return mrifsStruct->imgM; } -void dcm2niix_fswrapper::dicomDump(const char* dicomdir) +void dcm2niix_fswrapper::dicomDump(const char* dicomdir, const char *series_info, bool max, bool extrainfo) { strcpy(tdcmOpts.indir, dicomdir); tdcmOpts.isDumpNotConvert = true; nii_loadDirCore(tdcmOpts.indir, &tdcmOpts); + char fnamecopy[2048] = {'\0'}; + memcpy(fnamecopy, series_info, strlen(series_info)); + char *logdir = dirname(fnamecopy); + + FILE *fpout = fopen(series_info, "w"); + + std::vector *mrifsStruct_vector = dcm2niix_fswrapper::getMrifsStructVector(); + int nitems = (*mrifsStruct_vector).size(); + for (int n = 0; n < nitems; n++) + { + struct TDICOMdata *tdicomData = &(*mrifsStruct_vector)[n].tdicomData; + + // output the dicom list for the series into seriesNum-dicomflst.txt + char dicomflst[2048] = {'\0'}; + sprintf(dicomflst, "%s/%ld-dicomflst.txt", logdir, tdicomData->seriesNum); + FILE *fp_dcmLst = fopen(dicomflst, "w"); + for (int nDcm = 0; nDcm < (*mrifsStruct_vector)[n].nDcm; nDcm++) + fprintf(fp_dcmLst, "%s\n", (*mrifsStruct_vector)[n].dicomlst[nDcm]); + fclose(fp_dcmLst); + + // output series_info + fprintf(fpout, "%ld %s %f %f %f %f\\%f %c %f %s %s", + tdicomData->seriesNum, tdicomData->seriesDescription, + tdicomData->TE, tdicomData->TR, tdicomData->flipAngle, tdicomData->xyzMM[1], tdicomData->xyzMM[2], + tdicomData->phaseEncodingRC, tdicomData->pixelBandwidth, (*mrifsStruct_vector)[n].dicomfile, tdicomData->imageType); +#if 0 + if (max) + { + //$max kImageStart 0x7FE0 + (0x0010 << 16) + fprintf(fpout, " max-value"); + } +#endif + if (extrainfo) + fprintf(fpout, " %s %s %s %s %f %s", tdicomData->patientName, tdicomData->studyDate, mfrCode2Str(tdicomData->manufacturer), tdicomData->manufacturersModelName, tdicomData->fieldStrength, tdicomData->deviceSerialNumber); + + fprintf(fpout, "\n"); + } + fclose(fpout); + return; } + +const char *dcm2niix_fswrapper::mfrCode2Str(int code) +{ + if (code == kMANUFACTURER_SIEMENS) + return "SIEMENS"; + else if (code == kMANUFACTURER_GE) + return "GE"; + else if (code == kMANUFACTURER_PHILIPS) + return "PHILIPS"; + else if (code == kMANUFACTURER_TOSHIBA) + return "TOSHIBA"; + else if (code == kMANUFACTURER_UIH) + return "UIH"; + else if (code == kMANUFACTURER_BRUKER) + return "BRUKER"; + else if (code == kMANUFACTURER_HITACHI) + return "HITACHI"; + else if (code == kMANUFACTURER_CANON) + return "CANON"; + else if (code == kMANUFACTURER_MEDISO) + return "MEDISO"; + else + return "UNKNOWN"; +} + + +void dcm2niix_fswrapper::seriesInfoDump(FILE *fpdump, const MRIFSSTRUCT *pmrifsStruct) +{ + // print dicom-info for the series converted using (*mrifsStruct_vector)[0] + // see output from mri_probedicom --i $f --no-siemens-ascii + + fprintf(fpdump, "###### %s ######\n", pmrifsStruct->dicomfile); + + const struct TDICOMdata *tdicomData = &(pmrifsStruct->tdicomData); + + // kManufacturer 0x0008 + (0x0070 << 16) + fprintf(fpdump, "Manufacturer %s\n", dcm2niix_fswrapper::mfrCode2Str(tdicomData->manufacturer)); + + // kManufacturersModelName 0x0008 + (0x1090 << 16) + fprintf(fpdump, "ScannerModel %s\n", tdicomData->manufacturersModelName); + + // kSoftwareVersions 0x0018 + (0x1020 << 16) //LO + fprintf(fpdump, "SoftwareVersion %s\n", tdicomData->softwareVersions); + + // kDeviceSerialNumber 0x0018 + (0x1000 << 16) //LO + fprintf(fpdump, "ScannerSerialNo %s\n", tdicomData->deviceSerialNumber); + + // kInstitutionName 0x0008 + (0x0080 << 16) + fprintf(fpdump, "Institution %s\n", tdicomData->institutionName); + + // kSeriesDescription 0x0008 + (0x103E << 16) // '0008' '103E' 'LO' 'SeriesDescription' + fprintf(fpdump, "SeriesDescription %s\n", tdicomData->seriesDescription); + + // kStudyInstanceUID 0x0020 + (0x000D << 16) + fprintf(fpdump, "StudyUID %s\n", tdicomData->studyInstanceUID); + + // kStudyDate 0x0008 + (0x0020 << 16) + fprintf(fpdump, "StudyDate %s\n", tdicomData->studyDate); + + // kStudyTime 0x0008 + (0x0030 << 16) + fprintf(fpdump, "StudyTime %s\n", tdicomData->studyTime); + + // kPatientName 0x0010 + (0x0010 << 16) + fprintf(fpdump, "PatientName %s\n", tdicomData->patientName); + + // kSeriesNum 0x0020 + (0x0011 << 16) + fprintf(fpdump, "SeriesNo %ld\n", tdicomData->seriesNum); + + // kImageNum 0x0020 + (0x0013 << 16) + fprintf(fpdump, "ImageNo %d\n", tdicomData->imageNum); + + // kMRAcquisitionType 0x0018 + (0x0023 << 16) + fprintf(fpdump, "AcquisitionType %s\n", (tdicomData->is3DAcq) ? "3D" : ((tdicomData->is3DAcq) ? "2D" : "unknown")); //dcm2niix has two values: d.is2DAcq, d.is3DAcq + + // kImageTypeTag 0x0008 + (0x0008 << 16) + fprintf(fpdump, "ImageType %s\n", tdicomData->imageType); + + // kImagingFrequency 0x0018 + (0x0084 << 16) //DS + fprintf(fpdump, "ImagingFrequency %f\n", tdicomData->imagingFrequency); + + // kPixelBandwidth 0x0018 + (0x0095 << 16) //'DS' 'PixelBandwidth' + fprintf(fpdump, "PixelFrequency %f\n", tdicomData->pixelBandwidth); + + // dcm2niix doesn't seem to retrieve this 0x18, 0x85 + //fprintf(fpdump, "ImagedNucleus %s\n",e->d.string); + + // kEchoNum 0x0018 + (0x0086 << 16) //IS + fprintf(fpdump, "EchoNumber %d\n", tdicomData->echoNum); + + // kMagneticFieldStrength 0x0018 + (0x0087 << 16) //DS + fprintf(fpdump, "FieldStrength %f\n", tdicomData->fieldStrength); + + // kSequenceName 0x0018 + (0x0024 << 16) + fprintf(fpdump, "PulseSequence %s\n", tdicomData->sequenceName); + + // kProtocolName 0x0018 + (0x1030 << 16) + fprintf(fpdump, "ProtocolName %s\n", tdicomData->protocolName); + + // kScanningSequence 0x0018 + (0x0020 << 16) + fprintf(fpdump, "ScanningSequence %s\n", tdicomData->scanningSequence); + + // dcm2niix doesn't seem to retrieve this + // kTransmitCoilName 0x0018 + (0x1251 << 16) // SH issue527 + //fprintf(fpdump, "TransmittingCoil %s\n",e->d.string); + + // kPatientOrient 0x0018 + (0x5100 << 16) //0018,5100. patient orientation - 'HFS' + fprintf(fpdump, "PatientPosition %s\n", tdicomData->patientOrient); + + // kFlipAngle 0x0018 + (0x1314 << 16) + fprintf(fpdump, "FlipAngle %f\n", tdicomData->flipAngle); + + // kTE 0x0018 + (0x0081 << 16) + fprintf(fpdump, "EchoTime %f\n", tdicomData->TE); + + // kTR 0x0018 + (0x0080 << 16) + fprintf(fpdump, "RepetitionTime %f\n", tdicomData->TR); + + // kTI 0x0018 + (0x0082 << 16) // Inversion time + fprintf(fpdump, "InversionTime %f\n", tdicomData->TI); + + // kPhaseEncodingSteps 0x0018 + (0x0089 << 16) //'IS' + fprintf(fpdump, "NPhaseEnc %d\n", tdicomData->phaseEncodingSteps); + + // kInPlanePhaseEncodingDirection 0x0018 + (0x1312 << 16) //CS + fprintf(fpdump, "PhaseEncDir %c\n", tdicomData->phaseEncodingRC); + + // kZSpacing 0x0018 + (0x0088 << 16) //'DS' 'SpacingBetweenSlices' + fprintf(fpdump, "SliceDistance %f\n", tdicomData->zSpacing); + + // kZThick 0x0018 + (0x0050 << 16) + fprintf(fpdump, "SliceThickness %f\n", tdicomData->zThick); // d.zThick=d.xyzMM[3] + + // kXYSpacing 0x0028 + (0x0030 << 16) //DS 'PixelSpacing' + fprintf(fpdump, "PixelSpacing %f\\%f\n", tdicomData->xyzMM[1], tdicomData->xyzMM[2]); + + // kDim2 0x0028 + (0x0010 << 16) + fprintf(fpdump, "NRows %d\n", tdicomData->xyzDim[2]); + + // kDim1 0x0028 + (0x0011 << 16) + fprintf(fpdump, "NCols %d\n", tdicomData->xyzDim[1]); + + // kBitsAllocated 0x0028 + (0x0100 << 16) + fprintf(fpdump, "BitsPerPixel %d\n", tdicomData->bitsAllocated); + + // dcm2niix doesn't seem to retrieve this + //fprintf(fpdump, "HighBit %d\n",*(e->d.us)); + + // dcm2niix doesn't seem to retrieve this + //fprintf(fpdump, "SmallestValue %d\n",*(e->d.us)); + + // dcm2niix doesn't seem to retrieve this + //fprintf(fpdump, "LargestValue %d\n",*(e->d.us)); + + // kOrientation 0x0020 + (0x0037 << 16) + //fprintf(fpdump, "ImageOrientation %f\\%f\\%f\\%f\\%f\\%f\n", + fprintf(fpdump, "ImageOrientation %g\\%g\\%g\\%g\\%g\\%g\n", + tdicomData->orient[1], tdicomData->orient[2], tdicomData->orient[3], + tdicomData->orient[4], tdicomData->orient[5], tdicomData->orient[6]); // orient[7] ??? + + // kImagePositionPatient 0x0020 + (0x0032 << 16) // Actually ! patientPosition[4] ??? + fprintf(fpdump, "ImagePosition %f\\%f\\%f\n", + tdicomData->patientPosition[1], tdicomData->patientPosition[2], tdicomData->patientPosition[3]); //two values: d.patientPosition, d.patientPositionLast + + // dcm2niix doesn't seem to retrieve this + // kSliceLocation 0x0020+(0x1041 << 16 ) //DS would be useful if not optional type 3 + //case kSliceLocation : //optional so useless, infer from image position patient (0020,0032) and image orientation (0020,0037) + // sliceLocation = dcmStrFloat(lLength, &buffer[lPos]); + // break; + //fprintf(fpdump, "SliceLocation %s\n",e->d.string); + + // kTransferSyntax 0x0002 + (0x0010 << 16) + fprintf(fpdump, "TransferSyntax %s\n", tdicomData->transferSyntax); + + // dcm2niix doesn't seem to retrieve this 0x51, 0x1016 + //fprintf(fpdump, "SiemensCrit %s\n",e->d.string); +} diff --git a/console/dcm2niix_fswrapper.h b/console/dcm2niix_fswrapper.h index 8cb0189f..0a71a8da 100644 --- a/console/dcm2niix_fswrapper.h +++ b/console/dcm2niix_fswrapper.h @@ -17,7 +17,7 @@ class dcm2niix_fswrapper { public: // set TDCMopts defaults, overwrite settings to output in mgz orientation. - static void setOpts(const char* dcmindir, const char* niioutdir=NULL, bool createBIDS=false); + static void setOpts(const char* dcmindir, const char* niioutdir=NULL, bool createBIDS=false, int ForceStackSameSeries=1); // interface to isDICOMfile() in nii_dicom.cpp static bool isDICOM(const char* file); @@ -32,10 +32,18 @@ class dcm2niix_fswrapper // return nifti header saved in MRIFSSTRUCT static nifti_1_header* getNiiHeader(void); + // interface to nii_getMrifsStructVector() + static std::vector* getMrifsStructVector(void); + // return image data saved in MRIFSSTRUCT static const unsigned char* getMRIimg(void); - static void dicomDump(const char* dicomdir); + static void dicomDump(const char* dicomdir, const char *series_info, bool max=false, bool extrainfo=false); + + // + static void seriesInfoDump(FILE *fpdump, const MRIFSSTRUCT *pmrifsStruct); + + static const char *mfrCode2Str(int code); private: static struct TDCMopts tdcmOpts; diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 07764988..209bf3e6 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -37,7 +37,9 @@ #include #include // discriminate files from folders #include +#ifndef USING_DCM2NIIXFSWRAPPER #include +#endif #ifdef USING_R #undef isnan @@ -2042,6 +2044,9 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt strcat(d.seriesDescription, Comment[6]); strcat(d.seriesDescription, Comment[7]); cleanStr(d.seriesDescription); +#ifdef USING_DCM2NIIXFSWRAPPER + remove_specialchars(d.seriesDescription); +#endif } if ((strcmp(Comment[0], "Examination") == 0) && (strcmp(Comment[1], "date/time") == 0)) { if ((strlen(Comment[3]) >= 10) && (strlen(Comment[5]) >= 8)) { @@ -2362,7 +2367,12 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt //offset images by type: mag+0,real+1, imag+2,phase+3 //if (cols[kImageType] != 0) //yikes - phase maps! // slice = slice + numExpected; - maxSlice2D = std::max(slice, maxSlice2D); +#ifdef USING_DCM2NIIXFSWRAPPER + // fix ubuntu18 compiler error + maxSlice2D = (slice > maxSlice2D) ? slice : maxSlice2D; +#else + maxSlice2D = std::max(slice, maxSlice2D); +#endif if ((slice >= 0) && (slice < kMaxSlice2D) && (numSlice2D < kMaxSlice2D) && (numSlice2D >= 0)) { dti4D->sliceOrder[slice - 1] = numSlice2D; //printMessage("%d\t%d\t%d\n", numSlice2D, slice, (int)cols[kSlice],(int)vol); @@ -2428,7 +2438,12 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt if (d.isHasImaginary) nType ++; if (d.isHasPhase) nType ++; if (d.isHasReal) nType ++; - nType = std::max(nType, 1); +#ifdef USING_DCM2NIIXFSWRAPPER + // fix ubuntu18 compiler error + nType = (nType > 1) ? nType : 1; +#else + nType = std::max(nType, 1); +#endif if (slice != numSlice2D) { printError("Catastrophic error: found %d but expected %d slices. %s\n", slice, numSlice2D, parname); printMessage(" slices*grad*bval*cardiac*echo*dynamic*mix*labels*types = %d*%d*%d*%d*%d*%d*%d*%d*%d\n", @@ -5223,6 +5238,9 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD char transferSyntax[kDICOMStr]; strcpy(transferSyntax, ""); dcmStr(lLength, &buffer[lPos], transferSyntax); +#ifdef USING_DCM2NIIXFSWRAPPER + strcpy(d.transferSyntax, transferSyntax); +#endif if (strcmp(transferSyntax, "1.2.840.10008.1.2.1") == 0) ; //default isExplicitVR=true; //d.isLittleEndian=true else if (strcmp(transferSyntax, "1.2.840.10008.1.2.4.50") == 0) { @@ -5572,6 +5590,9 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD break; case kSeriesDescription: dcmStr(lLength, &buffer[lPos], d.seriesDescription); +#ifdef USING_DCM2NIIXFSWRAPPER + remove_specialchars(d.seriesDescription); +#endif break; case kInstitutionalDepartmentName: dcmStr(lLength, &buffer[lPos], d.institutionalDepartmentName); @@ -8070,3 +8091,32 @@ struct TDICOMdata readDICOM(char *fname) { free(dti4D); return ret; } // readDICOM() + + +#ifdef USING_DCM2NIIXFSWRAPPER +// remove spaces, '[', ']' in given buf +// ??? also remove <, >, & +void remove_specialchars(char *buf) +{ + if (strchr(buf, ' ') == NULL) + return; + + int buflen = strlen(buf)+1; + char *newbuf = new char[buflen]; + memset(newbuf, '\0' , buflen); + + char *ptr_newbuf = &newbuf[0]; + for (int i = 0; i < buflen; i++) + { + // skip spaces, '[', ']' + if (buf[i] == ' ' || buf[i] == '[' || buf[i] == ']') + continue; + + *ptr_newbuf = buf[i]; + ptr_newbuf++; + } + + memcpy(buf, newbuf, ptr_newbuf-newbuf); + free(newbuf); +} +#endif diff --git a/console/nii_dicom.h b/console/nii_dicom.h index e9e6aee6..d9db4dba 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -243,7 +243,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; 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]; + char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[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]; @@ -276,6 +276,9 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; int headerDcm2Nii2(struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1_header *h, int isVerbose); int headerDcm2Nii(struct TDICOMdata d, struct nifti_1_header *h, bool isComputeSForm) ; unsigned char * nii_loadImgXL(char* imgname, struct nifti_1_header *hdr, struct TDICOMdata dcm, bool iVaries, int compressFlag, int isVerbose, struct TDTI4D *dti4D); +#ifdef USING_DCM2NIIXFSWRAPPER + void remove_specialchars(char *buf); +#endif #ifdef __cplusplus } #endif diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 2d0b7fc2..24eb184e 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -113,6 +113,7 @@ const char kFileSep[2] = "/"; // create the struct to save nifti header, image data, TDICOMdata, & TDTI information. // no .nii, .bval, .bvec are created. MRIFSSTRUCT mrifsStruct; +std::vector mrifsStruct_vector; // retrieve the struct MRIFSSTRUCT* nii_getMrifsStruct() @@ -125,6 +126,36 @@ void nii_clrMrifsStruct() { free(mrifsStruct.imgM); free(mrifsStruct.tdti); + + for (int n = 0; n < mrifsStruct.nDcm; n++) + free(mrifsStruct.dicomlst[n]); + + if (mrifsStruct.dicomlst != NULL) + free(mrifsStruct.dicomlst); + +} + +// retrieve the struct +std::vector* nii_getMrifsStructVector() +{ + return &mrifsStruct_vector; +} + +// free the memory used for the image and dti +void nii_clrMrifsStructVector() +{ + int nitem = mrifsStruct_vector.size(); + for (int n = 0; n < nitem; n++) + { + free(mrifsStruct_vector[n].imgM); + free(mrifsStruct_vector[n].tdti); + + for (int n = 0; n < mrifsStruct.nDcm; n++) + free(mrifsStruct_vector[n].dicomlst[n]); + + if (mrifsStruct_vector[n].dicomlst != NULL) + free(mrifsStruct_vector[n].dicomlst); + } } #endif @@ -3397,6 +3428,9 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts //strcat (outname,newstr); strcat(outname, "_c"); strcat(outname, dcm.coilName); +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s_c%s", mrifsStruct.namePostFixes, dcm.coilName); +#endif } // myMultiEchoFilenameSkipEcho1 https://github.com/rordenlab/dcm2niix/issues/237 #ifdef myMultiEchoFilenameSkipEcho1 @@ -3410,15 +3444,24 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts snprintf(newstr, PATH_MAX, "_e%d", dcm.echoNum); strcat(outname, newstr); isEchoReported = true; +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s%s", mrifsStruct.namePostFixes, newstr); +#endif } if ((isAddNamePostFixes) && (!isSeriesReported) && (!isEchoReported) && (dcm.echoNum > 1)) { //last resort: user provided no method to disambiguate echo number in filename snprintf(newstr, PATH_MAX, "_e%d", dcm.echoNum); strcat(outname, newstr); isEchoReported = true; +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s%s", mrifsStruct.namePostFixes, newstr); +#endif } if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { snprintf(newstr, PATH_MAX, "_i%05d", dcm.imageNum); strcat(outname, newstr); +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s%s", mrifsStruct.namePostFixes, newstr); +#endif } /*if (dcm.maxGradDynVol > 0) { //Philips segmented snprintf(newstr, PATH_MAX, "_v%04d", dcm.gradDynVol+1); //+1 as indexed from zero @@ -3426,21 +3469,36 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts }*/ if ((isAddNamePostFixes) && (dcm.isHasImaginary)) { strcat(outname, "_imaginary"); //has phase map +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s_imaginary", mrifsStruct.namePostFixes); +#endif } if ((isAddNamePostFixes) && (dcm.isHasReal) && (dcm.isRealIsPhaseMapHz)) { strcat(outname, "_fieldmaphz"); //has field map +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s_fieldmaphz", mrifsStruct.namePostFixes); +#endif } if ((isAddNamePostFixes) && (dcm.isHasReal) && (!dcm.isRealIsPhaseMapHz)) { strcat(outname, "_real"); //has phase map +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s_real", mrifsStruct.namePostFixes); +#endif } if ((isAddNamePostFixes) && (dcm.isHasPhase)) { strcat(outname, "_ph"); //has phase map if (dcm.isHasMagnitude) strcat(outname, "Mag"); //Philips enhanced with BOTH phase and Magnitude in single file +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s_ph", mrifsStruct.namePostFixes); +#endif } if ((isAddNamePostFixes) && (dcm.aslFlags == kASL_FLAG_NONE) && (dcm.triggerDelayTime >= 1) && (dcm.manufacturer != kMANUFACTURER_GE)) { //issue 336 GE uses this for slice timing snprintf(newstr, PATH_MAX, "_t%d", (int)roundf(dcm.triggerDelayTime)); strcat(outname, newstr); +#ifdef USING_DCM2NIIXFSWRAPPER + sprintf(mrifsStruct.namePostFixes, "%s%s", mrifsStruct.namePostFixes, newstr); +#endif } //could add (isAddNamePostFixes) to these next two, but consequences could be catastrophic if (dcm.isRawDataStorage) //avoid name clash for Philips XX_ files @@ -5131,10 +5189,11 @@ int nii_saveNII(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, fwrite(&pad, sizeof(pad), 1, fp); fwrite(&im[0], imgsz, 1, fp); fclose(fp); -#endif if (!opts.isSaveNativeEndian) swapEndian(&hdr, im, false); //unbyte-swap endian (e.g. big->little) +#endif + if ((opts.isGz) && (strlen(opts.pigzname) > 0)) { #ifndef myDisableGzSizeLimits if ((imgsz + hdr.vox_offset) > kMaxPigz) { @@ -6690,6 +6749,7 @@ void loadOverlay(char *imgname, unsigned char *img, int offset, int x, int y, in } //loadOverlay() int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts, struct TDTI4D *dti4D, int segVol) { +#if 0 #ifdef USING_DCM2NIIXFSWRAPPER double seriesNum = (double) dcmList[dcmSort[0].indx].seriesUidCrc; int segVolEcho = segVol; @@ -6700,6 +6760,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if (!isSameDouble(opts.seriesNumber[0], seriesNum)) return EXIT_SUCCESS; +#endif #endif bool iVaries = intensityScaleVaries(nConvert, dcmSort, dcmList); @@ -7194,6 +7255,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d free(imgM); return EXIT_FAILURE; } + +#ifndef USING_DCM2NIIXFSWRAPPER // skip converting if user has specified one or more series, but has not specified this one if (opts.numSeries > 0) { //issue453: moved to before saveBIDS int i = 0; @@ -7210,6 +7273,12 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if (i == opts.numSeries) return EXIT_SUCCESS; } +#else + double seriesNum = (double)dcmList[dcmSort[0].indx].seriesUidCrc; + if (!isSameDouble(opts.seriesNumber[0], seriesNum)) + return EXIT_SUCCESS; +#endif + if (opts.numSeries >= 0) //issue453 nii_SaveBIDSX(pathoutname, dcmList[dcmSort[0].indx], opts, &hdr0, nameList->str[dcmSort[0].indx], dti4D); if (opts.isOnlyBIDS) { @@ -7506,6 +7575,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d mrifsStruct.hdr0 = hdr0; mrifsStruct.imgsz = nii_ImgBytes(hdr0); mrifsStruct.imgM = imgM; + + mrifsStruct_vector.push_back(mrifsStruct); #else free(imgM); #endif @@ -7516,22 +7587,23 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts, struct TDTI4D *dti4D) { #ifdef USING_DCM2NIIXFSWRAPPER + memset(&mrifsStruct, 0, sizeof(mrifsStruct)); + + int indx0 = dcmSort[0].indx; + + int len_dicomfile = strlen(nameList->str[indx0]); + mrifsStruct.dicomfile = (char*)malloc(len_dicomfile+1); + memset(mrifsStruct.dicomfile, 0, len_dicomfile+1); + memcpy(mrifsStruct.dicomfile, nameList->str[indx0], len_dicomfile); + if (opts.isDumpNotConvert) { - int indx0 = dcmSort[0].indx; - if (opts.isIgnoreSeriesInstanceUID) - printMessage("%d %s %s (total %d)\n", dcmList[indx0].seriesUidCrc, dcmList[indx0].protocolName, nameList->str[indx0], nConvert); - else - printMessage("%d %ld %s %s (total %d)\n", dcmList[indx0].seriesUidCrc, dcmList[indx0].seriesNum, dcmList[indx0].protocolName, nameList->str[indx0], nConvert); + mrifsStruct.tdicomData = dcmList[indx0]; // first in sorted list dcmSort + mrifsStruct.dicomlst = new char*[nConvert]; + mrifsStruct.nDcm = nConvert; -#if 1 - for (int i = 0; i < nConvert; i++) { - int indx = dcmSort[i].indx; - if (opts.isIgnoreSeriesInstanceUID) - printMessage("\t#\%d: %d %s\n", i+1, dcmList[indx].seriesUidCrc, nameList->str[indx]); - else - printMessage("\t#\%d: %d %ld %s\n", i+1, dcmList[indx].seriesUidCrc, dcmList[indx].seriesNum, nameList->str[indx]); - } -#endif + dcmListDump(nConvert, dcmSort, dcmList, nameList, opts); + + mrifsStruct_vector.push_back(mrifsStruct); return 0; } @@ -9174,3 +9246,23 @@ void saveIniFile(struct TDCMopts opts) { } //saveIniFile() #endif + + +#ifdef USING_DCM2NIIXFSWRAPPER +// this function outputs information in imageList.dat for dcmunpack +// the following fields from struct TDICOMdata are printed: +// patientName seriesNum studyDate studyTime TE TR flipAngle xyzMM[1]\xyzMM[2] phaseEncodingRC pixelBandwidth dicom-file imageType +void dcmListDump(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts) { + for (int i = 0; i < nConvert; i++) { + int indx = dcmSort[i].indx; + mrifsStruct.dicomlst[i] = new char[strlen(nameList->str[indx])+1]; + memset(mrifsStruct.dicomlst[i], 0, strlen(nameList->str[indx])+1); + memcpy(mrifsStruct.dicomlst[i], nameList->str[indx], strlen(nameList->str[indx])); + + printMessage("%s %ld %s %s %f %f %f %f\\%f %c %f %s %s\n", + dcmList[indx].patientName, dcmList[indx].seriesNum, dcmList[indx].studyDate, dcmList[indx].studyTime, + dcmList[indx].TE, dcmList[indx].TR, dcmList[indx].flipAngle, dcmList[indx].xyzMM[1], dcmList[indx].xyzMM[2], + dcmList[indx].phaseEncodingRC, dcmList[indx].pixelBandwidth, nameList->str[indx], dcmList[indx].imageType); + } +} +#endif diff --git a/console/nii_dicom_batch.h b/console/nii_dicom_batch.h index 00ab73ec..3c56cefc 100644 --- a/console/nii_dicom_batch.h +++ b/console/nii_dicom_batch.h @@ -4,6 +4,38 @@ #ifndef MRIpro_nii_batch_h #define MRIpro_nii_batch_h +#ifdef USING_DCM2NIIXFSWRAPPER +#include "nifti1.h" +#include "nii_dicom.h" +#include + +struct MRIFSSTRUCT +{ + struct nifti_1_header hdr0; + + size_t imgsz; + unsigned char *imgM; + + struct TDICOMdata tdicomData; + char namePostFixes[256]; + char *dicomfile; + + int nDcm; + char **dicomlst; + + struct TDTI *tdti; + int numDti; +}; + +MRIFSSTRUCT* nii_getMrifsStruct(); +void nii_clrMrifsStruct(); + +std::vector* nii_getMrifsStructVector(); +void nii_clrMrifsStructVector(); + +void dcmListDump(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts); +#endif + #ifdef __cplusplus extern "C" { #endif @@ -23,22 +55,6 @@ extern "C" { }; #endif -typedef struct -{ - struct nifti_1_header hdr0; - - size_t imgsz; - unsigned char *imgM; - - struct TDICOMdata tdicomData; - - struct TDTI *tdti; - int numDti; -} MRIFSSTRUCT; - -MRIFSSTRUCT* nii_getMrifsStruct(); -void nii_clrMrifsStruct(); - #define kNAME_CONFLICT_SKIP 0 //0 = write nothing for a file that exists with desired name #define kNAME_CONFLICT_OVERWRITE 1 //1 = overwrite existing file with same name #define kNAME_CONFLICT_ADD_SUFFIX 2 //default 2 = write with new suffix as a new file diff --git a/console/nii_foreign.h b/console/nii_foreign.h index 9249a4c9..a3fea7e5 100644 --- a/console/nii_foreign.h +++ b/console/nii_foreign.h @@ -3,11 +3,13 @@ #ifndef _NII_FOREIGN_ #define _NII_FOREIGN_ +#include "nii_dicom_batch.h" + #ifdef __cplusplus extern "C" { #endif -#include "nii_dicom_batch.h" +//#include "nii_dicom_batch.h" //int open_foreign (const char *fn); int convert_foreign (const char *fn, struct TDCMopts opts); From 303051bc49d8d9fb9afb2f4a26edb6fcbc82a70b Mon Sep 17 00:00:00 2001 From: "Brice Fernandez (100026991)" Date: Fri, 7 Jul 2023 15:56:28 +0200 Subject: [PATCH 11/49] Documentation fix for GE TotalReadoutTime. It is now clarified that the `EchoSpacing` variable is the GE private DICOM field "(0043,102C) Effective Echo Spacing". On branch fix-documentation-GE-TotalReadoutTime Changes to be committed: modified: GE/README.md --- GE/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/GE/README.md b/GE/README.md index 59e244f0..a8024647 100644 --- a/GE/README.md +++ b/GE/README.md @@ -60,6 +60,7 @@ NotPhysicalNumberOfAcquiredPELinesGE = (ceil((1/Round_factor) * PE_AcquisitionMa NotPhysicalTotalReadOutTimeGE = (NotPhysicalNumberOfAcquiredPELinesGE - 1) * EchoSpacing * 0.000001 ``` +with `EchoSpacing` referring to the GE private DICOM field "(0043,102C) Effective Echo Spacing". Then, the formula for FSL's definition of `EffectiveEchoSpacing` and `TotalReadoutTime` (in seconds) are: ``` From 2b0092a459d05709dbf18773ac9b8afbdd95e6a3 Mon Sep 17 00:00:00 2001 From: "Brice Fernandez (100026991)" Date: Mon, 10 Jul 2023 12:45:48 +0200 Subject: [PATCH 12/49] Fix code and doc for GE TotalReadoutTime Fix typos in GE/README.md and code mofification to add the intermediate variables for TotalReadoutTime on GE system in debug mode only (via compilation flag MY_DEBUG). Added a note for that in GE/README.md: For debugging purposes, the intermediate variables `NotPhysicalTotalReadOutTimeGE`, `NotPhysicalNumberOfAcquiredPELinesGE` and `EchoSpacing` (renamed `EchoSpacingMicroSecondsGE`) are written to the BIDS-sidecar JSON file when dcm2niix is compiled with the flag `MY_DEBUG`. On branch dev-fix-GETotalReadoutTime-doc-and-code Changes to be committed: modified: GE/README.md modified: console/nii_dicom_batch.cpp --- GE/README.md | 3 ++- console/nii_dicom_batch.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/GE/README.md b/GE/README.md index a8024647..5bfbfceb 100644 --- a/GE/README.md +++ b/GE/README.md @@ -53,7 +53,7 @@ Some sequences allow the user to interpolate images in plane (e.g. saving a 2D 6 One often wants to determine [echo spacing, bandwidth](https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth) and total read-out time for EPI data so they can be undistorted. Specifically, we are interested in FSL's definition of total read-out time, which may differ from the actual read-out time. FSL expects “the time from the middle of the first echo to the middle of the last echo, as it would have been had partial k-space not been used”. So total read-out time is influenced by parallel acceleration factor, bandwidth, number of EPI lines, but not partial Fourier. For GE data we can use the Acquisition Matrix (0018,1310) in the phase-encoding direction, the in-plane acceleration ASSET R factor (the reciprocal of this is stored as the first element of 0043,1083) and the Effective Echo Spacing (0043,102C). Note that the Effective Echo Spacing (0043,102C) in GE DICOMS is defined as the time between two consecutives acquired phase encoding lines divided by the number of shots (usually, this is equal to 1 for fMRI and Diffusion). While GE does not tell us the [partial Fourier fraction](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html), is does reveal if it is present with the ScanOptions (0018,1022) reporting [PFF](http://dicomlookup.com/lookup.asp?sw=Ttable&q=C.8-4) (in my experience, GE does not populate [(0018,9081)](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,9081))). While partial Fourier does not impact FSL's totalReadoutTime directly, it can interact with the number of lines acquired when combined with parallel imaging (the `Round_factor` 2 (Full Fourier) or 4 (Partial Fourier)). -Let `NotPhysicalNumberOfAcquiredPELinesGE` be the number of acquired phase encoding lines if there was no partial Fourier and `NotPhysicalTotalReadOutTimeGE` be the physical total read-out time if there was no partial Fourier. Please, note that these two intermadiate variables does not take partial Fourier into account. These two variables can be computed as +Let `NotPhysicalNumberOfAcquiredPELinesGE` be the number of acquired phase encoding lines if there was no partial Fourier and `NotPhysicalTotalReadOutTimeGE` be the physical total read-out time if there was no partial Fourier. Please, note that these two intermediate variables do not take partial Fourier into account. These two variables can be computed as ``` NotPhysicalNumberOfAcquiredPELinesGE = (ceil((1/Round_factor) * PE_AcquisitionMatrix / Asset_R_factor) * Round_factor) @@ -83,6 +83,7 @@ From this we can derive: ``` ASSET= 1.5 PE_AcquisitionMatrix= 128 EchoSpacing= 636 Round_Factor= 4 TotalReadoutTime= 0.055332 ``` +For debugging purposes, the intermediate variables `NotPhysicalTotalReadOutTimeGE`, `NotPhysicalNumberOfAcquiredPELinesGE` and `EchoSpacing` (renamed `EchoSpacingMicroSecondsGE`) are written to the BIDS-sidecar JSON file when dcm2niix is compiled with the flag `MY_DEBUG`. ## Image Acceleration diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 2d0b7fc2..a3ff6dbd 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -2008,9 +2008,11 @@ tse3d: T2*/ //json_Float(fp, "\t\"TotalReadoutTime\": %g,\n", totalReadoutTime); effectiveEchoSpacing = NotPhysicalTotalReadOutTimeGE / (d.phaseEncodingLines - 1); // if this is considered acceptable, meaningful intermediate variables can be written, this might help the end-user. +#ifdef MY_DEBUG fprintf(fp, "\t\"EchoSpacingMicroSecondsGE\": %d,\n", d.effectiveEchoSpacingGE); fprintf(fp, "\t\"NotPhysicalNumberOfAcquiredPELinesGE\": %d,\n", (int)(NotPhysicalNumberOfAcquiredPELinesGE)); json_Float(fp, "\t\"NotPhysicalTotalReadOutTimeGE\": %g,\n", NotPhysicalTotalReadOutTimeGE); +#endif } json_Float(fp, "\t\"EffectiveEchoSpacing\": %g,\n", effectiveEchoSpacing); // Calculate true echo spacing (should match what Siemens reports on the console) From 0d48e95839ba767dcda4bf0bdccff23810a0c642 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Wed, 9 Aug 2023 09:53:04 -0400 Subject: [PATCH 13/49] BidsGuess feature --- BidsGuess/BidsGuess.png | Bin 0 -> 35441 bytes BidsGuess/README.md | 63 +++ README.md | 2 +- console/nii_dicom.cpp | 31 +- console/nii_dicom.h | 4 +- console/nii_dicom_batch.cpp | 750 +++++++++++++++++++++++++++++++++++- console/nii_dicom_batch.h | 2 +- console/pypeline.log | 0 8 files changed, 829 insertions(+), 23 deletions(-) create mode 100644 BidsGuess/BidsGuess.png create mode 100644 BidsGuess/README.md create mode 100644 console/pypeline.log diff --git a/BidsGuess/BidsGuess.png b/BidsGuess/BidsGuess.png new file mode 100644 index 0000000000000000000000000000000000000000..dbc7102dd7e4bbfa4de330a8dbe34b2aad81ab89 GIT binary patch literal 35441 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 literal 0 HcmV?d00001 diff --git a/BidsGuess/README.md b/BidsGuess/README.md new file mode 100644 index 00000000..6f436593 --- /dev/null +++ b/BidsGuess/README.md @@ -0,0 +1,63 @@ +## About + +**The BidsGuess feature is intended for wrapper developers only and should not be used in isolation of a wrapper** + +dcm2niix version v1.0.20230731 and later will insert the field `BidsGuess` into the [BIDS](https://bids-specification.readthedocs.io/en/stable/) JSON sidecar files. This experimental feature is designed to aid wrappers that use dcm2niix to create BIDS compatible datasets. BIDS aids reproducible, reusable and automatic analysis of neuroimaging data. This feature was inspired by the automatic BIDS conversion wrappers [niix2bids](https://github.com/benoitberanger/niix2bids) and [ezBIDS](https://brainlife.io/ezbids/). The `BidsGuess` field can be leveraged by wrappers to fully automate conversion (though this will likely require a ezBIDS style user validation) or to flag improbable naming from user configuration files. + +## Compiling the development branch + +The BidsGuess feature is currently only available in the development branch, so you will need to comile and run this version (v1.0.20230731 and later). + +``` +git clone --branch development https://github.com/rordenlab/dcm2niix.git +cd dcm2niix/console +make +./dcm2niix +``` + +## A concrete example + +The BidsGuess feature is designed to be leveraged by dcm2niix wrappers, and not used to directly create BIDS format files. Specifically, bidsGuess converts each DICOM series in isolation, and has no information about the user intention. Therefore, it is unable to resolve fieldmap [IntendedFor](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#using-intendedfor-metadata), unable to distinguish fMRI tasks from resting state, BIDS subject ID, BIDS session number, or create meaningful [dataset_description](https://bids-specification.readthedocs.io/en/stable/glossary.html#dataset_description-files) or [readme](https://bids-specification.readthedocs.io/en/stable/glossary.html#readme-files) files. + +For the developers of wrappers, you can use the hazardous file naming argument (`-f $h`) to create a minimal BIDS structure for the [bids-validator](https://github.com/bids-standard/bids-validator). Note that this mode will always claim that the data is from `sub-1` and that data is only from a single session. Here is a simple example: + +``` +cd ~ +mkdir bids +git clone git@github.com:neurolabusc/dcm_qa_pdt2.git +dcm2niix -f %h -w 1 -i y -o ~/bids ~/dcm_qa_pdt2 +bids-validator ~/bids +``` +You can see that the bids-validator is happy with the results and that the data appears organized correctly: + +![BidsGuess](BidsGuess.png) + +Inspecting the JSON files, we can see that dcm2niix has suggested a likely <[datatype](https://bids-specification.readthedocs.io/en/stable/schema/index.html#bids-filenames)> (`anat`) and <[entities](https://bids-specification.readthedocs.io/en/stable/schema/index.html#bids-filenames)>. + +``` + "BidsGuess": ["anat","_acq-tse2_run-3_PDw"], + "BidsGuess": ["anat","_acq-tse2_run-3_T2w"], +``` + +Developers can use the `hazardous` file naming to validate and extend the modality detection. However, production quality wrappers should use the `BidsGuess` in the JSON file combined by a file naming scheme that avoids name clashes between different participants and sessions (e.g. one can segment data by datetime, series and protocol name with `-f %t/%s_%p`). + +## The acq entity + +The `BidsGuess` creates a meaningful [`_acq-` entity](https://bids-specification.readthedocs.io/en/stable/appendices/entities.html#acq) that can provide consistency across wrappers, minimizes the risk of naming clashes and allows users to quickly detect sequence details. The first part of this reveals the manufacturer's name for the sequence. For example, a Siemens 2D turbo spin-echo will report `tse2`, while a 2D echo-planar spin-echo will report `epse2` and a 3D turbo flash will report `tfl3`. If acceleration was used, this will be reported next. For example, a 2D echo-planar gradient-echo with x3 in-plane and x4 mult-iband acceleration would be reported as `epfid2p3m4`. + +## The run entity + +The `BidsGuess` will typically include a [`_run-` entity](https://bids-specification.readthedocs.io/en/stable/appendices/entities.html#run) that reports the DICOM series number of an image. This is often useful for determining the temporal order of images (e.g. if the first attempt to acquire the data was due to head motion). This entity also avoids naming clashes. Note that wrapper developers may want to remove this entity from the BIDS guess - it is redundant with the `SeriesNumber` stored in the sidecar JSON. Finally, dcm2niix will not append a `_run` entity for fieldmaps: the BIDS validation tool expects that the different images associated with a fieldmap have identical file names except the suffix (e.g. `magnitude1` and `phasediff`). + +## The dir entity + +The `BidsGuess` will typically include a [`_dir-` entity](https://bids-specification.readthedocs.io/en/stable/appendices/entities.html#dir) for modalities when required by the BIDS validator. This field is redunant with the JSON `PhaseEncodingDirection` field. Note that the JSON uses the values `j`, `j-`, `k` and `k-` to specify row versus column and polarity. In contrast, the BidsGuess will use the `AP`, `PA`, `LR`, and `RL` tags though note these tags will only be correct for axial acquisitions. Note that it is impossible to infer phase encoding polarity for Philips data, so this entity will not be populated leading to errors from the bids-validator. + +## Limitations + +This feature is very experimental, and is currently provided to get feedback from wrapper developers and to get community support to enhance the guessing accuracy. + + - This feature currently only supports images from GE, Philips and Siemens MR scanners. + - Multi-Echo MP-RAGE where individual echos (rather than mean) are saved will include the [_echo](https://bids-specification.readthedocs.io/en/stable/appendices/entities.html#echo) entity to distinguish them, e.g. `_acq-tflme3p2_run-5_echo-2_T1w`, `_acq-tflme3p2_run-5_echo-1_T1w`. This will [cause issues](https://github.com/bids-standard/bids-specification/issues/654) with the current bids-validator. + - ASL datasets will generate errors with the bids-validator. The ASL BEP introduced many required tags for converted data without explaining how these are determined or even if they exist in the core DICOM images. The validation dataset [only provides the desired BIDS translation and not the source DICOMs](https://osf.io/yru2q/). + - Philips DICOMs are underspecified for BIDS conversion. Beyond the previously noted issue with phaseEncodingDirection, the `SliceTiming` is also unknown. This is a limitation of Philips DICOM, not dcm2niix. diff --git a/README.md b/README.md index 1a9f26ff..51851de1 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,6 @@ The following tools exploit dcm2niix - [BraTS-Preprocessor](https://neuronflow.github.io/BraTS-Preprocessor/) uses dcm2niix to import files for [Brain Tumor Segmentation](https://www.frontiersin.org/articles/10.3389/fnins.2020.00125/full). - [CardioNIfTI](https://github.com/UK-Digital-Heart-Project/CardioNIfTI) processes cardiac MR DICOM datasets and converts them to NIfTI. - [clinica](https://github.com/aramis-lab/clinica) is a software platform for clinical neuroimaging studies that uses dcm2niix to convert DICOM images. - - [clinical_dicom2bids_smk](https://github.com/greydongilmore/clinical_dicom2bids_smk) Snakemake workflow to convert a clinical dicom directory into BIDS structure. - [clpipe](https://github.com/cohenlabUNC/clpipe) uses dcm2bids for DICOM import. - [conversion](https://github.com/pnlbwh/conversion) is a Python library that can convert dcm2niix created NIfTI files to the popular NRRD format (including DWI gradient tables). Note, recent versions of dcm2niix can directly convert DICOM images to NRRD. - [convert_source](https://github.com/AdebayoBraimah/convert_source) to convert DICOM to BIDS directory layout. @@ -196,6 +195,7 @@ The following tools exploit dcm2niix - [pydra-dcm2bids](https://github.com/aramis-lab/pydra-dcm2bids) supports Pydra tasks for dcm2bids. - [pydra-dcm2niix](https://github.com/nipype/pydra-dcm2niix) is a contains Pydra task interface for dcm2niix. - [qsm](https://github.com/CAIsr/qsm) Quantitative Susceptibility Mapping software. + - [QSMxT](https://github.com/QSMxT/QSMxT) is an end-to-end software toolbox for Quantitative Susceptibility Mapping. - [reproin](https://github.com/ReproNim/reproin) is a setup for automatic generation of shareable, version-controlled BIDS datasets from MR scanners. - [Retina_OCT_dcm2nii](https://github.com/Choupan/Retina_OCT_dcm2nii) converts optical coherence tomography (OCT) data to NIfTI. - [sci-tran dcm2niix](https://github.com/scitran-apps/dcm2niix) Flywheel Gear (docker). diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 209bf3e6..924b4fe0 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -953,6 +953,8 @@ struct TDICOMdata clear_dicom_data() { d.CSA.SeriesHeader_offset = 0; d.CSA.SeriesHeader_length = 0; d.CSA.coilNumber = -1; + strcpy(d.CSA.bidsDataType, ""); + strcpy(d.CSA.bidsEntitySuffix, ""); return d; } //clear_dicom_data() @@ -1464,7 +1466,7 @@ int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, i CSA->coilNumber = csaICEdims(&buff[lPos]); else if (strcmp(tagCSA.name, "NumberOfImagesInMosaic") == 0) CSA->mosaicSlices = (int)round(csaMultiFloat(&buff[lPos], 1, lFloats, &itemsOK)); - else if (strcmp(tagCSA.name, "B_value") == 0) { + else if (strcmp(tagCSA.name, "B_value") == 0) { CSA->dtiV[0] = csaMultiFloat(&buff[lPos], 1, lFloats, &itemsOK); if (CSA->dtiV[0] < 0.0) { printWarning("(Corrupt) CSA reports negative b-value! %g\n", CSA->dtiV[0]); @@ -4401,6 +4403,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D #define kPartialFourier 0x0018 + uint32_t(0x9081 << 16) //CS const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD //#define kDiffusionBFactorSiemens 0x0019+(0x100C<< 16 ) // 0019;000C;SIEMENS MR HEADER;B_value +#define kDiffusionGradientDirectionSQ 0x0018 + uint32_t(0x9076 << 16) // SQ #define kDiffusion_bValue 0x0018 + uint32_t(0x9087 << 16) // FD #define kDiffusionOrientation 0x0018 + uint32_t(0x9089 << 16) // FD, seen in enhanced DICOM from Philips 5.* and Siemens XA10. #define kImagingFrequencyFD 0x0018 + uint32_t(0x9098 << 16) //FD @@ -5052,6 +5055,11 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD vr[1] = 'Q'; lLength = 0; //Do not skip kItemTag - required to determine nesting of Philips Enhanced } + if (groupElement == kDiffusionGradientDirectionSQ) { //https://github.com/rordenlab/dcm2niix/issues/144 + vr[0] = 'S'; + vr[1] = 'Q'; + lLength = 0; //Do not skip kItemTag - required to determine nesting of Philips Enhanced + } } //if explicit else implicit VR if ((lLength == 0xFFFFFFFF) && (vr[0] == 'O') && (vr[1] == 'B') && (isIconImageSequence)) { lLength = 0; //encapuslated data of unspecified length @@ -5894,6 +5902,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD break; dcmStr(lLength, &buffer[lPos], d.pulseSequenceName); if (strstr( d.pulseSequenceName, "epi_pepolar") != NULL) { + //if ((strstr( d.pulseSequenceName, "epi_pepolar") != NULL) || (strstr( d.pulseSequenceName, "epi2_pepolar") != NULL)){ d.epiVersionGE = kGE_EPI_PEPOLAR_FWD; //n.b. combine with 0019,10B3 } else if (strstr( d.pulseSequenceName, "epi2") != NULL) { d.epiVersionGE = kGE_EPI_EPI2; //-1 = not epi, 0 = epi, 1 = epiRT, 2 = epi2 @@ -7116,6 +7125,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD if (d.manufacturer == kMANUFACTURER_GE) { d.CSA.dtiV[0] = (float)set_bValGE(&volDiffusion, lLength, &buffer[lPos]); d.CSA.numDti = 1; + d.isDiffusion = true; } break; case kEpiRTGroupDelayGE: //FL @@ -7500,9 +7510,10 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD } if (isGEfieldMap) { //issue501 : to do check zip factor //Volume 1) derived phase field map [Hz] and 2) magnitude volume. - d.isDerived = (d.imageNum <= locationsInAcquisitionGE); //first volume - d.isRealIsPhaseMapHz = d.isDerived; - d.isHasReal = d.isDerived; + //issue 777 while a fieldmap is technically derived, do not exclude with -i y + bool isDerived = (d.imageNum <= locationsInAcquisitionGE); //first volume + d.isRealIsPhaseMapHz = isDerived; + d.isHasReal = isDerived; } /* SAH.end */ if (locationsInAcquisitionGE < d.locationsInAcquisition) { @@ -7935,8 +7946,13 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD } d.seriesUidCrc = mz_crc32X((unsigned char *)&d.seriesInstanceUID, strlen(d.seriesInstanceUID)); } - if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (strstr(d.sequenceName, "fl2d1") != NULL)) { - d.isLocalizer = true; + if (d.manufacturer == kMANUFACTURER_SIEMENS) { + if (strstr(d.seriesDescription, "AAHScout") != NULL) + d.isLocalizer = true; + if (strstr(d.protocolName, "AAHScout") != NULL) + d.isLocalizer = true; + if (strstr(d.sequenceName, "fl2d1") != NULL) + d.isLocalizer = true; } // detect GE diffusion gradient cycling mode (see issue 635) // GE diffusion epi @@ -7984,6 +8000,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD d.epiVersionGE = kGE_EPI_PEPOLAR_REV_FWD_FLIP; if ((d.epiVersionGE == kGE_EPI_PEPOLAR_FWD_REV) && (volumeNumber > 0) && ((volumeNumber % 2) == 0)) d.epiVersionGE = kGE_EPI_PEPOLAR_FWD_REV_FLIP; + #ifndef myDisableGEPEPolarFlip //e.g. to disable patch for issue 532 "make CFLAGS=-DmyDisableGEPEPolarFlip" if ((d.epiVersionGE == kGE_EPI_PEPOLAR_REV) || (d.epiVersionGE == kGE_EPI_PEPOLAR_FWD_REV_FLIP) || (d.epiVersionGE == kGE_EPI_PEPOLAR_REV_FWD_FLIP)) { if (d.epiVersionGE != kGE_EPI_PEPOLAR_REV) d.seriesNum += 1000; @@ -7997,7 +8014,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD if ((d.manufacturer == kMANUFACTURER_UIH) && (strstr(d.sequenceName, "gre_fsp") != NULL)) d.echoTrainLength = 0; //printf(">>%s\n", d.sequenceName); d.isValid = false; - // Andrey Fedorov has requested keeping GE bvalues, see issue 264 + // Andrey Fedorov has requested keeping GE bvaecues, see issue 264 //if ((d.CSA.numDti > 0) && (d.manufacturer == kMANUFACTURER_GE) && (d.numberOfDiffusionDirectionGE < 1)) // d.CSA.numDti = 0; //https://github.com/rordenlab/dcm2niix/issues/264 if ((!d.isLocalizer) && (isInterpolated) && (d.imageNum <= 1)) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index d9db4dba..0dd79581 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.20230706" +#define kDCMdate "v1.0.20230731" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic @@ -230,6 +230,8 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; float sliceTiming[kMaxEPI3D], dtiV[4], sliceNormV[4], bandwidthPerPixelPhaseEncode, sliceMeasurementDuration; int coilNumber, numDti, SeriesHeader_offset, SeriesHeader_length, multiBandFactor, sliceOrder, slice_start, slice_end, mosaicSlices, protocolSliceNumber1, phaseEncodingDirectionPositive; bool isPhaseMap; + char bidsDataType[kDICOMStr]; //anat, func, dwi + char bidsEntitySuffix[kDICOMStrLarge]; //anat, func, dwi }; struct TDICOMdata { long seriesNum; diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 75472e33..11ba0a1f 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -683,7 +683,7 @@ int phoenixOffsetCSASeriesHeader(unsigned char *buff, int lLength) { #define kMaxWipFree 64 typedef struct { float TE0, TE1, delayTimeInTR, phaseOversampling, phaseResolution, txRefAmp, accelFactTotal; - int phaseEncodingLines, existUcImageNumb, ucMode, baseResolution, interp, partialFourier, echoSpacing, + int lInvContrasts, lContrasts ,phaseEncodingLines, existUcImageNumb, ucMode, baseResolution, interp, partialFourier, echoSpacing, difBipolar, parallelReductionFactorInPlane, refLinesPE, combineMode, patMode, ucMTC, accelFact3D; float alFree[kMaxWipFree]; float adFree[kMaxWipFree]; @@ -709,6 +709,8 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i csaAscii->interp = 0; csaAscii->partialFourier = 0; csaAscii->echoSpacing = 0; + csaAscii->lInvContrasts = 0; + csaAscii->lContrasts = 0; csaAscii->difBipolar = 0; //0=not assigned,1=bipolar,2=monopolar csaAscii->parallelReductionFactorInPlane = 0; csaAscii->accelFact3D = 0;//lAccelFact3D @@ -758,7 +760,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i char keyStr[] = "### ASCCONV BEGIN"; //skip to start of ASCII often "### ASCCONV BEGIN ###" but also "### ASCCONV BEGIN object=MrProtDataImpl@MrProtocolData" char *keyPos = (char *)memmem(bufferTrim, csaLengthTrim, keyStr, strlen(keyStr)); if (keyPos) { - //We could detect multi-echo MPRAGE here, e.g. "lContrasts = 4"- but ideally we want an earlier detection + //We can detect multi-echo MPRAGE here, e.g. "lContrasts = 4"- but ideally we want an earlier detection csaLengthTrim -= (keyPos - bufferTrim); //FmriExternalInfo listed AFTER AscConvEnd and uses different delimiter || // char keyStrExt[] = "FmriExternalInfo"; @@ -772,7 +774,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i #endif char keyStrLns[] = "sKSpace.lPhaseEncodingLines"; csaAscii->phaseEncodingLines = readKey(keyStrLns, keyPos, csaLengthTrim); - char keyStrUcImg[] = "sSliceArray.ucImageNumb"; + char keyStrUcImg[] = "sSliceArray.ucImageNumb"; //some non-mosaics like ToF include "sSliceArray.ucImageNumbSag" csaAscii->existUcImageNumb = readKey(keyStrUcImg, keyPos, csaLengthTrim); char keyStrUcMode[] = "sSliceArray.ucMode"; csaAscii->ucMode = readKeyN1(keyStrUcMode, keyPos, csaLengthTrim); @@ -784,6 +786,10 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i csaAscii->partialFourier = readKey(keyStrPF, keyPos, csaLengthTrim); char keyStrES[] = "sFastImaging.lEchoSpacing"; csaAscii->echoSpacing = readKey(keyStrES, keyPos, csaLengthTrim); + char keyStrNumInv[] = "lInvContrasts"; + csaAscii->lInvContrasts = readKey(keyStrNumInv, keyPos, csaLengthTrim); + char keyStrNumEcho[] = "lContrasts"; + csaAscii->lContrasts = readKey(keyStrNumEcho, keyPos, csaLengthTrim); char keyStrDS[] = "sDiffusion.dsScheme"; csaAscii->difBipolar = readKey(keyStrDS, keyPos, csaLengthTrim); if (csaAscii->difBipolar == 0) { @@ -946,7 +952,7 @@ void siemensCsaAscii(const char *filename, TCsaAscii *csaAscii, int csaOffset, i #define myReadGeProtocolBlock #endif #ifdef myReadGeProtocolBlock -int geProtocolBlock(const char *filename, int geOffset, int geLength, int isVerbose, int *sliceOrder, int *viewOrder, int *mbAccel, int *nSlices, float *groupDelay, char ioptGE[]) { +int geProtocolBlock(const char *filename, int geOffset, int geLength, int isVerbose, int *sliceOrder, int *viewOrder, int *mbAccel, int *nSlices, float *groupDelay, char ioptGE[], char seqStr[]) { *sliceOrder = -1; *viewOrder = 0; *mbAccel = 0; @@ -1038,6 +1044,9 @@ int geProtocolBlock(const char *filename, int geOffset, int geLength, int isVerb readKeyStr(keyStrDELACQ, (char *)pUnCmp, unCmpSz, DELACQ); char keyStrGD[] = "DELACQNOAV"; *groupDelay = readKeyFloat(keyStrGD, (char *)pUnCmp, unCmpSz); + + char keyStrPSEQ[] = "PSEQ"; + readKeyStr(keyStrPSEQ, (char *)pUnCmp, unCmpSz, seqStr); char keyStrIOPT[] = "IOPT"; readKeyStr(keyStrIOPT, (char *)pUnCmp, unCmpSz, ioptGE); char PHASEDELAYS1[10000]; @@ -1615,7 +1624,7 @@ tse3d: T2*/ if (csaAscii.dAveragesDouble > 1.0) //*spcR_44ns fractional and independent of (0018,0083) DS NumberOfAverages, e.g. 0018,0083=2, dAveragesDouble = 1.4? json_Float(fp, "\t\"AveragesDouble\": %g,\n", csaAscii.dAveragesDouble); phaseOversampling = csaAscii.phaseOversampling; - if (csaAscii.existUcImageNumb > 0) { + if ((d.CSA.mosaicSlices > 1) && (csaAscii.existUcImageNumb > 0)) { if (d.CSA.protocolSliceNumber1 < 2) { printWarning("Assuming mosaics saved in reverse order due to 'sSliceArray.ucImageNumb'\n"); //never seen such an image in the wild.... sliceDir may need to be reversed @@ -2148,6 +2157,10 @@ tse3d: T2*/ fprintf(fp, "\t\"InPlanePhaseEncodingDirectionDICOM\": \"COL\",\n"); if (d.phaseEncodingRC == 'R') fprintf(fp, "\t\"InPlanePhaseEncodingDirectionDICOM\": \"ROW\",\n"); + if ((opts.isGuessBidsFilename) && (strlen(d.CSA.bidsDataType)) && (strlen(d.CSA.bidsDataType))) + fprintf(fp, "\t\"BidsGuess\": [\"%s\",\"%s\"],\n",d.CSA.bidsDataType, d.CSA.bidsEntitySuffix); + //json_Str(fp, "\t\"StationName\": \"%s\",\n", d.stationName); + // Finish up with info on the conversion tool fprintf(fp, "\t\"ConversionSoftware\": \"dcm2niix\",\n"); fprintf(fp, "\t\"ConversionSoftwareVersion\": \"%s\"\n", kDCMdate); @@ -3214,6 +3227,51 @@ void cleanISO8859(char *cString) { cString[i] = 'y'; } } + +void createDummyBidsBoilerplate(char *pth, bool isFunc) { + //https://remi-gau.github.io/bids_cookbook/#starters + char pathSep[2] = {"a"}; + pathSep[0] = kPathSeparator; + char descfnm[PATH_MAX] = {""}; + char taskfnm[PATH_MAX] = {""}; + char fnm[PATH_MAX] = {""}; + strcat(fnm, pth); + strcat(fnm, pathSep); + strcat(taskfnm, fnm); + strcat(descfnm, fnm); + snprintf(fnm + strlen(fnm), PATH_MAX - strlen(fnm), "%s", "README.md"); + if (!is_fileexists(fnm)) { + FILE *fp = fopen(fnm, "w"); + static const char readmePre[] = "Generated using dcm2niix ("; + static const char readmePost[] = ")\n\nDescribe your dataset here. This file was generated by dcm2niix in a single pass. Details like IntendedFor, Subject ID, Session and tasks are not defined."; + + if (fp != NULL) + fprintf(fp, readmePre); + fprintf(fp, kDCMdate); + fprintf(fp, readmePost); + fclose(fp); + } + snprintf(descfnm + strlen(descfnm), PATH_MAX - strlen(descfnm), "%s", "dataset_description.json"); + if (!is_fileexists(descfnm)) { + FILE *fp = fopen(descfnm, "w"); + static const char readme[] = "{\n \"Name\": \"dcm2niix dummy dataset\",\n \"Authors\": [\"Chris Rorden\", \"Alex Teghipco\"],\n \"BIDSVersion\": \"1.6.0\"\n}\n"; + if (fp != NULL) + fprintf(fp, readme); + fclose(fp); + } + if (!isFunc) + return; //only functional data gets a task file + snprintf(taskfnm + strlen(taskfnm), PATH_MAX - strlen(taskfnm), "%s", "task-rest_bold.json"); + if (!is_fileexists(taskfnm)) { + FILE *fp = fopen(taskfnm, "w"); + static const char taskRest[] = "{\n\"TaskName\": \"rest\",\n\"CogAtlasID\": \"https://www.cognitiveatlas.org/task/id/trm_4c8a834779883/\"\n}\n"; + if (fp != NULL) + fprintf(fp, taskRest); + fclose(fp); + } + +} + int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts opts) { char pth[PATH_MAX] = {""}; if (strlen(opts.outdir) > 0) { @@ -3249,6 +3307,8 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcpy(inname, "T%t_N%n_S%s"); } const char kTempPathSeparator = '\a'; + char pathSep[2] = {"a"}; + pathSep[0] = kTempPathSeparator; for (size_t pos = 0; pos < strlen(inname); pos++) if ((inname[pos] == '\\') || (inname[pos] == '/')) inname[pos] = kTempPathSeparator; @@ -3291,6 +3351,30 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts strcat(outname, opts.indirParent); if (f == 'G') strcat(outname, dcm.accessionNumber); + if (f == 'H') { + printWarning("hazardous (%%h) bids naming experimental\n"); + createDummyBidsBoilerplate(pth, (strstr(dcm.CSA.bidsDataType, "func") != NULL)); + if (strlen(dcm.CSA.bidsDataType) < 1) { + strcat(outname, "Unknown"); + snprintf(newstr, PATH_MAX, "%c", kTempPathSeparator); + strcat(outname, newstr); + snprintf(newstr, PATH_MAX, "%ld", dcm.seriesNum); + strcat(outname, newstr); + strcat(outname, "_"); + strcat(outname, dcm.protocolName); + + } else { + isAddNamePostFixes = false; + strcat(outname, "sub-1"); + strcat(outname, pathSep); + strcat(outname, dcm.CSA.bidsDataType); + strcat(outname, pathSep); + strcat(outname, "sub-1"); + if (strstr(dcm.CSA.bidsDataType, "func") != NULL) + strcat(outname, "_task-rest"); + strcat(outname, dcm.CSA.bidsEntitySuffix); + } + } if (f == 'I') strcat(outname, dcm.patientID); if (f == 'J') @@ -3369,16 +3453,22 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts if (f == 'V') { if (dcm.manufacturer == kMANUFACTURER_BRUKER) strcat(outname, "Bruker"); + else if (dcm.manufacturer == kMANUFACTURER_CANON) + strcat(outname, "Canon"); else if (dcm.manufacturer == kMANUFACTURER_GE) strcat(outname, "GE"); + else if (dcm.manufacturer == kMANUFACTURER_HYPERFINE) + strcat(outname, "Hyperfine"); + else if (dcm.manufacturer == kMANUFACTURER_MEDISO) + strcat(outname, "Mediso"); + else if (dcm.manufacturer == kMANUFACTURER_MRSOLUTIONS) + strcat(outname, "MRsolutions"); else if (dcm.manufacturer == kMANUFACTURER_PHILIPS) strcat(outname, "Philips"); else if (dcm.manufacturer == kMANUFACTURER_SIEMENS) strcat(outname, "Siemens"); else if (dcm.manufacturer == kMANUFACTURER_TOSHIBA) strcat(outname, "Toshiba"); - else if (dcm.manufacturer == kMANUFACTURER_CANON) - strcat(outname, "Canon"); else if (dcm.manufacturer == kMANUFACTURER_UIH) strcat(outname, "UIH"); else @@ -6320,18 +6410,647 @@ void sliceTimingGE_Testx0021x105E(struct TDICOMdata *d, struct TDCMopts opts, st printMessage("\t%g\t%g\n", d->CSA.sliceTiming[v], sliceTiming[v]); } -void reportProtocolBlockGE(struct TDICOMdata *d, const char *filename) { +void reportProtocolBlockGE(struct TDICOMdata *d, const char *filename, int isVerbose) { #ifdef myReadGeProtocolBlock - if ((d->manufacturer != kMANUFACTURER_GE) || (d->protocolBlockStartGE < 1) || (d->protocolBlockLengthGE < 19)) + if ((d->manufacturer != kMANUFACTURER_GE) || (d->modality != kMODALITY_MR)) + return; + if ((d->protocolBlockStartGE < 1) || (d->protocolBlockLengthGE < 19)) { + //if (isVerbose) + printWarning("Missing GE protocol data block (0025,101B)\n"); return; + } int viewOrderGE = -1; int sliceOrderGE = -1; int mbAccel = -1; int nSlices = -1; float groupDelay = 0.0; char ioptGE[3000] = ""; - geProtocolBlock(filename, d->protocolBlockStartGE, d->protocolBlockLengthGE, 2, &sliceOrderGE, &viewOrderGE, &mbAccel, &nSlices, &groupDelay, ioptGE); + char seqName[kDICOMStr] = ""; + geProtocolBlock(filename, d->protocolBlockStartGE, d->protocolBlockLengthGE, isVerbose, &sliceOrderGE, &viewOrderGE, &mbAccel, &nSlices, &groupDelay, ioptGE, seqName); + if (strlen(d->procedureStepDescription) < 2) + strcpy(d->procedureStepDescription, seqName); #endif +} //bidsGE + +void bidsStr() { // + +} + +void setBidsSiemens(struct TDICOMdata *d, int nConvert, int isVerbose, const char *filename) { + char seqDetails[kDICOMStrLarge] = ""; + float inv1 = NAN; + float inv2 = NAN; + bool isDualTI = false; + int lContrasts = 0; //detect me-mprage + #ifdef myReadAsciiCsa + if ((d->CSA.SeriesHeader_offset > 0) && (d->CSA.SeriesHeader_length > 0)) { + float pf = 1.0f; //partial fourier + float shimSetting[8]; + char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; + TCsaAscii csaAscii; + siemensCsaAscii(filename, &csaAscii, d->CSA.SeriesHeader_offset, d->CSA.SeriesHeader_length, shimSetting, coilID, consistencyInfo, coilElements, seqDetails, fmriExternalInfo, protocolName, wipMemBlock); + inv1 = csaAscii.alTI[0] / 1000.0; + inv2 = csaAscii.alTI[1] / 1000.0; + lContrasts = csaAscii.lContrasts; + //If parameter lInvContrasts exists in the protocol, a value of 1 indicates MPRAGE and a value of 2 MP2RAGE. Note that lInvContrasts is different from lContrasts and that only lInvContrasts must be considered. + //If parameter lInvContrasts does not exist, then the presence of alTI[1] indicates that this is an MP2RAGE protocol. An MPRAGE protocol will only contain alTI[0]. + if (csaAscii.lInvContrasts == 1) //explicitly reports one TI + inv2 = NAN; + if ((!isnan(inv1)) && (!isnan(inv2)) && (inv1 > 0.0) && (inv2 > 0.0)) + isDualTI = true; + + } + #endif // myReadAsciiCsa + char *dataTypeBIDS = d->CSA.bidsDataType; + strcpy(dataTypeBIDS, ""); + char *suffixBIDS = d->CSA.bidsEntitySuffix; + strcpy(suffixBIDS, ""); + char modalityBIDS[kDICOMStrLarge] = ""; //T1w, bold, dwi + char recBIDS[kDICOMStrLarge] = ""; //_desc-preproc + if (d->manufacturer != kMANUFACTURER_SIEMENS) + return; + bool isReportEcho = true; //do not report _echo for PD/T2 pair or fieldmap + bool isMultiEcho = false; + bool isDerived = d->isDerived; //report phase encoding direction + bool isDirLabel = false; + bool isPart = false; + bool isAddSeriesToRun = true; //except phasemap, where phasediff and magnitude have different series numbers but must share acq + char seqName[kDICOMStrLarge]; + strcpy(seqName, d->sequenceName); + if (strlen(d->sequenceName) < 2) //e.g. XA uses 0018,9005 while VE uses 0018,0024 + strcpy(seqName, d->pulseSequenceName); + if (strlen(seqDetails) < 2) + strcpy(seqDetails, seqName); + if (strstr(d->imageType, "DERIVED") != NULL) { + isDerived = true; //to do: respond to derived images + } + if (d->modality != kMODALITY_MR) + return; + if (((d->xyzDim[3] < 2) && (nConvert < 1)) || (d->isLocalizer)) { //need nConvert or nifti header + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "localizer"); + } else if ((strstr(seqDetails, "tfl") != NULL) || (strstr(seqDetails, "mp2rage") != NULL) || (strstr(seqDetails, "wip925") != NULL)) { //prog_mprage + strcpy(dataTypeBIDS, "anat"); + if (isDualTI) + strcpy(modalityBIDS, "MP2RAGE"); + else { //bizarrely mprage can populate both alTI[0] alTI[1] see dcm_qa_xa30 + strcpy(modalityBIDS, "T1w"); + //bork inv2 = NAN; + } + if (strstr(d->imageType, "T1 MAP") != NULL) + strcpy(modalityBIDS, "T1map"); + if (strstr(d->imageType, "_UNI") != NULL) { + strcpy(modalityBIDS, "UNIT1"); + if (strstr(d->imageComments, "DENOISED IMAGE") != NULL) + strcat(recBIDS, "denoise"); + } + } else if ((d->CSA.numDti > 0) || (strstr(seqDetails, "_diff") != NULL) || (strstr(seqDetails, "resolve") != NULL) || (strstr(seqDetails, "PtkSmsVB13ADwDualSpinEchoEpi") != NULL) || (strstr(seqDetails, "ep2d_stejskal_386") != NULL)) { //prog_diff + strcpy(dataTypeBIDS, "dwi"); + strcpy(modalityBIDS, "dwi"); + if (strstr(d->seriesDescription, "_SBRef") != NULL) + strcpy(modalityBIDS, "sbref"); + //if seriesDesc trace", "fa", "adc" isDerived = true; + isDirLabel = true; + } else if ((strstr(seqDetails, "_asl") != NULL) || (strstr(seqDetails, "_pasl") != NULL) || (strstr(seqDetails, "pcasl") != NULL) || (strstr(seqDetails, "PCASL") != NULL)) { //prog_asl + strcpy(dataTypeBIDS, "perf"); + strcpy(modalityBIDS, "asl"); + if (strstr(d->seriesDescription, "_m0") != NULL) + strcpy(modalityBIDS, "m0scan"); + } else if (strstr(d->pulseSequenceName, "spcR") != NULL) { + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "T2w"); + } else if (strstr(seqDetails, "tse_vfl") != NULL) { //prog_tse_vfl + strcpy(dataTypeBIDS, "anat"); + if ((strstr(seqDetails, "spcir") != NULL) || (strstr(d->sequenceName, "spcir") != NULL)) + strcpy(modalityBIDS, "FLAIR"); + else + strcpy(modalityBIDS, "T2w"); + } else if (strstr(seqDetails, "tse") != NULL) { //prog_tse + isReportEcho = false; //do not report _echo for T2/PDw pair + strcpy(dataTypeBIDS, "anat"); + if (strstr(d->sequenceName, "tir") != NULL) + strcpy(modalityBIDS, "FLAIR"); + if ((strstr(d->sequenceName, "tse2d") != NULL) || (strstr(d->pulseSequenceName, "tse2d") != NULL)) { + if (d->TE < 50) + strcpy(modalityBIDS, "PDw"); + else + strcpy(modalityBIDS, "T2w"); + } + } else if ((strstr(seqDetails, "ep2d_ase") != NULL)) { //prog_ep2d_se + // oxygen extraction fraction(OEF) Asymmetric Spin Echo (ASE) + //printWarning("BIDS does not yet specify `ase` data\n"); + //n.b. we do not specify dataTypeBIDS as not yet defined + strcpy(modalityBIDS, "oef_ase"); + } else if ((strstr(seqDetails, "ep2d_se") != NULL)) { //prog_ep2d_se + strcpy(dataTypeBIDS, "fmap"); + strcpy(modalityBIDS, "epi"); + isDirLabel = true; + } else if ((strstr(seqDetails, "gre_field_mapping") != NULL)) { //prog_fmap + isReportEcho = false; //echo encoded in "_magnitude" + isAddSeriesToRun = false; + strcpy(dataTypeBIDS, "fmap"); + if (d->isHasPhase) + strcpy(modalityBIDS, "phasediff"); + if (d->isHasMagnitude) + snprintf(modalityBIDS, kDICOMStrLarge - strlen(modalityBIDS), "magnitude%d", d->echoNum); + //printf("magnitude%d\n", d->echoNum); + //} else if (( seqName,"_tfl2d1") == 0) || (strcmp(dcmList[indx].sequenceName, "_fl3d1_ns") == 0) || (strcmp(dcmList[indx].sequenceName, "_fl2d1" + } else if ((strstr(seqDetails, "\\trufi") != NULL) || (strstr(seqName, "fl3d1_ns") != NULL) || (strstr(seqName, "fl2d1") != NULL) || (strstr(seqName, "tfl2d1") != NULL)) { //localizers + //seqDetails borg + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "localizer"); + } else if ((strstr(seqName, "fl3d1r") != NULL) || (strstr(seqName, "fl2d1") != NULL) || (strstr(seqName, "tfl2d1") != NULL)) { //localizers + //nb ToF fl3d1r_ts fl3d1r_t70 fl3d1r7t but check for fl3d1r SWI + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "angio"); + } else if (strstr(seqDetails, "ep_seg_fid") != NULL) { + //n.b. large echoTrainLength even for single echo acquition + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "T2starw"); + isPart = true; + if (strstr(d->seriesDescription, "mIP") != NULL) { //derived minimum intensity + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "mIP"); + } + if (strstr(d->seriesDescription, "SWI_Images") != NULL) { //derived SWI + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "SWI_Images"); + } + } else if (strstr(seqDetails, "gre") != NULL) { //prog_gre + //printf("GRE T2starw or if MEGRE\n"); + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "T2starw"); + if ((d->echoNum > 1) || (lContrasts > 1)) { + //borg: ETL is not a good way to detect echoes: + strcpy(modalityBIDS, "MEGRE"); + isMultiEcho = true; + } + isPart = true; + } else if ((strstr(seqDetails, "AALScout") != NULL) || (strstr(seqDetails, "haste") != NULL)) { //localizer: unused + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "localizer"); + } else if ((strstr(seqDetails, "_bold") != NULL) || (strstr(seqDetails, "pace") != NULL)){ //prog_bold + //n.b. "Space" is not "pace" + strcpy(dataTypeBIDS, "func"); + strcpy(modalityBIDS, "bold"); + isDirLabel = true; + if (strstr(d->seriesDescription, "_SBRef") != NULL) + strcpy(modalityBIDS, "sbref"); + } else if ((strstr(d->imageType, "FMRI") != NULL) || (strstr(seqDetails, "ep2d_fid") != NULL)) { + //Hail mary: one Skyra XA30 ADNI rsfMRI_LR 20230109 has no CSA + strcpy(dataTypeBIDS, "func"); + strcpy(modalityBIDS, "bold"); + isDirLabel = true; + } else if (strstr(d->sequenceName, "*epse2d") != NULL) { + //pepolar? + strcpy(dataTypeBIDS, "fmap"); + strcpy(modalityBIDS, "epi"); + isDirLabel = true; + } + char acqStr[kDICOMStrLarge] = ""; + strcat(acqStr, "_acq-"); + int len = strlen(seqName); + if (len > 0) { + for (int i = 0; i < len; i++) { + char ch = seqName[i]; + if (ch == '*') continue; + if ((ch >= '0' & ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) + strncat(acqStr, &ch, 1); + if (isdigit(ch)) break; + } + } //len > 0 + if (d->accelFactPE > 1) + snprintf(acqStr + strlen(acqStr), kDICOMStrLarge - strlen(acqStr), "p%d", (int)round(d->accelFactPE)); + if (d->CSA.multiBandFactor > 1) + snprintf(acqStr + strlen(acqStr), kDICOMStrLarge - strlen(acqStr), "m%d", (int)round(d->CSA.multiBandFactor)); + strcat(suffixBIDS, acqStr); + //add _rec + // https://bids-specification.readthedocs.io/en/stable/05-derivatives/02-common-data-types.html#common-file-level-metadata-fields + if (strlen(recBIDS) > 0) { + strcat(suffixBIDS, "_rec-"); + strcat(suffixBIDS, recBIDS); + } + //add dir + //nb for func dir comes before echo + if (isDirLabel) { + char dirLabel[kDICOMStrLarge] = "_dir-"; + if (d->phaseEncodingRC == 'C') { + if (d->CSA.phaseEncodingDirectionPositive) + strcat(dirLabel, "AP"); + else + strcat(dirLabel, "PA"); + } else { + if (d->CSA.phaseEncodingDirectionPositive) + strcat(dirLabel, "RL"); + else + strcat(dirLabel, "LR"); + } + strcat(suffixBIDS, dirLabel); + } + //add run + if (isAddSeriesToRun) + snprintf(suffixBIDS + strlen(suffixBIDS), kDICOMStrLarge - strlen(suffixBIDS), "_run-%ld", d->seriesNum); + //add echo + if (isReportEcho) + if ((d->echoNum > 1) || (isMultiEcho) || ((d->isMultiEcho) && (d->echoNum > 0))) + snprintf(suffixBIDS + strlen(suffixBIDS), kDICOMStrLarge - strlen(suffixBIDS), "_echo-%d", d->echoNum); + //add inv + // https://bids-specification.readthedocs.io/en/stable/appendices/entities.html#inv + if (isDualTI) { + //n.b. Siemens uses alTI[0]/alTI[1] for inversion times: alTI[2] used by ASL + int invIdx = 1; + if (isSameFloatGE(d->TI, inv2)) + invIdx = 2; + snprintf(suffixBIDS + strlen(suffixBIDS), kDICOMStrLarge - strlen(suffixBIDS), "_inv-%d", invIdx); + } + //add part + if (isPart) { + if (d->isHasPhase) + strcat(suffixBIDS, "_part-phase"); + if (d->isHasMagnitude) + strcat(suffixBIDS, "_part-mag"); + } + if (strlen(modalityBIDS) > 0) { + strcat(suffixBIDS, "_"); + strcat(suffixBIDS, modalityBIDS); + } + if ((isVerbose > 0) || (strlen(dataTypeBIDS) < 1)) + printf("::autoBids:Siemens CSAseqFname:'%s' pulseSeq:'%s' seqName:'%s'\n", + seqDetails, d->pulseSequenceName, d->sequenceName); + if (isDerived) + strcpy(dataTypeBIDS, "derived"); +} // setBidsSiemens() + +void setBidsPhilips(struct TDICOMdata *d, int nConvert, int isVerbose) { + if (d->manufacturer != kMANUFACTURER_PHILIPS) + return; + char *dataTypeBIDS = d->CSA.bidsDataType; + strcpy(dataTypeBIDS, ""); + char *suffixBIDS = d->CSA.bidsEntitySuffix; + strcpy(suffixBIDS, ""); + if (d->modality != kMODALITY_MR) + return; + char seqName[kDICOMStr] = ""; + strcpy(seqName, d->sequenceVariant); + char modalityBIDS[kDICOMStrLarge] = ""; //_acq-FL3p2m2 + bool isReportEcho = true; + bool isDirLabel = false; //report phase encoding direction + bool isDerived = d->isDerived; //report phase encoding direction + bool isAddSeriesToRun = true; + bool isPart = false; + //d->pulseSequenceName, d->scanningSequence, d->sequenceVariant) + if (((d->xyzDim[3] < 4) && (nConvert < 4)) || (d->isLocalizer)) { + strcpy(dataTypeBIDS, "discard"); + strcpy(modalityBIDS, "localizer"); + } else if (strstr(seqName, "MP") != NULL) { + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "T1w"); + } else if ((d->isDiffusion) && (strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "SE") != NULL) ) { + strcpy(dataTypeBIDS, "dwi"); + strcpy(modalityBIDS, "dwi"); + } else if (strstr(d->imageType, "PERFUSION") != NULL) { + //scanSeq:'GR' seqVariant:'SK' + strcpy(dataTypeBIDS, "perf"); + strcpy(modalityBIDS, "asl"); + } else if ((strstr(d->pulseSequenceName, "SEEPI") != NULL) && (!d->isDiffusion) && (strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "SE") != NULL) ) { + //pepolar? d->pulseSequenceName + isAddSeriesToRun = false; + strcpy(dataTypeBIDS, "fmap"); + strcpy(modalityBIDS, "epi"); + printWarning("Unable to estimate BIDS `_dir` for fmap epi as Philips DICOMs do not report phase encoding polarity\n"); + isDirLabel = true; + } else if ((!d->isDiffusion) && (strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "SE") != NULL) ) { + strcpy(dataTypeBIDS, "anat"); + if (false)//((strstr(d->scanningSequence, "IR") != NULL)) + strcpy(modalityBIDS, "FLAIR"); + else if (d->TE < 40) + strcpy(modalityBIDS, "PDw"); + else + strcpy(modalityBIDS, "T2w"); + } else if ((strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "IR") != NULL) ) { + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "FLAIR"); + } else if ((strstr(d->imageType, "PERFUSION") != NULL) && (strstr(d->pulseSequenceName, "FEEPI") != NULL) && (strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "GR") != NULL) ) { + strcpy(dataTypeBIDS, "perf"); + strcpy(modalityBIDS, "asl"); + } else if (((d->rawDataRunNumber >= 1) || (strstr(d->pulseSequenceName, "FEEPI") != NULL)) && (strstr(seqName, "SK") != NULL) && (strstr(d->scanningSequence, "GR") != NULL) ) { + //nb older Philips data does not record EPI so use 2005,1063 fMRIStatusIndication + strcpy(dataTypeBIDS, "func"); + strcpy(modalityBIDS, "bold"); + isDirLabel = true; + } else if ((strstr(seqName, "SS") != NULL) && (strstr(d->scanningSequence, "GR") != NULL) ) { + strcpy(dataTypeBIDS, "anat"); + strcpy(modalityBIDS, "T2starw"); + isPart = true; + } else if ((d->isRealIsPhaseMapHz) && (strstr(seqName, "SS") != NULL) && (strstr(d->scanningSequence, "RM") != NULL)) { + //https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#expressing-the-mr-protocol-intent-for-fieldmaps + //isRealIsPhaseMapHz for 'Case 3: Direct field mapping' + //sub-