diff -Nru dcm2niix-1.0.20180622/console/jpg_0XC3.cpp dcm2niix-1.0.20181125/console/jpg_0XC3.cpp --- dcm2niix-1.0.20180622/console/jpg_0XC3.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/jpg_0XC3.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -142,6 +142,7 @@ uint16_t SOFxdim = 0; // long SOSarrayPos; //SOFarrayPos int lnHufTables = 0; + int lFrameCount = 1; const int kmaxFrames = 4; struct HufTables l[kmaxFrames+1]; do { //read each marker in the header @@ -180,7 +181,6 @@ } } else if (btMarkerType == 0xC4) {//if SOF marker else if define-Huffman-tables marker (DHT) if (verbose) printMessage(" [Huffman Length %d]\n", lSegmentLength); - int lFrameCount = 1; do { uint8_t DHTnLi = readByte(lRawRA, &lRawPos, lRawSz ); //we read but ignore DHTtcth. #pragma unused(DHTnLi) //we need to increment the input file position, but we do not care what the value is @@ -325,8 +325,9 @@ } //For each frame, e.g. once each for Red/Green/Blue //NEXT: some RGB images use only a single Huffman table for all 3 colour planes. In this case, replicate the correct values if (lnHufTables < SOFnf) { //use single Hufman table for each frame - for (int lFrameCount = 2; lFrameCount <= SOFnf; lFrameCount++) { - l[lFrameCount] = l[1]; + for (int lFrameCount = lnHufTables+1; lFrameCount <= SOFnf; lFrameCount++) { + l[lFrameCount] = l[lnHufTables]; + } //for each frame } // if lnHufTables < SOFnf //NEXT: uncompress data: different loops for different predictors @@ -503,4 +504,4 @@ if (verbose) printMessage("JPEG ends %ld@%ld\n", lRawPos, lRawPos+skipBytes); return lImgRA8; -}// decode_JPEG_SOF_0XC3() \ No newline at end of file +}// decode_JPEG_SOF_0XC3() diff -Nru dcm2niix-1.0.20180622/console/main_console.cpp dcm2niix-1.0.20181125/console/main_console.cpp --- dcm2niix-1.0.20180622/console/main_console.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/main_console.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -85,7 +85,7 @@ #else #define kQstr "" #endif - printf(" -f : filename (%%a=antenna (coil) number, %%c=comments, %%d=description, %%e=echo number, %%f=folder name, %%i=ID of patient, %%j=seriesInstanceUID, %%k=studyInstanceUID, %%m=manufacturer, %%n=name of patient, %%p=protocol,%s %%s=series number, %%t=time, %%u=acquisition number, %%v=vendor, %%x=study ID; %%z=sequence name; default '%s')\n", kQstr, opts.filename); + printf(" -f : filename (%%a=antenna (coil) name, %%b=basename, %%c=comments, %%d=description, %%e=echo number, %%f=folder name, %%i=ID of patient, %%j=seriesInstanceUID, %%k=studyInstanceUID, %%m=manufacturer, %%n=name of patient, %%p=protocol,%s %%r=instance number, %%s=series number, %%t=time, %%u=acquisition number, %%v=vendor, %%x=study ID; %%z=sequence name; default '%s')\n", kQstr, opts.filename); printf(" -g : generate defaults file (y/n/o/i [o=only: reset and write defaults; i=ignore: reset defaults], default n)\n"); printf(" -h : show help\n"); printf(" -i : ignore derived, localizer and 2D images (y/n, default n)\n"); @@ -96,6 +96,7 @@ printf(" -n : only convert this series number - can be used up to %i times (default convert all)\n", MAX_NUM_SERIES); printf(" -o : output directory (omit to save to input folder)\n"); printf(" -p : Philips precise float (not display) scaling (y/n, default y)\n"); + printf(" -r : rename instead of convert DICOMs (y/n, default n)\n"); printf(" -s : single file mode, do not convert other images in folder (y/n, default n)\n"); printf(" -t : text notes includes private patient details (y/n, default n)\n"); #if !defined(_WIN64) && !defined(_WIN32) //shell script for Unix only @@ -273,7 +274,7 @@ isSaveIni = true; if (((argv[i][0] == 'i') || (argv[i][0] == 'I')) && (!isResetDefaults)) { isResetDefaults = true; - printf("Defaults reset\n"); + printf("Defaults ignored\n"); setDefaultOpts(&opts, argv); i = 0; //re-read all settings for this pass, e.g. "dcm2niix -f %p_%s -d o" should save filename as "%p_%s" } @@ -314,7 +315,11 @@ opts.isPhilipsFloatNotDisplayScaling = false; else opts.isPhilipsFloatNotDisplayScaling = true; - + } else if ((argv[i][1] == 'r') && ((i+1) < argc)) { + i++; + if (invalidParam(i, argv)) return 0; + if ((argv[i][0] == 'y') || (argv[i][0] == 'Y')) + opts.isRenameNotConvert = true; } else if ((argv[i][1] == 's') && ((i+1) < argc)) { i++; if (invalidParam(i, argv)) return 0; diff -Nru dcm2niix-1.0.20180622/console/nifti1_io_core.cpp dcm2niix-1.0.20181125/console/nifti1_io_core.cpp --- dcm2niix-1.0.20180622/console/nifti1_io_core.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nifti1_io_core.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -31,7 +31,7 @@ #include "print.h" -#ifndef HAVE_R +#ifndef USING_R void nifti_swap_8bytes( size_t n , void *ar ) // 4 bytes at a time { size_t ii ; @@ -192,7 +192,7 @@ return Q44; } -#ifndef HAVE_R +#ifndef USING_R float nifti_mat33_determ( mat33 R ) /* determinant of 3x3 matrix */ { double r11,r12,r13,r21,r22,r23,r31,r32,r33 ; @@ -239,7 +239,7 @@ return B; } -#ifndef HAVE_R +#ifndef USING_R mat33 nifti_mat33_inverse( mat33 R ) /* inverse of 3x3 matrix */ { double r11,r12,r13,r21,r22,r23,r31,r32,r33 , deti ; diff -Nru dcm2niix-1.0.20180622/console/nifti1_io_core.h dcm2niix-1.0.20181125/console/nifti1_io_core.h --- dcm2niix-1.0.20180622/console/nifti1_io_core.h 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nifti1_io_core.h 2018-11-25 21:24:07.000000000 +0000 @@ -4,7 +4,7 @@ #ifndef _NIFTI_IO_CORE_HEADER_ #define _NIFTI_IO_CORE_HEADER_ -#ifdef HAVE_R +#ifdef USING_R #define STRICT_R_HEADERS #include "RNifti.h" #endif @@ -17,7 +17,7 @@ #include -#ifndef HAVE_R +#ifndef USING_R typedef struct { /** 4x4 matrix struct **/ float m[3][3] ; } mat33 ; diff -Nru dcm2niix-1.0.20180622/console/nii_dicom_batch.cpp dcm2niix-1.0.20181125/console/nii_dicom_batch.cpp --- dcm2niix-1.0.20180622/console/nii_dicom_batch.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_dicom_batch.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -29,7 +29,7 @@ #include "tinydir.h" #include "print.h" #include "nifti1_io_core.h" -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif #include "nii_dicom_batch.h" @@ -64,7 +64,7 @@ const char kFileSep[2] = "/"; #endif -#ifdef HAVE_R +#ifdef USING_R #include "ImageList.h" #undef isnan @@ -111,22 +111,6 @@ path[len] = '\0'; } -void getFileName( char *pathParent, const char *path) //if path is c:\d1\d2 then filename is 'd2' -{ - const char *filename = strrchr(path, '/'); //UNIX - if (filename == 0) { - filename = strrchr(path, '\\'); //Windows - if (filename == NULL) filename = strrchr(path, ':'); //Windows - } - //const char *filename = strrchr(path, kPathSeparator); //x - if (filename == NULL) {//no path separator - strcpy(pathParent,path); - return; - } - filename++; - strcpy(pathParent,filename); -} - bool is_fileexists(const char * filename) { FILE * fp = NULL; if ((fp = fopen(filename, "r"))) { @@ -234,16 +218,29 @@ printMessage("Saving %d DTI gradients. GE Reorienting %s : please validate. isCol=%d sliceDir=%d flp=%d %d %d\n", d->CSA.numDti, d->protocolName, col, sliceDir, flp.v[0], flp.v[1],flp.v[2]); if (!col) printMessage(" reorienting for ROW phase-encoding untested.\n"); + bool scaledBValWarning = false; for (int i = 0; i < d->CSA.numDti; i++) { float vLen = sqrt( (vx[i].V[1]*vx[i].V[1]) + (vx[i].V[2]*vx[i].V[2]) + (vx[i].V[3]*vx[i].V[3])); - if ((vx[i].V[0] <= FLT_EPSILON)|| (vLen <= FLT_EPSILON) ) { //bvalue=0 + if ((vx[i].V[0] <= FLT_EPSILON)|| (vLen <= FLT_EPSILON) ) { //bvalue=0 for (int v= 1; v < 4; v++) vx[i].V[v] = 0.0f; continue; //do not normalize or reorient 0 vectors } - if (!col) { //rows need to be swizzled + if ((vLen > 0.03) && (vLen < 0.97)) { + //bVal scaled by norm(g)^2 https://github.com/rordenlab/dcm2niix/issues/163 + float bVal = vx[i].V[0] * (vLen * vLen); + if (!scaledBValWarning) { + printMessage("GE BVal scaling (e.g. %g -> %g s/mm^2)\n", vx[i].V[0], bVal); + scaledBValWarning = true; + } + vx[i].V[0] = bVal; + vx[i].V[1] = vx[i].V[1]/vLen; + vx[i].V[2] = vx[i].V[2]/vLen; + vx[i].V[3] = vx[i].V[3]/vLen; + } + if (!col) { //rows need to be swizzled //see Stanford dataset Ax_DWI_Tetrahedral_7 unable to resolve between possible solutions // http://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging float swap = vx[i].V[1]; @@ -274,13 +271,23 @@ //convert DTI vectors from scanner coordinates to image frame of reference //Uses 6 orient values from ImageOrientationPatient (0020,0037) // requires PatientPosition 0018,5100 is HFS (head first supine) - if ((d->manufacturer != kMANUFACTURER_SIEMENS) && (d->manufacturer != kMANUFACTURER_PHILIPS)) return; + if ((d->manufacturer != kMANUFACTURER_UIH) && (d->manufacturer != kMANUFACTURER_SIEMENS) && (d->manufacturer != kMANUFACTURER_PHILIPS)) return; if (d->CSA.numDti < 1) return; + if (d->manufacturer == kMANUFACTURER_UIH) { + for (int i = 0; i < d->CSA.numDti; i++) { + vx[i].V[2] = -vx[i].V[2]; + for (int v= 0; v < 4; v++) + if (vx[i].V[v] == -0.0f) vx[i].V[v] = 0.0f; //remove sign from values that are virtually zero + } + //for (int i = 0; i < 3; i++) + // printf("%g %g %g\n", vx[i].V[1], vx[i].V[2], vx[i].V[3]); + return; + } //https://github.com/rordenlab/dcm2niix/issues/225 if ((toupper(d->patientOrient[0])== 'H') && (toupper(d->patientOrient[1])== 'F') && (toupper(d->patientOrient[2])== 'S')) ; //participant was head first supine else { - printMessage("Siemens/Philips DTI directions require head first supine acquisition\n"); - return; + printMessage("Check Siemens/Philips bvecs: expected Patient Position (0018,5100) to be 'HFS' not '%s'\n", d->patientOrient); + //return; //see https://github.com/rordenlab/dcm2niix/issues/238 } vec3 read_vector = setVec3(d->orient[1],d->orient[2],d->orient[3]); vec3 phase_vector = setVec3(d->orient[4],d->orient[5],d->orient[6]); @@ -293,10 +300,8 @@ + (vx[i].V[2]*vx[i].V[2]) + (vx[i].V[3]*vx[i].V[3])); if ((vx[i].V[0] <= FLT_EPSILON)|| (vLen <= FLT_EPSILON) ) { //bvalue=0 - if (vx[i].V[0] > FLT_EPSILON) - printWarning("Volume %d appears to be an ADC map (non-zero b-value with zero vector length)\n", i); - //for (int v= 0; v < 4; v++) - // vx[i].V[v] =0.0f; + if (vx[i].V[0] > 5.0) //Philip stores n.b. UIH B=1.25126 Vec=0,0,0 while Philips stored isotropic images + printWarning("Volume %d appears to be derived image ADC/Isotropic (non-zero b-value with zero vector length)\n", i); continue; //do not normalize or reorient b0 vectors }//if bvalue=0 vec3 bvecs_old =setVec3(vx[i].V[1],vx[i].V[2],vx[i].V[3]); @@ -311,13 +316,10 @@ } //for each direction if (abs(sliceDir) == kSliceOrientMosaicNegativeDeterminant) { printWarning("Saving %d DTI gradients. Validate vectors (matrix had a negative determinant).\n", d->CSA.numDti); //perhaps Siemens sagittal - } else if ( d->sliceOrient == kSliceOrientTra) { + } else if (( d->sliceOrient == kSliceOrientTra) || (d->manufacturer != kMANUFACTURER_PHILIPS)) { printMessage("Saving %d DTI gradients. Validate vectors.\n", d->CSA.numDti); - } else if ( d->sliceOrient == kSliceOrientUnknown) { + } else if ( d->sliceOrient == kSliceOrientUnknown) printWarning("Saving %d DTI gradients. Validate vectors (image slice orientation not reported, e.g. 2001,100B).\n", d->CSA.numDti); - } else { - printWarning("Saving %d DTI gradients. Validate vectors (images are not axial slices).\n", d->CSA.numDti); - } }// siemensPhilipsCorrectBvecs() bool isNanPosition(struct TDICOMdata d) { //in 2007 some Siemens RGB DICOMs did not include the PatientPosition 0020,0032 tag @@ -345,7 +347,7 @@ fprintf(fp, "%s\tField Strength:\t%g\tProtocolName:\t%s\tScanningSequence00180020:\t%s\tTE:\t%g\tTR:\t%g\tSeriesNum:\t%ld\tAcquNum:\t%d\tImageNum:\t%d\tImageComments:\t%s\tDateTime:\t%f\tName:\t%s\tConvVers:\t%s\tDoB:\t%s\tGender:\t%c\tAge:\t%s\tDimXYZT:\t%d\t%d\t%d\t%d\tCoil:\t%d\tEchoNum:\t%d\tOrient(6)\t%g\t%g\t%g\t%g\t%g\t%g\tbitsAllocated\t%d\tInputName\t%s\n", pathoutname, d.fieldStrength, d.protocolName, d.scanningSequence, d.TE, d.TR, d.seriesNum, d.acquNum, d.imageNum, d.imageComments, d.dateTime, d.patientName, kDCMvers, d.patientBirthDate, d.patientSex, d.patientAge, h->dim[1], h->dim[2], h->dim[3], h->dim[4], - d.coilNum,d.echoNum, d.orient[1], d.orient[2], d.orient[3], d.orient[4], d.orient[5], d.orient[6], + d.coilCrc,d.echoNum, d.orient[1], d.orient[2], d.orient[3], d.orient[4], d.orient[5], d.orient[6], d.bitsAllocated, dcmname); fclose(fp); }// nii_SaveText() @@ -409,28 +411,6 @@ //n.b. memchr returns "const void *" not "void *" for Windows C++ https://msdn.microsoft.com/en-us/library/d7zdhf37.aspx #endif //for systems without memmem -/*int readKeyX(const char * key, char * buffer, int remLength) { //look for text key in binary data stream, return subsequent integer value - int ret = 0; - char *keyPos = (char *)memmem(buffer, remLength, key, strlen(key)); - printWarning("<><><>\n"); - if (!keyPos) return 0; - printWarning("<><> %d\n", strlen(keyPos)); - int i = (int)strlen(key); - int numDigits = 0; - while( ( i< remLength) && (numDigits >= 0) ) { - printMessage("%c", keyPos[i]); - if( keyPos[i] >= '0' && keyPos[i] <= '9' ) { - ret = (10 * ret) + keyPos[i] - '0'; - numDigits ++; - } else if (numDigits > 0) - numDigits = -1; - i++; - } - printWarning("---> %d\n", ret); - return ret; -} //readKey() -*/ - int readKey(const char * key, char * buffer, int remLength) { //look for text key in binary data stream, return subsequent integer value int ret = 0; char *keyPos = (char *)memmem(buffer, remLength, key, strlen(key)); @@ -474,7 +454,7 @@ tmpstr[1] = 0; bool isQuote = false; while( ( i < remLength) && (keyPos[i] != 0x0A) ) { - if ((isQuote) && (keyPos[i] != '"') && (outLen < kDICOMStr)) { + if ((isQuote) && (keyPos[i] != '"') && (outLen < kDICOMStrLarge)) { tmpstr[0] = keyPos[i]; strcat (outStr, tmpstr); outLen ++; @@ -517,7 +497,7 @@ return 0; } // phoenixOffsetCSASeriesHeader() -void siemensCsaAscii(const char * filename, int csaOffset, int csaLength, float* delayTimeInTR, float* phaseOversampling, float* phaseResolution, float* txRefAmp, float* shimSetting, int* baseResolution, int* interp, int* partialFourier, int* echoSpacing, int* parallelReductionFactorInPlane, char* coilID, char* consistencyInfo, char* coilElements, char* pulseSequenceDetails, char* fmriExternalInfo, char * protocolName) { +void siemensCsaAscii(const char * filename, int csaOffset, int csaLength, float* delayTimeInTR, float* phaseOversampling, float* phaseResolution, float* txRefAmp, float* shimSetting, int* baseResolution, int* interp, int* partialFourier, int* echoSpacing, int* parallelReductionFactorInPlane, char* coilID, char* consistencyInfo, char* coilElements, char* pulseSequenceDetails, char* fmriExternalInfo, char* protocolName, char* wipMemBlock) { //reads ASCII portion of CSASeriesHeaderInfo and returns lEchoTrainDuration or lEchoSpacing value // returns 0 if no value found *delayTimeInTR = 0.0; @@ -535,6 +515,7 @@ strcpy(coilElements, ""); strcpy(pulseSequenceDetails, ""); strcpy(fmriExternalInfo, ""); + strcpy(wipMemBlock, ""); strcpy(protocolName, ""); if ((csaOffset < 0) || (csaLength < 8)) return; FILE * pFile = fopen ( filename, "rb" ); @@ -563,10 +544,16 @@ if (keyPos) { //We could 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"; + // readKeyStr(keyStrExt, keyPos, csaLengthTrim, fmriExternalInfo); + #define myCropAtAscConvEnd + #ifdef myCropAtAscConvEnd char keyStrEnd[] = "### ASCCONV END"; char *keyPosEnd = (char *)memmem(keyPos, csaLengthTrim, keyStrEnd, strlen(keyStrEnd)); if ((keyPosEnd) && ((keyPosEnd - keyPos) < csaLengthTrim)) //ignore binary data at end csaLengthTrim = (int)(keyPosEnd - keyPos); + #endif char keyStrES[] = "sFastImaging.lEchoSpacing"; *echoSpacing = readKey(keyStrES, keyPos, csaLengthTrim); char keyStrBase[] = "sKSpace.lBaseResolution"; @@ -589,8 +576,8 @@ readKeyStr(keyStrCS, keyPos, csaLengthTrim, coilElements); char keyStrSeq[] = "tSequenceFileName"; readKeyStr(keyStrSeq, keyPos, csaLengthTrim, pulseSequenceDetails); - char keyStrExt[] = "FmriExternalInfo"; - readKeyStr(keyStrExt, keyPos, csaLengthTrim, fmriExternalInfo); + char keyStrWipMemBlock[] = "sWipMemBlock.tFree"; + readKeyStr(keyStrWipMemBlock, keyPos, csaLengthTrim, wipMemBlock); char keyStrPn[] = "tProtocolName"; readKeyStr(keyStrPn, keyPos, csaLengthTrim, protocolName); char keyStrDelay[] = "lDelayTimeInTR"; @@ -720,6 +707,7 @@ return EXIT_SUCCESS; } #endif //myReadGeProtocolBlock() + void json_Str(FILE *fp, const char *sLabel, char *sVal) { if (strlen(sVal) < 1) return; //fprintf(fp, sLabel, sVal ); @@ -746,6 +734,18 @@ fprintf(fp, sLabel, sVal ); } //json_Float +void rescueProtocolName(struct TDICOMdata *d, const char * filename) { + //tools like gdcmanon strip protocol name (0018,1030) but for Siemens we can recover it from CSASeriesHeaderInfo (0029,1020) + if ((d->manufacturer != kMANUFACTURER_SIEMENS) || (d->CSA.SeriesHeader_offset < 1) || (d->CSA.SeriesHeader_length < 1)) return; + if (strlen(d->protocolName) > 0) return; + int baseResolution, interpInt, partialFourier, echoSpacing, parallelReductionFactorInPlane; + float pf = 1.0f; //partial fourier + float phaseOversampling, delayTimeInTR, phaseResolution, txRefAmp, shimSetting[8]; + char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], pulseSequenceDetails[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; + siemensCsaAscii(filename, d->CSA.SeriesHeader_offset, d->CSA.SeriesHeader_length, &delayTimeInTR, &phaseOversampling, &phaseResolution, &txRefAmp, shimSetting, &baseResolution, &interpInt, &partialFourier, &echoSpacing, ¶llelReductionFactorInPlane, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); + strcpy(d->protocolName, protocolName); +} + void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename) { //https://docs.google.com/document/d/1HFUkAEE-pB-angVcYe6pf_-fVf4sCpOHKesUvfb8Grc/edit# // Generate Brain Imaging Data Structure (BIDS) info @@ -778,7 +778,14 @@ break; }; if (d.fieldStrength > 0.0) fprintf(fp, "\t\"MagneticFieldStrength\": %g,\n", d.fieldStrength ); + //Imaging Frequency (0018,0084) can be useful https://support.brainvoyager.com/brainvoyager/functional-analysis-preparation/29-pre-processing/78-epi-distortion-correction-echo-spacing-and-bandwidth + // however, UIH stores 128176031 not 128.176031 https://github.com/rordenlab/dcm2niix/issues/225 + if (d.imagingFrequency < 9000000) + json_Float(fp, "\t\"ImagingFrequency\": %g,\n", d.imagingFrequency); switch (d.manufacturer) { + case kMANUFACTURER_BRUKER: + fprintf(fp, "\t\"Manufacturer\": \"Bruker\",\n" ); + break; case kMANUFACTURER_SIEMENS: fprintf(fp, "\t\"Manufacturer\": \"Siemens\",\n" ); break; @@ -791,6 +798,9 @@ case kMANUFACTURER_TOSHIBA: fprintf(fp, "\t\"Manufacturer\": \"Toshiba\",\n" ); break; + case kMANUFACTURER_UIH: + fprintf(fp, "\t\"Manufacturer\": \"UIH\",\n" ); + break; }; json_Str(fp, "\t\"ManufacturersModelName\": \"%s\",\n", d.manufacturersModelName); json_Str(fp, "\t\"InstitutionName\": \"%s\",\n", d.institutionName); @@ -806,6 +816,13 @@ //Next lines directly reveal patient identity json_Str(fp, "\t\"PatientName\": \"%s\",\n", d.patientName); json_Str(fp, "\t\"PatientID\": \"%s\",\n", d.patientID); + if (strlen(d.patientBirthDate) == 8) { //DICOM DA YYYYMMDD -> ISO 8601 "YYYY-MM-DD" + int ayear,amonth,aday; + sscanf(d.patientBirthDate, "%4d%2d%2d", &ayear, &amonth, &aday); + fprintf(fp, "\t\"PatientBirthDate\": "); + fprintf(fp, (ayear >= 0 && ayear <= 9999) ? "\"%4d" : "\"%+4d", ayear); + fprintf(fp, "-%02d-%02d\",\n", amonth, aday); + } if (d.patientSex != '?') fprintf(fp, "\t\"PatientSex\": \"%c\",\n", d.patientSex); json_Float(fp, "\t\"PatientWeight\": %g,\n", d.patientWeight); //d.patientBirthDate //convert from DICOM YYYYMMDD to JSON @@ -878,7 +895,7 @@ fprintf(fp, "\t\"PhilipsRWVSlope\": %g,\n", d.RWVScale ); fprintf(fp, "\t\"PhilipsRWVIntercept\": %g,\n", d.RWVIntercept ); } - if ((d.intenScalePhilips != 0) || (d.manufacturer == kMANUFACTURER_PHILIPS)) { //for details, see PhilipsPrecise() + if ((d.intenScalePhilips != 0) && (d.manufacturer == kMANUFACTURER_PHILIPS)) { //for details, see PhilipsPrecise() fprintf(fp, "\t\"PhilipsRescaleSlope\": %g,\n", d.intenScale ); fprintf(fp, "\t\"PhilipsRescaleIntercept\": %g,\n", d.intenIntercept ); fprintf(fp, "\t\"PhilipsScaleSlope\": %g,\n", d.intenScalePhilips ); @@ -900,22 +917,21 @@ json_Float(fp, "\t\"SpacingBetweenSlices\": %g,\n", d.zSpacing); } json_Float(fp, "\t\"SAR\": %g,\n", d.SAR ); - if (d.echoNum > 1) fprintf(fp, "\t\"EchoNumber\": %d,\n", d.echoNum); + if ((d.echoNum > 1) || (d.isMultiEcho)) fprintf(fp, "\t\"EchoNumber\": %d,\n", d.echoNum); if ((d.TE > 0.0) && (!d.isXRay)) fprintf(fp, "\t\"EchoTime\": %g,\n", d.TE / 1000.0 ); //if ((d.TE2 > 0.0) && (!d.isXRay)) fprintf(fp, "\t\"EchoTime2\": %g,\n", d.TE2 / 1000.0 ); json_Float(fp, "\t\"RepetitionTime\": %g,\n", d.TR / 1000.0 ); json_Float(fp, "\t\"InversionTime\": %g,\n", d.TI / 1000.0 ); json_Float(fp, "\t\"FlipAngle\": %g,\n", d.flipAngle ); - float pf = 1.0f; //partial fourier bool interp = false; //2D interpolation float phaseOversampling = 0.0; int viewOrderGE = -1; int sliceOrderGE = -1; - if (d.phaseEncodingGE != kGE_PHASE_DIRECTION_UNKNOWN) { //only set for GE - if (d.phaseEncodingGE == kGE_PHASE_DIRECTION_BOTTOM_UP) fprintf(fp, "\t\"PhaseEncodingGE\": \"BottomUp\",\n" ); - if (d.phaseEncodingGE == kGE_PHASE_DIRECTION_TOP_DOWN) fprintf(fp, "\t\"PhaseEncodingGE\": \"TopDown\",\n" ); - if (d.phaseEncodingGE == kGE_PHASE_DIRECTION_CENTER_OUT_REV) fprintf(fp, "\t\"PhaseEncodingGE\": \"CenterOutReversed\",\n" ); - if (d.phaseEncodingGE == kGE_PHASE_DIRECTION_CENTER_OUT) fprintf(fp, "\t\"PhaseEncodingGE\": \"CenterOut\",\n" ); + //n.b. https://neurostars.org/t/getting-missing-ge-information-required-by-bids-for-common-preprocessing/1357/7 + json_Str(fp, "\t\"PhaseEncodingDirectionDisplayed\": \"%s\",\n", d.phaseEncodingDirectionDisplayedUIH); + if ((d.manufacturer == kMANUFACTURER_GE) && (d.phaseEncodingGE != kGE_PHASE_ENCODING_POLARITY_UNKNOWN)) { //only set for GE + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) fprintf(fp, "\t\"PhaseEncodingPolarityGE\": \"Unflipped\",\n" ); + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_FLIPPED) fprintf(fp, "\t\"PhaseEncodingPolarityGE\": \"Flipped\",\n" ); } #ifdef myReadGeProtocolBlock if ((d.manufacturer == kMANUFACTURER_GE) && (d.protocolBlockStartGE> 0) && (d.protocolBlockLengthGE > 19)) { @@ -929,9 +945,10 @@ #ifdef myReadAsciiCsa if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.SeriesHeader_offset > 0) && (d.CSA.SeriesHeader_length > 0)) { int baseResolution, interpInt, partialFourier, echoSpacing, parallelReductionFactorInPlane; + float pf = 1.0f; //partial fourier float delayTimeInTR, phaseResolution, txRefAmp, shimSetting[8]; - char protocolName[kDICOMStr], fmriExternalInfo[kDICOMStr], coilID[kDICOMStr], consistencyInfo[kDICOMStr], coilElements[kDICOMStr], pulseSequenceDetails[kDICOMStr]; - siemensCsaAscii(filename, d.CSA.SeriesHeader_offset, d.CSA.SeriesHeader_length, &delayTimeInTR, &phaseOversampling, &phaseResolution, &txRefAmp, shimSetting, &baseResolution, &interpInt, &partialFourier, &echoSpacing, ¶llelReductionFactorInPlane, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName); + char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], pulseSequenceDetails[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; + siemensCsaAscii(filename, d.CSA.SeriesHeader_offset, d.CSA.SeriesHeader_length, &delayTimeInTR, &phaseOversampling, &phaseResolution, &txRefAmp, shimSetting, &baseResolution, &interpInt, &partialFourier, &echoSpacing, ¶llelReductionFactorInPlane, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); if (partialFourier > 0) { //https://github.com/ismrmrd/siemens_to_ismrmrd/blob/master/parameter_maps/IsmrmrdParameterMap_Siemens_EPI_FLASHREF.xsl if (partialFourier == 1) pf = 0.5; // 4/8 @@ -967,8 +984,12 @@ //if (epiFactor > 0) fprintf(fp, "\t\"EPIFactor\": %d,\n", epiFactor); json_Str(fp, "\t\"ReceiveCoilName\": \"%s\",\n", coilID); json_Str(fp, "\t\"ReceiveCoilActiveElements\": \"%s\",\n", coilElements); + if (strcmp(coilElements,d.coilName) != 0) + json_Str(fp, "\t\"CoilString\": \"%s\",\n", d.coilName); + strcpy(d.coilName, ""); json_Str(fp, "\t\"PulseSequenceDetails\": \"%s\",\n", pulseSequenceDetails); json_Str(fp, "\t\"FmriExternalInfo\": \"%s\",\n", fmriExternalInfo); + json_Str(fp, "\t\"WipMemBlock\": \"%s\",\n", wipMemBlock); if (strlen(d.protocolName) < 1) //insert protocol name if it exists in CSA but not DICOM header: https://github.com/nipy/heudiconv/issues/80 json_Str(fp, "\t\"ProtocolName\": \"%s\",\n", protocolName); json_Str(fp, "\t\"ConsistencyInfo\": \"%s\",\n", consistencyInfo); @@ -980,6 +1001,20 @@ if (parallelReductionFactorInPlane != (int)(d.accelFactPE)) printWarning("ParallelReductionFactorInPlane reported in DICOM [0051,1011] (%d) does not match CSA series value %d\n", (int)(d.accelFactPE), parallelReductionFactorInPlane); } + } else { //e.g. Siemens Vida does not have CSA header, but has many attributes + json_Str(fp, "\t\"ReceiveCoilActiveElements\": \"%s\",\n", d.coilElements); + if (strcmp(d.coilElements,d.coilName) != 0) + json_Str(fp, "\t\"CoilString\": \"%s\",\n", d.coilName); + if ((!d.is3DAcq) && (d.phaseEncodingLines > d.echoTrainLength) && (d.echoTrainLength > 1)) { + //ETL is > 1, as some GE files list 1, as an example see series mr_0005 in dcm_qa_nih + float pf = (float)d.phaseEncodingLines; + if (d.accelFactPE > 1) + pf = (float)pf / (float)d.accelFactPE; //estimate: not sure if we round up or down + pf = (float)d.echoTrainLength / (float)pf; + if (pf < 1.0) //e.g. if difference between lines and echo length not all explained by iPAT (SENSE/GRAPPA) + fprintf(fp, "\t\"PartialFourier\": %g,\n", pf); + } //compute partial Fourier: not reported in XA10, so infer + //printf("PhaseLines=%d EchoTrainLength=%d SENSE=%g\n", d.phaseEncodingLines, d.echoTrainLength, d.accelFactPE); //n.b. we can not distinguish pF from SENSE/GRAPPA for UIH } #endif if (d.CSA.multiBandFactor > 1) //AccelFactorSlice @@ -1009,7 +1044,7 @@ if (bandwidthPerPixelPhaseEncode == 0.0) bandwidthPerPixelPhaseEncode = d.CSA.bandwidthPerPixelPhaseEncode; json_Float(fp, "\t\"BandwidthPerPixelPhaseEncode\": %g,\n", bandwidthPerPixelPhaseEncode ); - if (d.accelFactPE > 1.0) fprintf(fp, "\t\"ParallelReductionFactorInPlane\": %g,\n", d.accelFactPE); + if ((!d.is3DAcq) && (d.accelFactPE > 1.0)) fprintf(fp, "\t\"ParallelReductionFactorInPlane\": %g,\n", d.accelFactPE); //EffectiveEchoSpacing // Siemens bandwidthPerPixelPhaseEncode already accounts for the effects of parallel imaging, // interpolation, phaseOversampling, and phaseResolution, in the context of the size of the @@ -1041,15 +1076,20 @@ // FSL definition is start of first line until start of last line. // Other than the use of (n-1), the value is basically just 1.0/bandwidthPerPixelPhaseEncode. // https://github.com/rordenlab/dcm2niix/issues/130 - if ((reconMatrixPE > 0) && (effectiveEchoSpacing > 0.0)) + if ((reconMatrixPE > 0) && (effectiveEchoSpacing > 0.0) && (d.manufacturer != kMANUFACTURER_UIH)) fprintf(fp, "\t\"TotalReadoutTime\": %g,\n", effectiveEchoSpacing * (reconMatrixPE - 1.0)); + if (d.manufacturer == kMANUFACTURER_UIH) //https://github.com/rordenlab/dcm2niix/issues/225 + json_Float(fp, "\t\"TotalReadoutTime\": %g,\n", d.acquisitionDuration / 1000.0); json_Float(fp, "\t\"PixelBandwidth\": %g,\n", d.pixelBandwidth ); if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.dwellTime > 0)) fprintf(fp, "\t\"DwellTime\": %g,\n", d.dwellTime * 1E-9); // Phase encoding polarity int phPos = d.CSA.phaseEncodingDirectionPositive; - if (viewOrderGE > -1) - phPos = viewOrderGE; + //next two conditionals updated: make GE match Siemens + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) + phPos = 1; + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_FLIPPED) + phPos = 0; if (((d.phaseEncodingRC == 'R') || (d.phaseEncodingRC == 'C')) && (!d.is3DAcq) && (phPos < 0)) { //when phase encoding axis is known but we do not know phase encoding polarity // https://github.com/rordenlab/dcm2niix/issues/163 @@ -1066,16 +1106,6 @@ fprintf(fp, "\t\"PhaseEncodingDirection\": \"i"); else fprintf(fp, "\t\"PhaseEncodingDirection\": \"?"); - //these next lines temporary while we understand GE - if (viewOrderGE > -1) { - fprintf(fp, "\",\n"); - if (d.phaseEncodingRC == 'C') //Values should be "R"ow, "C"olumn or "?"Unknown - fprintf(fp, "\t\"ProbablePhaseEncodingDirection\": \"j"); - else if (d.phaseEncodingRC == 'R') - fprintf(fp, "\t\"ProbablePhaseEncodingDirection\": \"i"); - else - fprintf(fp, "\t\"ProbablePhaseEncodingDirection\": \"?"); - } //phaseEncodingDirectionPositive has one of three values: UNKNOWN (-1), NEGATIVE (0), POSITIVE (1) //However, DICOM and NIfTI are reversed in the j (ROW) direction //Equivalent to dicm2nii's "if flp(iPhase), phPos = ~phPos; end" @@ -1090,50 +1120,34 @@ fprintf(fp, "-"); fprintf(fp, "\",\n"); } //only save PhaseEncodingDirection if BOTH direction and POLARITY are known - // Slice Timing GE - if ((sliceOrderGE > -1) && (h->dim[3] > 1) && (h->dim[4] > 1) && (d.TR > 0)) { // - //Warning: not correct for multiband sequences... not sure how these are stored - //Warning: will not create correct times for sparse acquisitions where DelayTimeInTR > 0 - float t = d.TR/ (float)h->dim[3] ; - fprintf(fp, "\t\"ProbableSliceTiming\": [\n"); - if (sliceOrderGE == 1) {//interleaved ascending - for (int i = 0; i < h->dim[3]; i++) { - if (i != 0) - fprintf(fp, ",\n"); - int s = (i / 2); - if ((i % 2) != 0) s += (h->dim[3]+1)/2; - fprintf(fp, "\t\t%g", (float) s * t / 1000.0 ); - } - } else { //sequential ascending - for (int i = 0; i < h->dim[3]; i++) { + //Slice Timing UIH or GE >>>> + //in theory, we should also report XA10 slice times here, but see series 24 of https://github.com/rordenlab/dcm2niix/issues/236 + if (((d.manufacturer == kMANUFACTURER_UIH) || (d.manufacturer == kMANUFACTURER_GE) || (d.isXA10A)) && (d.CSA.sliceTiming[0] >= 0.0)) { + fprintf(fp, "\t\"SliceTiming\": [\n"); + for (int i = 0; i < h->dim[3]; i++) { if (i != 0) fprintf(fp, ",\n"); - fprintf(fp, "\t\t%g", (float) i * t / 1000.0 ); + if (d.CSA.protocolSliceNumber1 < 0) + fprintf(fp, "\t\t%g", d.CSA.sliceTiming[(h->dim[3]-1) - i]); + else + fprintf(fp, "\t\t%g", d.CSA.sliceTiming[i]); } - } fprintf(fp, "\t],\n"); } //Slice Timing Siemens - if (d.CSA.sliceTiming[0] >= 0.0) { + if ((!d.isXA10A) && (d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.sliceTiming[0] >= 0.0)) { fprintf(fp, "\t\"SliceTiming\": [\n"); if (d.CSA.protocolSliceNumber1 > 1) { //https://github.com/rordenlab/dcm2niix/issues/40 //equivalent to dicm2nii "s.SliceTiming = s.SliceTiming(end:-1:1);" - int mx = 0; - for (int i = 0; i < kMaxEPI3D; i++) { + for (int i = (h->dim[3]-1); i >= 0; i--) { if (d.CSA.sliceTiming[i] < 0.0) break; - mx++; - } - mx--; - for (int i = mx; i >= 0; i--) { - if (d.CSA.sliceTiming[i] < 0.0) break; - if (i != mx) + if (i != (h->dim[3]-1)) fprintf(fp, ",\n"); fprintf(fp, "\t\t%g", d.CSA.sliceTiming[i] / 1000.0 ); } } else { - for (int i = 0; i < kMaxEPI3D; i++) { - if (d.CSA.sliceTiming[i] < 0.0) break; + for (int i = 0; i < h->dim[3]; i++) { if (i != 0) fprintf(fp, ",\n"); fprintf(fp, "\t\t%g", d.CSA.sliceTiming[i] / 1000.0 ); @@ -1324,20 +1338,41 @@ if (minB0 > 50) printWarning("This diffusion series does not have a B0 (reference) volume\n"); if ((!opts.isSortDTIbyBVal) && (minB0idx > 0)) printMessage("Note: B0 not the first volume in the series (FSL eddy reference volume is %d)\n", minB0idx); - float kADCval = maxB0 + 1; //mark as unusual *numADC = 0; bvals = (float *) malloc(numDti * sizeof(float)); + int numGEwarn = 0; + bool isGEADC = (dcmList[indx0].numberOfDiffusionDirectionGE == 0); for (int i = 0; i < numDti; i++) { bvals[i] = vx[i].V[0]; //printMessage("---bxyz %g %g %g %g\n",vx[i].V[0],vx[i].V[1],vx[i].V[2],vx[i].V[3]); - if (isADCnotDTI(vx[i])) { + //Philips includes derived isotropic images + //if (((dcmList[indx0].manufacturer == kMANUFACTURER_GE) || (dcmList[indx0].manufacturer == kMANUFACTURER_PHILIPS)) && (isADCnotDTI(vx[i]))) { + if (((dcmList[indx0].manufacturer == kMANUFACTURER_GE)) && (isADCnotDTI(vx[i]))) { + numGEwarn += 1; + if (isGEADC) { //e.g. GE Trace where bval=900, bvec=0,0,0 + *numADC = *numADC + 1; + //printWarning("GE ADC volume %d\n", i+1); + bvals[i] = kADCval; + } else + vx[i].V[0] = 0; //e.g. GE raw B=0 where bval=900, bvec=0,0,0 + } //see issue 245 + if (((dcmList[indx0].manufacturer == kMANUFACTURER_PHILIPS)) && (isADCnotDTI(vx[i]))) { *numADC = *numADC + 1; bvals[i] = kADCval; //printMessage("+++bxyz %d\n",i); } bvals[i] = bvals[i] + (0.5 * i/numDti); //add a small bias so ties are kept in sequential order } + if (numGEwarn > 0) + printWarning("Some images had bval>0 but bvec=0 (either Trace or b=0, see issue 245)\n"); + if ((*numADC == numDti) || (numGEwarn == numDti)) { + //all isotropic/ADC images - no valid bvecs + *numADC = 0; + free(bvals); + free(vx); + return NULL; + } if (*numADC > 0) { // DWIs (i.e. short diffusion scans with too few directions to // calculate tensors...they typically acquire b=0 + 3 b > 0 so @@ -1424,7 +1459,7 @@ } //for each direction } //printMessage("%f\t%f\t%f",dcmList[indx0].CSA.dtiV[1][1],dcmList[indx0].CSA.dtiV[1][2],dcmList[indx0].CSA.dtiV[1][3]); -#ifdef HAVE_R +#ifdef USING_R std::vector bValues(numDti); std::vector bVectors(numDti*3); for (int i = 0; i < numDti; i++) @@ -1489,6 +1524,7 @@ if ( isNanPosition(d1) || isNanPosition(d2)) return d1.xyzMM[3]; float tilt = 1.0; + //printMessage("0020,0032 %g %g %g -> %g %g %g\n",d1.patientPosition[1],d1.patientPosition[2],d1.patientPosition[3],d2.patientPosition[1],d2.patientPosition[2],d2.patientPosition[3]); if (d1.gantryTilt != 0) tilt = (float) cos(d1.gantryTilt * M_PI/180); //for CT scans with gantry tilt, we need to compute distance between slices, not distance along bed return tilt * sqrt( sqr(d1.patientPosition[1]-d2.patientPosition[1])+ @@ -1581,14 +1617,19 @@ strcpy(pth, opts.outdir); int w =access(pth,W_OK); if (w != 0) { - if (getcwd(pth, sizeof(pth)) != NULL) { - w =access(pth,W_OK); - if (w != 0) { - printError("You do not have write permissions for the directory %s\n",opts.outdir); - return EXIT_FAILURE; - } - printWarning("%s write permission denied. Saving to working directory %s \n", opts.outdir, pth); - } + if (getcwd(pth, sizeof(pth)) != NULL) { + #ifdef USE_CWD_IF_OUTDIR_NO_WRITE //optional: fall back to current working directory + w =access(pth,W_OK); + if (w != 0) { + printError("You do not have write permissions for the directory %s\n",opts.outdir); + return EXIT_FAILURE; + } + printWarning("%s write permission denied. Saving to working directory %s \n", opts.outdir, pth); + #else + printError("You do not have write permissions for the directory %s\n",opts.outdir); + return EXIT_FAILURE; + #endif + } } } char inname[PATH_MAX] = {""};//{"test%t_%av"}; //% a = acquisition, %n patient name, %t time @@ -1603,6 +1644,7 @@ bool isCoilReported = false; bool isEchoReported = false; bool isSeriesReported = false; + bool isImageNumReported = false; while (pos < strlen(inname)) { if (inname[pos] == '%') { if (pos > start) { @@ -1613,11 +1655,11 @@ pos++; //extra increment: skip both % and following character char f = 'P'; if (pos < strlen(inname)) f = toupper(inname[pos]); - if ((f == 'A') && (dcm.coilNum > 0)) { + if (f == 'A') { isCoilReported = true; - sprintf(newstr, "%02d", dcm.coilNum); - strcat (outname,newstr); + strcat (outname,dcm.coilName); } + if (f == 'B') strcat (outname,dcm.imageBaseName); if (f == 'C') strcat (outname,dcm.imageComments); if (f == 'D') strcat (outname,dcm.seriesDescription); if (f == 'E') { @@ -1636,10 +1678,14 @@ if (f == 'L') //"L"ocal Institution-generated description or classification of the Procedure Step that was performed. strcat (outname,dcm.procedureStepDescription); if (f == 'M') { - if (dcm.manufacturer == kMANUFACTURER_GE) + if (dcm.manufacturer == kMANUFACTURER_BRUKER) + strcat (outname,"Br"); + else if (dcm.manufacturer == kMANUFACTURER_GE) strcat (outname,"GE"); else if (dcm.manufacturer == kMANUFACTURER_TOSHIBA) strcat (outname,"To"); + else if (dcm.manufacturer == kMANUFACTURER_UIH) + strcat (outname,"UI"); else if (dcm.manufacturer == kMANUFACTURER_PHILIPS) strcat (outname,"Ph"); else if (dcm.manufacturer == kMANUFACTURER_SIEMENS) @@ -1654,6 +1700,12 @@ if (strlen(dcm.protocolName) < 1) printWarning("Unable to append protocol name (0018,1030) to filename (it is empty).\n"); } + if (f == 'R') { + sprintf(newstr, "%d", dcm.imageNum); + strcat (outname,newstr); + isImageNumReported = true; + } + if (f == 'Q') strcat (outname,dcm.scanningSequence); if (f == 'S') { @@ -1674,7 +1726,9 @@ #endif } if (f == 'V') { - if (dcm.manufacturer == kMANUFACTURER_GE) + if (dcm.manufacturer == kMANUFACTURER_BRUKER) + strcat (outname,"Bruker"); + else if (dcm.manufacturer == kMANUFACTURER_GE) strcat (outname,"GE"); else if (dcm.manufacturer == kMANUFACTURER_PHILIPS) strcat (outname,"Philips"); @@ -1682,6 +1736,8 @@ strcat (outname,"Siemens"); else if (dcm.manufacturer == kMANUFACTURER_TOSHIBA) strcat (outname,"Toshiba"); + else if (dcm.manufacturer == kMANUFACTURER_UIH) + strcat (outname,"UIH"); else strcat (outname,"NA"); } @@ -1692,12 +1748,19 @@ if ((f >= '0') && (f <= '9')) { if ((pos 1)) { - sprintf(newstr, "_c%d", dcm.coilNum); - strcat (outname,newstr); + if ((!isCoilReported) && (dcm.isCoilVaries)) { + //sprintf(newstr, "_c%d", dcm.coilNum); + //strcat (outname,newstr); + strcat (outname, "_c"); + strcat (outname,dcm.coilName); } + // myMultiEchoFilenameSkipEcho1 https://github.com/rordenlab/dcm2niix/issues/237 + #ifdef myMultiEchoFilenameSkipEcho1 if ((!isEchoReported) && (dcm.isMultiEcho) && (dcm.echoNum >= 1)) { //multiple echoes saved as same series + #else + if ((!isEchoReported) && (dcm.isMultiEcho)) { //multiple echoes saved as same series + #endif sprintf(newstr, "_e%d", dcm.echoNum); strcat (outname,newstr); isEchoReported = true; } + if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { + sprintf(newstr, "_i%05d", dcm.imageNum); + strcat (outname,newstr); + } if ((!isSeriesReported) && (!isEchoReported) && (dcm.echoNum > 1)) { //last resort: user provided no method to disambiguate echo number in filename sprintf(newstr, "_e%d", dcm.echoNum); strcat (outname,newstr); @@ -1743,16 +1817,27 @@ if (strlen(outname) < 1) strcpy(outname, "dcm2nii_invalidName"); if (outname[0] == '.') outname[0] = '_'; //make sure not a hidden file //eliminate illegal characters http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx - #if defined(_WIN64) || defined(_WIN32) //https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names + // https://github.com/rordenlab/dcm2niix/issues/237 + #ifdef myOsSpecificFilenameMask + #define kMASK_WINDOWS_SPECIAL_CHARACTERS 0 + #else + #define kMASK_WINDOWS_SPECIAL_CHARACTERS 1 + #endif + #if defined(_WIN64) || defined(_WIN32) || defined(kMASK_WINDOWS_SPECIAL_CHARACTERS)//https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names for (size_t pos = 0; pos') || (outname[pos] == ':') || (outname[pos] == '"') // || (outname[pos] == '/') || (outname[pos] == '\\') || (outname[pos] == '^') || (outname[pos] == '*') || (outname[pos] == '|') || (outname[pos] == '?')) outname[pos] = '_'; + #if defined(_WIN64) || defined(_WIN32) + const char kForeignPathSeparator ='/'; + #else + const char kForeignPathSeparator ='\\'; + #endif for (int pos = 0; posdescrip)) < 80) strcat (hdr->descrip,newstr); } -void nii_scale16bitSigned(unsigned char *img, struct nifti_1_header *hdr) { +void nii_scale16bitSigned(unsigned char *img, struct nifti_1_header *hdr, int isVerbose) { //lossless scaling of INT16 data: e.g. input with range -100...3200 and scl_slope=1 // will be stored as -1000...32000 with scl_slope 0.1 if (hdr->datatype != DT_INT16) return; @@ -2194,7 +2277,11 @@ int scale = kMx / (int)max16; if (abs(min16) > max16) scale = kMx / (int)abs(min16); - if (scale < 2) return; //already uses dynamic range + if (scale < 2) { + if (isVerbose) + printMessage("Sufficient 16-bit range: raw %d..%d\n", min16, max16); + return; //already uses dynamic range + } hdr->scl_slope = hdr->scl_slope/ scale; for (int i=0; i < nVox; i++) img16[i] = img16[i] * scale; @@ -2202,8 +2289,7 @@ nii_storeIntegerScaleFactor(scale, hdr); } - -void nii_scale16bitUnsigned(unsigned char *img, struct nifti_1_header *hdr){ +void nii_scale16bitUnsigned(unsigned char *img, struct nifti_1_header *hdr, int isVerbose){ //lossless scaling of UINT16 data: e.g. input with range 0...3200 and scl_slope=1 // will be stored as 0...64000 with scl_slope 0.05 if (hdr->datatype != DT_UINT16) return; @@ -2219,7 +2305,11 @@ max16 = img16[i]; int kMx = 64000; //actually 65535 - maybe a bit of padding for interpolation ringing int scale = kMx / (int)max16; - if (scale < 2) return; //already uses dynamic range + if (scale < 2) { + if (isVerbose > 0) + printMessage("Sufficient unsigned 16-bit range: raw max %d\n", max16); + return; //already uses dynamic range + } hdr->scl_slope = hdr->scl_slope/ scale; for (int i=0; i < nVox; i++) img16[i] = img16[i] * scale; @@ -2227,7 +2317,9 @@ nii_storeIntegerScaleFactor(scale, hdr); } -void nii_check16bitUnsigned(unsigned char *img, struct nifti_1_header *hdr){ +#define UINT16_TO_INT16_IF_LOSSLESS +#ifdef UINT16_TO_INT16_IF_LOSSLESS +void nii_check16bitUnsigned(unsigned char *img, struct nifti_1_header *hdr, int isVerbose){ //default NIfTI 16-bit is signed, set to unusual 16-bit unsigned if required... if (hdr->datatype != DT_UINT16) return; int dim3to7 = 1; @@ -2243,11 +2335,19 @@ max16 = img16[i]; //printMessage("max16= %d vox=%d %fms\n",max16, nVox, ((double)(clock()-start))/1000); if (max16 > 32767) { - printMessage("Note: rare 16-bit UNSIGNED integer image. Older tools may require 32-bit conversion\n"); + if (isVerbose > 0) + printMessage("Note: rare 16-bit UNSIGNED integer image. Older tools may require 32-bit conversion\n"); } else hdr->datatype = DT_INT16; } //nii_check16bitUnsigned() +#else +void nii_check16bitUnsigned(unsigned char *img, struct nifti_1_header *hdr, int isVerbose){ + if (hdr->datatype != DT_UINT16) return; + if (isVerbose < 1) return; + printMessage("Note: rare 16-bit UNSIGNED integer image. Older tools may require 32-bit conversion\n"); +} +#endif int siemensCtKludge(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dcmList[]) { //Siemens CT bug: when a user draws an open object graphics object onto a 2D slice this is appended as an additional image, @@ -2270,6 +2370,84 @@ return (fabs (a - b) <= tolerance); } +unsigned char * nii_saveNII3DtiltFloat32(char * niiFilename, struct nifti_1_header * hdr, unsigned char* im, struct TDCMopts opts, float * sliceMMarray, float gantryTiltDeg, int manufacturer ) { + //correct for gantry tilt - http://www.mathworks.com/matlabcentral/fileexchange/24458-dicom-gantry-tilt-correction + if (opts.isOnlyBIDS) return im; + if (gantryTiltDeg == 0.0) return im; + struct nifti_1_header hdrIn = *hdr; + int nVox2DIn = hdrIn.dim[1]*hdrIn.dim[2]; + if ((nVox2DIn < 1) || (hdrIn.dim[0] != 3) || (hdrIn.dim[3] < 3)) return im; + if (hdrIn.datatype != DT_FLOAT32) { + printMessage("Only able to correct gantry tilt for 16-bit integer or 32-bit float data with at least 3 slices."); + return im; + } + printMessage("Gantry Tilt Correction is new: please validate conversions\n"); + float GNTtanPx = tan(gantryTiltDeg / (180/M_PI))/hdrIn.pixdim[2]; //tangent(degrees->radian) + //unintuitive step: reverse sign for negative gantry tilt, therefore -27deg == +27deg (why @!?#) + // seen in http://www.mathworks.com/matlabcentral/fileexchange/28141-gantry-detector-tilt-correction/content/gantry2.m + // also validated with actual data... + if (manufacturer == kMANUFACTURER_PHILIPS) //see 'Manix' example from Osirix + GNTtanPx = - GNTtanPx; + else if ((manufacturer == kMANUFACTURER_SIEMENS) && (gantryTiltDeg > 0.0)) + GNTtanPx = - GNTtanPx; + else if (manufacturer == kMANUFACTURER_GE) + ; //do nothing + else + if (gantryTiltDeg < 0.0) GNTtanPx = - GNTtanPx; //see Toshiba examples from John Muschelli + // printMessage("gantry tilt pixels per mm %g\n",GNTtanPx); + float * imIn32 = ( float*) im; + //create new output image: larger due to skew + // compute how many pixels slice must be extended due to skew + int s = hdrIn.dim[3] - 1; //top slice + float maxSliceMM = fabs(s * hdrIn.pixdim[3]); + if (sliceMMarray != NULL) maxSliceMM = fabs(sliceMMarray[s]); + int pxOffset = ceil(fabs(GNTtanPx*maxSliceMM)); + // printMessage("Tilt extends slice by %d pixels", pxOffset); + hdr->dim[2] = hdr->dim[2] + pxOffset; + int nVox2D = hdr->dim[1]*hdr->dim[2]; + unsigned char * imOut = (unsigned char *)malloc(nVox2D * hdrIn.dim[3] * 4);// *4 as 32-bits per voxel, sizeof(float) ); + float * imOut32 = ( float*) imOut; + //set surrounding voxels to darkest observed value + float minVoxVal = imIn32[0]; + for (int v = 0; v < (nVox2DIn * hdrIn.dim[3]); v++) + if (imIn32[v] < minVoxVal) + minVoxVal = imIn32[v]; + for (int v = 0; v < (nVox2D * hdrIn.dim[3]); v++) + imOut32[v] = minVoxVal; + //copy skewed voxels + for (int s = 0; s < hdrIn.dim[3]; s++) { //for each slice + float sliceMM = s * hdrIn.pixdim[3]; + if (sliceMMarray != NULL) sliceMM = sliceMMarray[s]; //variable slice thicknesses + //sliceMM -= mmMidZ; //adjust so tilt relative to middle slice + if (GNTtanPx < 0) + sliceMM -= maxSliceMM; + float Offset = GNTtanPx*sliceMM; + float fracHi = ceil(Offset) - Offset; //ceil not floor since rI=r-Offset not rI=r+Offset + float fracLo = 1.0f - fracHi; + for (int r = 0; r < hdr->dim[2]; r++) { //for each row of output + float rI = (float)r - Offset; //input row + if ((rI >= 0.0) && (rI < hdrIn.dim[2])) { + int rLo = floor(rI); + int rHi = rLo + 1; + if (rHi >= hdrIn.dim[2]) rHi = rLo; + rLo = (rLo * hdrIn.dim[1]) + (s * nVox2DIn); //offset to start of row below + rHi = (rHi * hdrIn.dim[1]) + (s * nVox2DIn); //offset to start of row above + int rOut = (r * hdrIn.dim[1]) + (s * nVox2D); //offset to output row + for (int c = 0; c < hdrIn.dim[1]; c++) { //for each row + imOut32[rOut+c] = round( ( ((float)imIn32[rLo+c])*fracLo) + ((float)imIn32[rHi+c])*fracHi); + } //for c (each column) + } //rI (input row) in range + } //for r (each row) + } //for s (each slice)*/ + free(im); + if (sliceMMarray != NULL) return imOut; //we will save after correcting for variable slice thicknesses + char niiFilenameTilt[2048] = {""}; + strcat(niiFilenameTilt,niiFilename); + strcat(niiFilenameTilt,"_Tilt"); + nii_saveNII3D(niiFilenameTilt, *hdr, imOut, opts); + return imOut; +}// nii_saveNII3DtiltFloat32() + unsigned char * nii_saveNII3Dtilt(char * niiFilename, struct nifti_1_header * hdr, unsigned char* im, struct TDCMopts opts, float * sliceMMarray, float gantryTiltDeg, int manufacturer ) { //correct for gantry tilt - http://www.mathworks.com/matlabcentral/fileexchange/24458-dicom-gantry-tilt-correction if (opts.isOnlyBIDS) return im; @@ -2277,6 +2455,8 @@ struct nifti_1_header hdrIn = *hdr; int nVox2DIn = hdrIn.dim[1]*hdrIn.dim[2]; if ((nVox2DIn < 1) || (hdrIn.dim[0] != 3) || (hdrIn.dim[3] < 3)) return im; + if (hdrIn.datatype == DT_FLOAT32) + return nii_saveNII3DtiltFloat32(niiFilename, hdr, im, opts, sliceMMarray, gantryTiltDeg, manufacturer); if (hdrIn.datatype != DT_INT16) { printMessage("Only able to correct gantry tilt for 16-bit integer data with at least 3 slices."); return im; @@ -2348,14 +2528,15 @@ return imOut; }// nii_saveNII3Dtilt() + int nii_saveNII3Deq(char * niiFilename, struct nifti_1_header hdr, unsigned char* im, struct TDCMopts opts, float * sliceMMarray ) { //convert image with unequal slice distances to equal slice distances //sliceMMarray = 0.0 3.0 6.0 12.0 22.0 <- ascending distance from first slice if (opts.isOnlyBIDS) return EXIT_SUCCESS; int nVox2D = hdr.dim[1]*hdr.dim[2]; if ((nVox2D < 1) || (hdr.dim[0] != 3) ) return EXIT_FAILURE; - if ((hdr.datatype != DT_UINT8) && (hdr.datatype != DT_RGB24) && (hdr.datatype != DT_INT16)) { - printMessage("Only able to make equidistant slices from 3D 8,16,24-bit volumes with at least 3 slices."); + if ((hdr.datatype != DT_FLOAT32) && (hdr.datatype != DT_UINT8) && (hdr.datatype != DT_RGB24) && (hdr.datatype != DT_INT16)) { + printMessage("Only able to make equidistant slices from 8,16,24-bit integer or 32-bit float data with at least 3 slices."); return EXIT_FAILURE; } float mn = sliceMMarray[1] - sliceMMarray[0]; @@ -2388,7 +2569,36 @@ hdrX.srow_z[2] = hdr.srow_z[2] * Scale; } unsigned char *imX; - if (hdr.datatype == DT_INT16) { + if (hdr.datatype == DT_FLOAT32) { + float * im32 = ( float*) im; + imX = (unsigned char *)malloc( (nVox2D * slices) * 4);//sizeof(float) + float * imX32 = ( float*) imX; + for (int s=0; s < slices; s++) { + float sliceXmm = s * mn; //distance from first slice + int sliceXi = (s * nVox2D);//offset for this slice + int sHi = 0; + while ((sHi < (hdr.dim[3] - 1) ) && (sliceMMarray[sHi] < sliceXmm)) + sHi += 1; + int sLo = sHi - 1; + if (sLo < 0) sLo = 0; + float mmHi = sliceMMarray[sHi]; + float mmLo = sliceMMarray[sLo]; + sLo = sLo * nVox2D; + sHi = sHi * nVox2D; + if ((mmHi == mmLo) || (sliceXmm > mmHi)) { //select only from upper slice TPX + //for (int v=0; v < nVox2D; v++) + // imX16[sliceXi+v] = im16[sHi+v]; + memcpy(&imX32[sliceXi], &im32[sHi], nVox2D* sizeof(float)); //memcpy( dest, src, bytes) + + } else { + float fracHi = (sliceXmm-mmLo)/ (mmHi-mmLo); + float fracLo = 1.0 - fracHi; + //weight between two slices + for (int v=0; v < nVox2D; v++) + imX32[sliceXi+v] = round( ( (float)im32[sLo+v]*fracLo) + (float)im32[sHi+v]*fracHi); + } + } + } else if (hdr.datatype == DT_INT16) { short * im16 = ( short*) im; imX = (unsigned char *)malloc( (nVox2D * slices) * 2);//sizeof( short) ); short * imX16 = ( short*) imX; @@ -2638,6 +2848,7 @@ }// nii_saveCrop() float dicomTimeToSec (float dicomTime) { +//convert HHMMSS to seconds, 135300.024 -> 135259.731 are 0.293 sec apart char acqTimeBuf[64]; snprintf(acqTimeBuf, sizeof acqTimeBuf, "%+013.5f", (double)dicomTime); int ahour,amin; @@ -2668,6 +2879,36 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { //detect images with slice timing errors. https://github.com/rordenlab/dcm2niix/issues/126 if ((d->TR < 0.0) || (d->CSA.sliceTiming[0] < 0.0)) return; //no slice timing + int nSlices = 0; + while ((nSlices < kMaxEPI3D) && (d->CSA.sliceTiming[nSlices] >= 0.0)) + nSlices++; + if (nSlices < 1) return; + bool isSliceTimeHHMMSS = (d->manufacturer == kMANUFACTURER_UIH); + //if (d->isXA10A) isSliceTimeHHMMSS = true; //for XA10 use TimeAfterStart 0x0021,0x1104 -> Siemens de-identification can corrupt acquisition ties https://github.com/rordenlab/dcm2niix/issues/236 + if (isSliceTimeHHMMSS) {//convert HHMMSS to Sec + for (int i = 0; i < nSlices; i++) + d->CSA.sliceTiming[i] = dicomTimeToSec(d->CSA.sliceTiming[i]); + float minT = d->CSA.sliceTiming[0]; + float maxT = minT; + for (int i = 0; i < nSlices; i++) { + if (d->CSA.sliceTiming[i] < minT) minT = d->CSA.sliceTiming[i]; + if (d->CSA.sliceTiming[i] < maxT) maxT = d->CSA.sliceTiming[i]; + } + float kMidnightSec = 86400; + float kNoonSec = 43200; + if ((maxT - minT) > kNoonSec) { //volume started before midnight but ended next day! + //identify and fix 'Cinderella error' where clock resets at midnight: untested + printWarning("UIH acquisition crossed midnight: check slice timing\n"); + for (int i = 0; i < nSlices; i++) + if (d->CSA.sliceTiming[i] > kNoonSec) d->CSA.sliceTiming[i] = d->CSA.sliceTiming[i] - kMidnightSec; + minT = d->CSA.sliceTiming[0]; + for (int i = 0; i < nSlices; i++) + if (d->CSA.sliceTiming[i] < minT) minT = d->CSA.sliceTiming[i]; + + } + for (int i = 0; i < nSlices; i++) + d->CSA.sliceTiming[i] = d->CSA.sliceTiming[i] - minT; + } //XA10/UIH: HHMMSS -> Sec float minT = d->CSA.sliceTiming[0]; float maxT = minT; for (int i = 0; i < kMaxEPI3D; i++) { @@ -2675,6 +2916,9 @@ if (d->CSA.sliceTiming[i] < minT) minT = d->CSA.sliceTiming[i]; if (d->CSA.sliceTiming[i] > maxT) maxT = d->CSA.sliceTiming[i]; } + if (isSliceTimeHHMMSS) //convert HHMMSS to Sec + for (int i = 0; i < kMaxEPI3D; i++) + d->CSA.sliceTiming[i] = dicomTimeToSec(d->CSA.sliceTiming[i]); if ((minT != maxT) && (maxT <= d->TR)) return; //looks fine if ((minT == maxT) && (d->is3DAcq)) return; //fine: 3D EPI if ((minT == maxT) && (d->CSA.multiBandFactor == d->CSA.mosaicSlices)) return; //fine: all slices single excitation @@ -2682,13 +2926,17 @@ //check if 2nd image has valud slice timing float minT1 = d1->CSA.sliceTiming[0]; float maxT1 = minT1; - for (int i = 0; i < kMaxEPI3D; i++) { - if (d1->CSA.sliceTiming[i] < 0.0) break; + for (int i = 0; i < nSlices; i++) { + //if (d1->CSA.sliceTiming[i] < 0.0) break; if (d1->CSA.sliceTiming[i] < minT1) minT1 = d1->CSA.sliceTiming[i]; if (d1->CSA.sliceTiming[i] > maxT1) maxT1 = d1->CSA.sliceTiming[i]; } + if (minT1 < 0.0) { //https://github.com/neurolabusc/MRIcroGL/issues/31 + printWarning("Siemens MoCo? Bogus slice timing (range %g..%g, TR=%gms)\n", minT1, maxT1, d->TR); + return; + } if ((minT1 == maxT1) || (maxT1 >= d->TR)) { //both first and second image corrupted - printWarning("CSA slice timing appears corrupted (range %g..%g, TR=%gms)\n", minT, maxT, d->TR); + printWarning("CSA slice timing appears corrupted (range %g..%g, TR=%gms)\n", minT1, maxT1, d->TR); return; } //1st image corrupted, but 2nd looks ok - substitute values from 2nd image @@ -2706,16 +2954,28 @@ uint64_t indx = dcmSort[0].indx; uint64_t indx0 = dcmSort[0].indx; uint64_t indx1 = indx0; + if ((dcmList[indx].isXA10A) && (dcmList[indx].CSA.mosaicSlices < 0)) { + printMessage("Siemens XA10 Mosaics are not primary images and lack vital data.\n"); + printMessage(" See https://github.com/rordenlab/dcm2niix/issues/236\n"); + #ifdef mySaveXA10Mosaics + int n; + printMessage("INPUT REQUIRED FOR %s\n", dcmList[indx].imageBaseName); + printMessage("PLEASE ENTER NUMBER OF SLICES IN MOSAIC:\n"); + scanf ("%d",&n); + for (int i = 0; i < nConvert; i++) + dcmList[dcmSort[i].indx].CSA.mosaicSlices = n; + #endif + } if (nConvert > 1) indx1 = dcmSort[1].indx; if (opts.isIgnoreDerivedAnd2D && dcmList[indx].isDerived) { printMessage("Ignoring derived image(s) of series %ld %s\n", dcmList[indx].seriesNum, nameList->str[indx]); return EXIT_SUCCESS; } - if ((opts.isIgnoreDerivedAnd2D) && ((dcmList[indx].isLocalizer) || (strcmp(dcmList[indx].sequenceName, "_fl3d1_ns")== 0) || (strcmp(dcmList[indx].sequenceName, "_fl2d1")== 0)) ) { + if ((opts.isIgnoreDerivedAnd2D) && ((dcmList[indx].isLocalizer) || (strcmp(dcmList[indx].sequenceName, "_tfl2d1")== 0) || (strcmp(dcmList[indx].sequenceName, "_fl3d1_ns")== 0) || (strcmp(dcmList[indx].sequenceName, "_fl2d1")== 0)) ) { printMessage("Ignoring localizer (sequence %s) of series %ld %s\n", dcmList[indx].sequenceName, dcmList[indx].seriesNum, nameList->str[indx]); return EXIT_SUCCESS; } - if ((opts.isIgnoreDerivedAnd2D) && (nConvert < 2) && (dcmList[indx].xyzDim[3] < 2)) { + if ((opts.isIgnoreDerivedAnd2D) && (nConvert < 2) && (dcmList[indx].CSA.mosaicSlices < 2) && (dcmList[indx].xyzDim[3] < 2)) { printMessage("Ignoring 2D image of series %ld %s\n", dcmList[indx].seriesNum, nameList->str[indx]); return EXIT_SUCCESS; } @@ -2767,17 +3027,30 @@ hdr0.dim[3] = nConvert/nAcq; hdr0.dim[4] = nAcq; hdr0.dim[0] = 4; + } else if ((dcmList[indx0].isXA10A) && (nConvert > nAcq) && (nAcq > 1) ) { + nAcq -= 1; + hdr0.dim[3] = nConvert/nAcq; + hdr0.dim[4] = nAcq; + hdr0.dim[0] = 4; + if ((nAcq > 1) && (nConvert != nAcq)) { + printMessage("Slice positions repeated, but number of slices (%d) not divisible by number of repeats (%d): converting only complete volumes.\n", nConvert, nAcq); + } } else { hdr0.dim[3] = nConvert; if ((nAcq > 1) && (nConvert != nAcq)) { printMessage("Slice positions repeated, but number of slices (%d) not divisible by number of repeats (%d): missing images?\n", nConvert, nAcq); } } - //next: detect if ANY file flagged as echo vaies - for (int i = 0; i < nConvert; i++) - if (dcmList[dcmSort[i].indx].isMultiEcho) - dcmList[indx0].isMultiEcho = true; - //next: detect variable inter-volume time https://github.com/rordenlab/dcm2niix/issues/184 + //next options removed: features now thoroughly detected in nii_loadDir() + for (int i = 0; i < nConvert; i++) { //make sure 1st volume describes shared features + if (dcmList[dcmSort[i].indx].isCoilVaries) dcmList[indx0].isCoilVaries = true; + if (dcmList[dcmSort[i].indx].isMultiEcho) dcmList[indx0].isMultiEcho = true; + if (dcmList[dcmSort[i].indx].isNonParallelSlices) dcmList[indx0].isNonParallelSlices = true; + if (dcmList[dcmSort[i].indx].isHasPhase) dcmList[indx0].isHasPhase = true; + if (dcmList[dcmSort[i].indx].isHasReal) dcmList[indx0].isHasReal = true; + if (dcmList[dcmSort[i].indx].isHasImaginary) dcmList[indx0].isHasImaginary = true; + } + //next: detect variable inter-volume time https://github.com/rordenlab/dcm2niix/issues/184 if (dcmList[indx0].modality == kMODALITY_PT) { bool trVaries = false; bool dayVaries = false; @@ -2835,7 +3108,6 @@ printMessage("instance=["); for (int i = 0; i < nConvert; i++) { printMessage(" %d", dcmList[dcmSort[i].indx].imageNum); - } printMessage("]\n"); } //imageNum not sequential @@ -2853,9 +3125,10 @@ hdr0.dim[4] = nConvert; hdr0.dim[0] = 4; } + if ((dx > 0) && (!isSameFloatGE(dx, hdr0.pixdim[3]))) + hdr0.pixdim[3] = dx; dcmList[dcmSort[0].indx].xyzMM[3] = dx; //16Sept2014 : correct DICOM for true distance between slice centers: // e.g. MCBI Siemens ToF 0018:0088 reports 16mm SpacingBetweenSlices, but actually 0.5mm - if (dx > 0) hdr0.pixdim[3] = dx; } else if (hdr0.dim[4] < 2) { hdr0.dim[4] = nConvert; hdr0.dim[0] = 4; @@ -2900,7 +3173,98 @@ if (hdr0.dim[4] > 1) //for 4d datasets, last volume should be acquired before first checkDateTimeOrder(&dcmList[dcmSort[0].indx], &dcmList[dcmSort[nConvert-1].indx]); } - if ((segVol >= 0) && (hdr0.dim[4] > 1)) { + //Siemens XA10 slice timing + // Ignore first volume: For an example of erroneous first volume timing, see series 10 (Functional_w_SMS=3) https://github.com/rordenlab/dcm2niix/issues/240 + // an alternative would be to use 0018,9074 - this would need to be converted from DT to Secs, and is scrambled if de-identifies data see enhanced de-identified series 26 from issue 236 + if ((dcmList[dcmSort[0].indx].isXA10A) && (hdr0.dim[4] < 2)) + dcmList[dcmSort[0].indx].CSA.sliceTiming[0] = -1.0; //XA10A slice timing often not correct for 1st volume + if (((dcmList[dcmSort[0].indx].isXA10A)) && (nConvert == (hdr0.dim[4])) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { + float mn = dcmList[dcmSort[1].indx].CSA.sliceTiming[0]; + //for (int v = 0; v < hdr0.dim[4]; v++) + // for (int z = 0; z < hdr0.dim[3]; z++) + // printf("%g\n",dcmList[dcmSort[v].indx].CSA.sliceTiming[z]); + //get slice timing from second volume + for (int v = 0; v < hdr0.dim[3]; v++) { + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[1].indx].CSA.sliceTiming[v]; + if (dcmList[dcmSort[0].indx].CSA.sliceTiming[v] < mn) mn = dcmList[dcmSort[0].indx].CSA.sliceTiming[v]; + } + if (mn < 0.0) mn = 0.0; + int mb = 0; + for (int v = 0; v < hdr0.dim[3]; v++) { + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] -= mn; + if (isSameFloatGE(dcmList[dcmSort[0].indx].CSA.sliceTiming[v], 0.0)) mb ++; + } + if ((dcmList[dcmSort[0].indx].CSA.multiBandFactor < 2) && (mb > 1)) + dcmList[dcmSort[0].indx].CSA.multiBandFactor = mb; + //for (int v = 0; v < hdr0.dim[3]; v++) + // printf("XA10sliceTiming\t%d\t%g\n", v, dcmList[dcmSort[0].indx].CSA.sliceTiming[v]); + } + //UIH 2D slice timing + if (((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_UIH)) && (nConvert == (hdr0.dim[3]*hdr0.dim[4])) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { + for (int v = 0; v < hdr0.dim[3]; v++) + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].acquisitionTime; + } + //GE check slice timing >>> + bool GEsliceTiming_x0018x1060 = false; + if ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_GE) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { + //GE: 1st method for "epi" PSD + GEsliceTiming_x0018x1060 = true; + for (int v = 0; v < hdr0.dim[3]; v++) { + if (dcmList[dcmSort[v].indx].CSA.sliceTiming[0] < 0) + GEsliceTiming_x0018x1060 = false; + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0] / 1000.0; //ms -> sec + } + //0018,1060 provides time at end of acquisition, not start... + if (GEsliceTiming_x0018x1060) { + float minT = dcmList[dcmSort[0].indx].CSA.sliceTiming[0]; + for (int v = 0; v < hdr0.dim[3]; v++) + if (dcmList[dcmSort[0].indx].CSA.sliceTiming[v] < minT) + minT = dcmList[dcmSort[0].indx].CSA.sliceTiming[v]; + for (int v = 0; v < hdr0.dim[3]; v++) + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[0].indx].CSA.sliceTiming[v] - minT; + } //adjust: first slice is time = 0.0 + } //GE slice timing from 0018,1060 + if ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_GE) && (!GEsliceTiming_x0018x1060) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { + //GE: 2nd method for "epiRT" PSD + //ignore bogus values of first volume https://neurostars.org/t/getting-missing-ge-information-required-by-bids-for-common-preprocessing/1357/6 + // this necessarily requires at last two volumes, hence dim[4] > 1 + int j = hdr0.dim[3]; + //since first volume is bogus, we define the volume start time as the first slice in the second volume + float minTime = dcmList[dcmSort[j].indx].rtia_timerGE; + float maxTime = minTime; + for (int v = 0; v < hdr0.dim[3]; v++) { + if (dcmList[dcmSort[v+j].indx].rtia_timerGE < minTime) + minTime = dcmList[dcmSort[v+j].indx].rtia_timerGE; + if (dcmList[dcmSort[v+j].indx].rtia_timerGE > maxTime) + maxTime = dcmList[dcmSort[v+j].indx].rtia_timerGE; + } + //compare all slice times in 2nd volume to start time for this volume + if (maxTime != minTime) { + for (int v = 0; v < hdr0.dim[3]; v++) + dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[v+j].indx].rtia_timerGE - minTime; + dcmList[dcmSort[0].indx].CSA.sliceTiming[hdr0.dim[3]] = -1; + //detect multi-band + int nZero = 0; + for (int v = 0; v < hdr0.dim[3]; v++) + if (isSameFloatGE(dcmList[dcmSort[0].indx].CSA.sliceTiming[hdr0.dim[3]], 0.0)) + nZero ++; + if ((nZero > 1) && (nZero < hdr0.dim[3]) && ((hdr0.dim[3] % nZero) == 0)) + dcmList[dcmSort[0].indx].CSA.multiBandFactor = nZero; + //report timines + if (opts.isVerbose > 1) { + printf("GE slice timing\n"); + printf("\tTime\tX\tY\tZ\tInstance\n"); + for (int v = 0; v < hdr0.dim[3]; v++) { + if (v == (hdr0.dim[3]-1)) + printf("...\n"); + if ((v < 4) || (v == (hdr0.dim[3]-1))) + printf("\t%g\t%g\t%g\t%g\t%d\n", dcmList[dcmSort[0].indx].CSA.sliceTiming[v], dcmList[dcmSort[v+j].indx].patientPosition[1], dcmList[dcmSort[v+j].indx].patientPosition[2], dcmList[dcmSort[v+j].indx].patientPosition[3], dcmList[dcmSort[v+j].indx].imageNum); + + } //for v + } //verbose > 1 + } //if maxTime != minTIme + } //GE slice timing from 0021,105E + if ((segVol >= 0) && (hdr0.dim[4] > 1)) { int inVol = hdr0.dim[4]; int nVol = 0; for (int v = 0; v < inVol; v++) @@ -2931,6 +3295,8 @@ free(img4D); saveAs3D = false; } + if (strlen(dcmList[dcmSort[0].indx].protocolName) < 1) + rescueProtocolName(&dcmList[dcmSort[0].indx], nameList->str[dcmSort[0].indx]); char pathoutname[2048] = {""}; if (nii_createFilename(dcmList[dcmSort[0].indx], pathoutname, opts) == EXIT_FAILURE) { free(imgM); @@ -2944,10 +3310,11 @@ for(int i = 0; i < nConvert; ++i) dcmList[dcmSort[i].indx].converted2NII = 1; if (opts.numSeries < 0) { //report series number but do not convert - if (segVol >= 0) + if (segVol >= 0) { printMessage("\t%ld.%d\t%s\n", dcmList[dcmSort[0].indx].seriesNum, segVol-1, pathoutname); - else + } else { printMessage("\t%ld\t%s\n", dcmList[dcmSort[0].indx].seriesNum, pathoutname); + } printMessage(" %s\n",nameList->str[dcmSort[0].indx]); return EXIT_SUCCESS; } @@ -2964,6 +3331,10 @@ if (sliceDir < 0) { imgM = nii_flipZ(imgM, &hdr0); sliceDir = abs(sliceDir); //change this, we have flipped the image so GE DTI bvecs no longer need to be flipped! + if ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_UIH) && (dcmList[dcmSort[0].indx].CSA.sliceTiming[0] >= 0.0) ) + dcmList[dcmSort[0].indx].CSA.protocolSliceNumber1 = -1; + if ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_GE) && (dcmList[dcmSort[0].indx].CSA.sliceTiming[0] >= 0.0) ) + dcmList[dcmSort[0].indx].CSA.protocolSliceNumber1 = -1; } // skip converting if user has specified one or more series, but has not specified this one if (opts.numSeries > 0) { @@ -2994,17 +3365,17 @@ int * volOrderIndex = nii_SaveDTI(pathoutname,nConvert, dcmSort, dcmList, opts, sliceDir, dti4D, &numADC); PhilipsPrecise(&dcmList[dcmSort[0].indx], opts.isPhilipsFloatNotDisplayScaling, &hdr0, opts.isVerbose); if ((opts.isMaximize16BitRange) && (hdr0.datatype == DT_INT16)) { - nii_scale16bitSigned(imgM, &hdr0); //allow INT16 to use full dynamic range + nii_scale16bitSigned(imgM, &hdr0, opts.isVerbose); //allow INT16 to use full dynamic range } else if ((opts.isMaximize16BitRange) && (hdr0.datatype == DT_UINT16) && (!dcmList[dcmSort[0].indx].isSigned)) { - nii_scale16bitUnsigned(imgM, &hdr0); //allow UINT16 to use full dynamic range + nii_scale16bitUnsigned(imgM, &hdr0, opts.isVerbose); //allow UINT16 to use full dynamic range } else if ((!opts.isMaximize16BitRange) && (hdr0.datatype == DT_UINT16) && (!dcmList[dcmSort[0].indx].isSigned)) - nii_check16bitUnsigned(imgM, &hdr0); //save UINT16 as INT16 if we can do this losslessly + nii_check16bitUnsigned(imgM, &hdr0, opts.isVerbose); //save UINT16 as INT16 if we can do this losslessly printMessage( "Convert %d DICOM as %s (%dx%dx%dx%d)\n", nConvert, pathoutname, hdr0.dim[1],hdr0.dim[2],hdr0.dim[3],hdr0.dim[4]); //~ if (!dcmList[dcmSort[0].indx].isSlicesSpatiallySequentialPhilips) //~ nii_reorderSlices(imgM, &hdr0, dti4D); if (hdr0.dim[3] < 2) printWarning("Check that 2D images are not mirrored.\n"); -#ifndef HAVE_R +#ifndef USING_R else fflush(stdout); //GUI buffers printf, display all results #endif @@ -3025,7 +3396,7 @@ else { if (volOrderIndex) //reorder volumes imgM = reorderVolumes(&hdr0, imgM, volOrderIndex); -#ifndef HAVE_R +#ifndef USING_R if ((opts.isIgnoreDerivedAnd2D) && (numADC > 0)) printMessage("Ignoring derived diffusion image(s). Better isotropic and ADC maps can be generated later processing.\n"); if ((!opts.isIgnoreDerivedAnd2D) && (numADC > 0)) {//ADC maps can disrupt analysis: save a copy with the ADC map, and another without @@ -3039,7 +3410,7 @@ } #endif imgM = removeADC(&hdr0, imgM, numADC); -#ifndef HAVE_R +#ifndef USING_R if (opts.isSave3D) returnCode = nii_saveNII3D(pathoutname, hdr0, imgM, opts); else @@ -3066,7 +3437,7 @@ } if ((opts.isCrop) && (dcmList[indx0].is3DAcq) && (hdr0.dim[3] > 1) && (hdr0.dim[0] < 4))//for T1 scan: && (dcmList[indx0].TE < 25) returnCode = nii_saveCrop(pathoutname, hdr0, imgM,opts); //n.b. must be run AFTER nii_setOrtho()! -#ifdef HAVE_R +#ifdef USING_R // Note that for R, only one image should be created per series // Hence the logical OR here if (returnCode == EXIT_SUCCESS || nii_saveNII(pathoutname,hdr0,imgM,opts) == EXIT_SUCCESS) @@ -3117,8 +3488,26 @@ dti4D->gradDynVol[i] = series; } } + //bvec/bval saved for each series (real, phase, magnitude, imaginary) https://github.com/rordenlab/dcm2niix/issues/219 + TDTI4D dti4Ds; + dti4Ds = *dti4D; + bool isHasDti = (dcmList[indx].CSA.numDti > 0); + if ((isHasDti) && (dcmList[indx].CSA.numDti == dcmList[indx].xyzDim[4])) { + int nDti = 0; + for (int i = 0; i < dcmList[indx].xyzDim[4]; i++) { + if (dti4D->gradDynVol[i] == 1) { + dti4Ds.S[nDti].V[0] = dti4Ds.S[i].V[0]; + dti4Ds.S[nDti].V[1] = dti4Ds.S[i].V[1]; + dti4Ds.S[nDti].V[2] = dti4Ds.S[i].V[2]; + dti4Ds.S[nDti].V[3] = dti4Ds.S[i].V[3]; + nDti++; + } + } + dcmList[indx].CSA.numDti = nDti; + } + //save each series for (int s = 1; s <= series; s++) { - for (int i = 0; i < dcmList[indx].xyzDim[4]; i++) + for (int i = 0; i < dcmList[indx].xyzDim[4]; i++) { if (dti4D->gradDynVol[i] == s) { //dti4D->gradDynVol[i] = s; //nVol ++; @@ -3135,8 +3524,10 @@ dcmList[indx].isHasMagnitude = false; dcmList[indx].echoNum = echoNum[i]; break; - } - int ret2 = saveDcm2NiiCore(nConvert, dcmSort, dcmList, nameList, opts, dti4D, s); + } + } + if (s > 1) dcmList[indx].CSA.numDti = 0; //only save bvec for first type (magnitude) + int ret2 = saveDcm2NiiCore(nConvert, dcmSort, dcmList, nameList, opts, &dti4Ds, s); if (ret2 != EXIT_SUCCESS) ret = ret2; //return EXIT_SUCCESS only if ALL are successful } return ret; @@ -3149,6 +3540,23 @@ tdcmref.img = ((uint64_t)dcmdata.seriesNum << 32) + dcmdata.imageNum; for(int i = 0; i < MAX_NUMBER_OF_DIMENSIONS; ++i) tdcmref.dimensionIndexValues[i] = dcmdata.dimensionIndexValues[i]; + //lines below added to cope with extreme anonymization + // https://github.com/rordenlab/dcm2niix/issues/211 + if (tdcmref.dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS-1] != 0) return; + //Since dimensionIndexValues are indexed from 1, 0 indicates unused + // we leverage this as a hail mary attempt to distinguish images with identical series and instance numbers + //See Correction Number CP-1242: + // "Clarify in the description of dimension indices ... start from 1" + // 0008,0032 stored as HHMMSS.FFFFFF, there are 86400000 ms per day + // dimensionIndexValues stored as uint32, so encode acquisition time in ms + uint32_t h = trunc(dcmdata.acquisitionTime / 10000.0); + double tm = dcmdata.acquisitionTime - (h * 10000.0); + uint32_t m = trunc(tm / 100.0); + tm = tm - (m * 100.0); + uint32_t ms = round(tm * 1000); + ms += (h * 3600000) + (m * 60000); + //printf("HHMMSS.FFFF %.5f -> %d ms\n", dcmdata.acquisitionTime, ms); + tdcmref.dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS-1] = ms; } // fillTDCMsort() int compareTDCMsort(void const *item1, void const *item2) { @@ -3242,35 +3650,45 @@ return r; } -bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opts, struct TWarnings* warnings, bool *isMultiEcho) { +bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opts, struct TWarnings* warnings, bool *isMultiEcho, bool *isNonParallelSlices, bool *isCoilVaries) { //returns true if d1 and d2 should be stacked together as a single output if (!d1.isValid) return false; if (!d2.isValid) return false; if (d1.modality != d2.modality) return false; //do not stack MR and CT data! if (d1.isDerived != d2.isDerived) return false; //do not stack raw and derived image types if (d1.manufacturer != d2.manufacturer) return false; //do not stack data from different vendors - if (d1.seriesNum != d2.seriesNum) return false; + if ((d1.isXA10A) && (d2.isXA10A) && (d1.seriesNum > 1000) && (d2.seriesNum > 1000)) { + //kludge XA10A (0020,0011) increments [16001, 16002, ...] https://github.com/rordenlab/dcm2niix/issues/236 + //images from series 16001,16002 report different study times (0008,0030)! + if ((d1.seriesNum / 1000) != (d2.seriesNum / 1000)) return false; + } else if (d1.seriesNum != d2.seriesNum) return false; #ifdef mySegmentByAcq if (d1.acquNum != d2.acquNum) return false; - #else - if (d1.acquNum != d2.acquNum) { - if (!warnings->acqNumVaries) - printMessage("slices stacked despite varying acquisition numbers (if this is not desired please recompile)\n"); - warnings->acqNumVaries = true; - } #endif - if ((d1.bitsAllocated != d2.bitsAllocated) || (d1.xyzDim[1] != d2.xyzDim[1]) || (d1.xyzDim[2] != d2.xyzDim[2]) || (d1.xyzDim[3] != d2.xyzDim[3]) ) { + bool isSameStudyInstanceUID = false; + if ((strlen(d1.studyInstanceUID)> 1) && (strlen(d2.studyInstanceUID)> 1)) { + if (strcmp(d1.studyInstanceUID, d2.studyInstanceUID) == 0) + isSameStudyInstanceUID = true; + } + bool isSameTime = isSameFloatDouble(d1.dateTime, d2.dateTime); + if ((isSameStudyInstanceUID) && (d1.isXA10A) && (d2.isXA10A)) + isSameTime = true; //kludge XA10A 0008,0030 incorrect https://github.com/rordenlab/dcm2niix/issues/236 + if ((!isSameStudyInstanceUID) && (!isSameTime)) + return false; + if ((d1.bitsAllocated != d2.bitsAllocated) || (d1.xyzDim[1] != d2.xyzDim[1]) || (d1.xyzDim[2] != d2.xyzDim[2]) || (d1.xyzDim[3] != d2.xyzDim[3]) ) { if (!warnings->bitDepthVaries) printMessage("slices not stacked: dimensions or bit-depth varies\n"); warnings->bitDepthVaries = true; return false; } - if (!isSameFloatDouble(d1.dateTime, d2.dateTime)) { //beware, some vendors incorrectly store Image Time (0008,0033) as Study Time (0008,0030). + #ifndef myIgnoreStudyTime + if (!isSameTime) { //beware, some vendors incorrectly store Image Time (0008,0033) as Study Time (0008,0030). if (!warnings->dateTimeVaries) printMessage("slices not stacked: Study Date/Time (0008,0020 / 0008,0030) varies %12.12f ~= %12.12f\n", d1.dateTime, d2.dateTime); warnings->dateTimeVaries = true; return false; } + #endif if (opts->isForceStackSameSeries) { if ((d1.TE != d2.TE) || (d1.echoNum != d2.echoNum)) *isMultiEcho = true; @@ -3291,10 +3709,11 @@ *isMultiEcho = true; return false; } - if (d1.coilNum != d2.coilNum) { + if (d1.coilCrc != d2.coilCrc) { if (!warnings->coilVaries) printMessage("slices not stacked: coil varies\n"); warnings->coilVaries = true; + *isCoilVaries = true; return false; } if ((strlen(d1.protocolName) < 1) && (strlen(d2.protocolName) < 1)) { @@ -3309,13 +3728,19 @@ } if ((!isSameFloatGE(d1.orient[1], d2.orient[1]) || !isSameFloatGE(d1.orient[2], d2.orient[2]) || !isSameFloatGE(d1.orient[3], d2.orient[3]) || !isSameFloatGE(d1.orient[4], d2.orient[4]) || !isSameFloatGE(d1.orient[5], d2.orient[5]) || !isSameFloatGE(d1.orient[6], d2.orient[6]) ) ) { - if (!warnings->orientVaries) - printMessage("slices not stacked: orientation varies (localizer?) [%g %g %g %g %g %g] != [%g %g %g %g %g %g]\n", + if ((!warnings->orientVaries) && (!d1.isNonParallelSlices)) + printMessage("slices not stacked: orientation varies (vNav or localizer?) [%g %g %g %g %g %g] != [%g %g %g %g %g %g]\n", d1.orient[1], d1.orient[2], d1.orient[3],d1.orient[4], d1.orient[5], d1.orient[6], d2.orient[1], d2.orient[2], d2.orient[3],d2.orient[4], d2.orient[5], d2.orient[6]); warnings->orientVaries = true; + *isNonParallelSlices = true; return false; } + if (d1.acquNum != d2.acquNum) { + if (!warnings->acqNumVaries) + printMessage("slices stacked despite varying acquisition numbers (if this is not desired recompile with 'mySegmentByAcq')\n"); + warnings->acqNumVaries = true; + } return true; }// isSameSet() @@ -3423,7 +3848,7 @@ } } if (nDuplicates > 0) - printMessage("%d images have identical time, series, acquisition and image values. DUPLICATES REMOVED.\n", nDuplicates); + printMessage("%d images have identical time, series, acquisition and instance values. DUPLICATES REMOVED.\n", nDuplicates); return nConvert - nDuplicates; }// removeDuplicates() @@ -3443,7 +3868,7 @@ } } if (nDuplicates > 0) - printMessage("%d images have identical time, series, acquisition and image values. Duplicates removed.\n", nDuplicates); + printMessage("%d images have identical time, series, acquisition and instance values. Duplicates removed.\n", nDuplicates); return nConvert - nDuplicates; }// removeDuplicatesVerbose() @@ -3483,6 +3908,35 @@ free(nameList.str); } +int copyFile (char * src_path, char * dst_path) { + #define BUFFSIZE 32768 + unsigned char buffer[BUFFSIZE]; + FILE *fin = fopen(src_path, "rb"); + if (fin == NULL) { + printError("Check file permissions: Unable to open input %s\n", src_path); + return EXIT_FAILURE; + } + if (is_fileexists(dst_path)) { + printError("File naming conflict. Existing file %s\n", dst_path); + return EXIT_FAILURE; + } + FILE *fou = fopen(dst_path, "wb"); + if (fou == NULL) { + printError("Check file permission. Unable to open output %s\n", dst_path); + return EXIT_FAILURE; + } + size_t bytes; + while ((bytes = fread(buffer, 1, BUFFSIZE, fin)) != 0) { + if(fwrite(buffer, 1, bytes, fou) != bytes) { + printError("Unable to write %zu bytes to output %s\n", bytes, dst_path); + return EXIT_FAILURE; + } + } + fclose(fin); + fclose(fou); + return EXIT_SUCCESS; +} + int nii_loadDir(struct TDCMopts* opts) { //Identifies all the DICOM files in a folder and its subfolders if (strlen(opts->indir) < 1) { @@ -3565,6 +4019,8 @@ int nConvertTotal = 0; bool compressionWarning = false; bool convertError = false; + bool isDcmExt = isExt(opts->filename, ".dcm"); // "%r.dcm" with multi-echo should generate "1.dcm", "1e2.dcm" + if (isDcmExt) opts->filename[strlen(opts->filename) - 4] = 0; // "%s_%r.dcm" -> "%s_%r" for (int i = 0; i < (int)nDcm; i++ ) { if ((isExt(nameList.str[i], ".par")) && (isDICOMfile(nameList.str[i]) < 1)) { strcpy(opts->indir, nameList.str[i]); //set to original file name, not path @@ -3578,7 +4034,21 @@ } dcmList[i] = readDICOMv(nameList.str[i], opts->isVerbose, opts->compressFlag, &dti4D); //ignore compile warning - memory only freed on first of 2 passes //~ if ((dcmList[i].isValid) &&((dcmList[i].totalSlicesIn4DOrder != NULL) ||(dcmList[i].patientPositionNumPhilips > 1) || (dcmList[i].CSA.numDti > 1))) { //4D dataset: dti4D arrays require huge amounts of RAM - write this immediately - if ((dcmList[i].isValid) &&((dti4D.sliceOrder[0] >= 0) || (dcmList[i].CSA.numDti > 1))) { //4D dataset: dti4D arrays require huge amounts of RAM - write this immediately + if ((dcmList[i].imageNum > 0) && (opts->isRenameNotConvert > 0)) { //use imageNum instead of isValid to convert non-images (kWaveformSq will have instance number but is not a valid image) + char outname[PATH_MAX] = {""}; + if (dcmList[i].echoNum > 1) dcmList[i].isMultiEcho = true; //last resort: Siemens gives different echoes the same image number: avoid overwriting, e.g "-f %r.dcm" should generate "1.dcm", "1_e2.dcm" for multi-echo volumes + nii_createFilename(dcmList[i], outname, *opts); + if (isDcmExt) strcat (outname,".dcm"); + int ret = copyFile (nameList.str[i], outname); + if (ret != EXIT_SUCCESS) { + printError("Unable to rename all DICOM images.\n"); + return ret; + } + if (opts->isVerbose > 0) + printMessage("Renaming %s -> %s\n", nameList.str[i], outname); + dcmList[i].isValid = false; + } + if ((dcmList[i].isValid) && ((dti4D.sliceOrder[0] >= 0) || (dcmList[i].CSA.numDti > 1))) { //4D dataset: dti4D arrays require huge amounts of RAM - write this immediately struct TDCMsort dcmSort[1]; fillTDCMsort(dcmSort[0], i, dcmList[i]); dcmList[i].converted2NII = 1; @@ -3593,7 +4063,10 @@ printMessage("Image Decompression is new: please validate conversions\n"); } } -#ifdef HAVE_R + if (opts->isRenameNotConvert > 0) { + return EXIT_SUCCESS; + } +#ifdef USING_R if (opts->isScanOnly) { TWarnings warnings = setWarnings(); // Create the first series from the first DICOM file @@ -3606,8 +4079,8 @@ bool matched = false; // If the file matches an existing series, add it to the corresponding file list for (int j = 0; j < opts->series.size(); j++) { - bool isMultiEchoUnused; - if (isSameSet(opts->series[j].representativeData, dcmList[i], opts, &warnings, &isMultiEchoUnused)) { + bool isMultiEchoUnused, isNonParallelSlices, isCoilVaries; + if (isSameSet(opts->series[j].representativeData, dcmList[i], opts, &warnings, &isMultiEchoUnused, &isNonParallelSlices, &isCoilVaries)) { opts->series[j].files.push_back(nameList.str[i]); matched = true; break; @@ -3626,27 +4099,43 @@ } else { #endif //3: stack DICOMs with the same Series + struct TWarnings warnings = setWarnings(); for (int i = 0; i < (int)nDcm; i++ ) { if ((dcmList[i].converted2NII == 0) && (dcmList[i].isValid)) { int nConvert = 0; - struct TWarnings warnings = setWarnings(); bool isMultiEcho = false; + bool isNonParallelSlices = false; + bool isCoilVaries = false; for (int j = i; j < (int)nDcm; j++) - if (isSameSet(dcmList[i], dcmList[j], opts, &warnings, &isMultiEcho ) ) + if (isSameSet(dcmList[i], dcmList[j], opts, &warnings, &isMultiEcho, &isNonParallelSlices, &isCoilVaries) ) nConvert++; if (nConvert < 1) nConvert = 1; //prevents compiler warning for next line: never executed since j=i always causes nConvert ++ TDCMsort * dcmSort = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); nConvert = 0; - isMultiEcho = false; - for (int j = i; j < (int)nDcm; j++) - if (isSameSet(dcmList[i], dcmList[j], opts, &warnings, &isMultiEcho)) { + for (int j = i; j < (int)nDcm; j++) { + isMultiEcho = false; + isNonParallelSlices = false; + isCoilVaries = false; + if (isSameSet(dcmList[i], dcmList[j], opts, &warnings, &isMultiEcho, &isNonParallelSlices, &isCoilVaries)) { dcmList[j].converted2NII = 1; //do not reprocess repeats fillTDCMsort(dcmSort[nConvert], j, dcmList[j]); nConvert++; } else { - dcmList[i].isMultiEcho = isMultiEcho; - dcmList[j].isMultiEcho = isMultiEcho; - } + if (isNonParallelSlices) { + dcmList[i].isNonParallelSlices = true; + dcmList[j].isNonParallelSlices = true; + } + if (isMultiEcho) { + dcmList[i].isMultiEcho = true; + dcmList[j].isMultiEcho = true; + } + if (isCoilVaries) { + dcmList[i].isCoilVaries = true; + dcmList[j].isCoilVaries = true; + } + + } //unable to stack images: mark files that may need file name dis-ambiguation + } qsort(dcmSort, nConvert, sizeof(struct TDCMsort), compareTDCMsort); //sort based on series and image numbers.... //dcmList[dcmSort[0].indx].isMultiEcho = isMultiEcho; if (opts->isVerbose) @@ -3655,14 +4144,14 @@ //nConvert = removeDuplicatesVerbose(nConvert, dcmSort, &nameList); nConvert = removeDuplicates(nConvert, dcmSort); int ret = saveDcm2Nii(nConvert, dcmSort, dcmList, &nameList, *opts, &dti4D); - if (ret == EXIT_SUCCESS) + if (ret == EXIT_SUCCESS) nConvertTotal += nConvert; else convertError = true; free(dcmSort); }//convert all images of this series } -#ifdef HAVE_R +#ifdef USING_R } #endif free(dcmList); @@ -3700,8 +4189,8 @@ strcpy(name,""); //not found! }*/ -#if defined(_WIN64) || defined(_WIN32) -#else //UNIX +#if defined(_WIN64) || defined(_WIN32) || defined(USING_R) +#else //UNIX, not R int findpathof(char *pth, const char *exe) { //Find executable by searching the PATH environment variable. @@ -3746,6 +4235,7 @@ } #endif +#ifndef USING_R void readFindPigz (struct TDCMopts *opts, const char * argv[]) { #if defined(_WIN64) || defined(_WIN32) strcpy(opts->pigzname,"pigz.exe"); @@ -3830,6 +4320,7 @@ //printMessage("Found pigz: %s\n", str); #endif } //readFindPigz() +#endif void setDefaultOpts (struct TDCMopts *opts, const char * argv[]) { //either "setDefaultOpts(opts,NULL)" or "setDefaultOpts(opts,argv)" where argv[0] is path to search strcpy(opts->pigzname,""); @@ -3848,6 +4339,7 @@ strcpy(opts->outdir,""); strcpy(opts->imageComments,""); opts->isOnlySingleFile = false; //convert all files in a directory, not just a single file + opts->isRenameNotConvert = false; opts->isForceStackSameSeries = false; opts->isIgnoreDerivedAnd2D = false; opts->isPhilipsFloatNotDisplayScaling = true; diff -Nru dcm2niix-1.0.20180622/console/nii_dicom_batch.h dcm2niix-1.0.20181125/console/nii_dicom_batch.h --- dcm2niix-1.0.20180622/console/nii_dicom_batch.h 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_dicom_batch.h 2018-11-25 21:24:07.000000000 +0000 @@ -10,12 +10,12 @@ #include //requires VS 2015 or later #include -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif #include "nii_dicom.h" -#ifdef HAVE_R +#ifdef USING_R struct TDicomSeries { TDICOMdata representativeData; std::vector files; @@ -25,12 +25,12 @@ #define MAX_NUM_SERIES 16 struct TDCMopts { - bool isMaximize16BitRange, isSave3D,isGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackSameSeries, isCrop; + bool isRenameNotConvert, isMaximize16BitRange, isSave3D,isGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackSameSeries, isCrop; int isVerbose, compressFlag, dirSearchDepth, gzLevel; //support for compressed data 0=none, char filename[512], outdir[512], indir[512], pigzname[512], optsname[512], indirParent[512], imageComments[24]; float seriesNumber[MAX_NUM_SERIES]; long numSeries; -#ifdef HAVE_R +#ifdef USING_R bool isScanOnly; void *imageList; std::vector series; @@ -41,7 +41,7 @@ void readIniFile (struct TDCMopts *opts, const char * argv[]); int nii_saveNII(char * niiFilename, struct nifti_1_header hdr, unsigned char* im, struct TDCMopts opts); //void readIniFile (struct TDCMopts *opts); - int nii_loadDir (struct TDCMopts *opts); + int nii_loadDir(struct TDCMopts *opts); //void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct TDTI4D *dti4D, struct nifti_1_header *h, const char * filename); void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename); int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopts opts); diff -Nru dcm2niix-1.0.20180622/console/nii_dicom.cpp dcm2niix-1.0.20181125/console/nii_dicom.cpp --- dcm2niix-1.0.20180622/console/nii_dicom.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_dicom.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -16,7 +16,7 @@ #include #endif //#include //clock() -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif #include "print.h" @@ -34,7 +34,7 @@ #include #include "nifti1_io_core.h" -#ifdef HAVE_R +#ifdef USING_R #undef isnan #define isnan ISNAN #endif @@ -503,7 +503,7 @@ R44.m[1][3] = offset.v[1]; R44.m[2][3] = offset.v[2]; //R44.m[3][3] = offset.v[3]; - if (sign(d.CSA.sliceNormV[iSL]) != sign(cosSL)) { + if (sign(d.CSA.sliceNormV[iSL]) != sign(cosSL)) { R44.m[0][2] = -R44.m[0][2]; R44.m[1][2] = -R44.m[1][2]; R44.m[2][2] = -R44.m[2][2]; @@ -556,7 +556,9 @@ } return Q44; } - if (d.CSA.mosaicSlices > 1) { + //next line only for Siemens mosaic: ignore for UIH grid + // https://github.com/xiangruili/dicm2nii/commit/47ad9e6d9bc8a999344cbd487d602d420fb1509f + if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.mosaicSlices > 1)) { double nRowCol = ceil(sqrt((double) d.CSA.mosaicSlices)); double lFactorX = (d.xyzDim[1] -(d.xyzDim[1]/nRowCol) )/2.0; double lFactorY = (d.xyzDim[2] -(d.xyzDim[2]/nRowCol) )/2.0; @@ -567,6 +569,7 @@ for (int r=0; r<4; r++) Q44.m[c][r] = -Q44.m[c][r]; mat33 Q; + LOAD_MAT33(Q, d.orient[1], d.orient[4],d.CSA.sliceNormV[1], d.orient[2],d.orient[5],d.CSA.sliceNormV[2], d.orient[3],d.orient[6],d.CSA.sliceNormV[3]); @@ -610,7 +613,7 @@ if ((d.isDerived) || ((d.bitsAllocated == 8) && (d.samplesPerPixel == 3) && (d.manufacturer == kMANUFACTURER_SIEMENS))) { printMessage("Unable to determine spatial orientation: 0020,0037 missing (probably not a problem: derived image)\n"); } else { - printMessage("Unable to determine spatial orientation: 0020,0037 missing!\n"); + printMessage("Unable to determine spatial orientation: 0020,0037 missing (Type 1 attribute: not a valid DICOM) Series %ld\n", d.seriesNum); } } mat44 Q44 = set_nii_header_x(d, d2, h, &sliceDir, isVerbose); @@ -641,7 +644,11 @@ sprintf(dtxt, ";mb=%d", d.CSA.multiBandFactor); strcat(txt,dtxt); } - snprintf(h->descrip,80, "%s",txt); + // GCC 8 warns about truncation using snprintf; using strncpy instead seems to keep it happy + // snprintf(h->descrip,80, "%s",txt); + strncpy(h->descrip, txt, 79); + h->descrip[79] = '\0'; + if (strlen(d.imageComments) > 0) snprintf(h->aux_file,24,"%.23s",d.imageComments); return headerDcm2NiiSForm(d,d2, h, isVerbose); @@ -671,7 +678,9 @@ } for (int i=0; i < MAX_NUMBER_OF_DIMENSIONS; ++i) d.dimensionIndexValues[i] = 0; - d.CSA.sliceTiming[0] = -1.0f; //impossible value denotes not known + //d.CSA.sliceTiming[0] = -1.0f; //impossible value denotes not known + for (int z = 0; z < kMaxEPI3D; z++) + d.CSA.sliceTiming[z] = -1.0; d.CSA.numDti = 0; for (int i=0; i < 5; i++) d.xyzDim[i] = 1; @@ -681,6 +690,8 @@ strcpy(d.patientID, ""); strcpy(d.imageType,""); strcpy(d.imageComments, ""); + strcpy(d.imageBaseName, ""); + strcpy(d.phaseEncodingDirectionDisplayedUIH, ""); strcpy(d.studyDate, ""); strcpy(d.studyTime, ""); strcpy(d.protocolName, ""); @@ -703,6 +714,8 @@ strcpy(d.studyID, ""); strcpy(d.studyInstanceUID, ""); strcpy(d.bodyPartExamined,""); + strcpy(d.coilName, ""); + strcpy(d.coilElements, ""); d.phaseEncodingLines = 0; //~ d.patientPositionSequentialRepeats = 0; //~ d.patientPositionRepeats = 0; @@ -723,6 +736,8 @@ d.TI = 0.0; d.flipAngle = 0.0; d.bandwidthPerPixelPhaseEncode = 0.0; + d.acquisitionDuration = 0.0; + d.imagingFrequency = 0.0; d.fieldStrength = 0.0; d.SAR = 0.0; d.pixelBandwidth = 0.0; @@ -736,7 +751,7 @@ d.protocolBlockStartGE = 0; d.protocolBlockLengthGE = 0; d.phaseEncodingSteps = 0; - d.coilNum = 0; + d.coilCrc = 0; d.accelFactPE = 0.0; //d.patientPositionNumPhilips = 0; d.imageBytes = 0; @@ -758,6 +773,7 @@ d.is2DAcq = false; // d.isDerived = false; //0008,0008 = DERIVED,CSAPARALLEL,POSDISP d.isSegamiOasis = false; //these images do not store spatial coordinates + d.isXA10A = false; //https://github.com/rordenlab/dcm2niix/issues/236 d.triggerDelayTime = 0.0; d.RWVScale = 0.0; d.RWVIntercept = 0.0; @@ -772,11 +788,16 @@ d.isFloat = false; //default is for integers, not single or double precision d.isResampled = false; //assume data not resliced to remove gantry tilt problems d.isLocalizer = false; + d.isNonParallelSlices = false; + d.isCoilVaries = false; d.compressionScheme = 0; //none d.isExplicitVR = true; d.isLittleEndian = true; //DICOM initially always little endian d.converted2NII = 0; - d.phaseEncodingGE = kGE_PHASE_DIRECTION_UNKNOWN; + d.numberOfDiffusionDirectionGE = -1; + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNKNOWN; + d.rtia_timerGE = -1.0; + d.numberOfImagesInGridUIH = 0; d.phaseEncodingRC = '?'; d.patientSex = '?'; d.patientWeight = 0.0; @@ -799,6 +820,26 @@ return d; } //clear_dicom_data() +int isdigitdot(int c) { //returns true if digit or '.' + if (c == '.') return 1; + return isdigit(c); +} + +void dcmStrDigitsDotOnlyKey(char key, char* lStr) { + //e.g. string "F:2.50" returns 2.50 if key==":" + size_t len = strlen(lStr); + if (len < 1) return; + bool isKey = false; + for (int i = 0; i < (int) len; i++) { + if (!isdigitdot(lStr[i]) ) { + isKey = (lStr[i] == key); + lStr[i] = ' '; + + } else if (!isKey) + lStr[i] = ' '; + } +} //dcmStrDigitsOnlyKey() + void dcmStrDigitsOnlyKey(char key, char* lStr) { //e.g. string "p2s3" returns 2 if key=="p" and 3 if key=="s" size_t len = strlen(lStr); @@ -823,6 +864,17 @@ lStr[i] = ' '; } +// Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ +uint32_t mz_crc32(unsigned char *ptr, uint32_t buf_len) +{ + static const uint32_t s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c }; + uint32_t crcu32 = 0; + if (!ptr) return crcu32; + crcu32 = ~crcu32; while (buf_len--) { uint8_t b = *ptr++; crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)]; crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)]; } + return ~crcu32; +} + void dcmStr(int lLength, unsigned char lBuffer[], char* lOut, bool isStrLarge = false) { if (lLength < 1) return; //#ifdef _MSC_VER @@ -1004,6 +1056,10 @@ ret = kMANUFACTURER_PHILIPS; if ((toupper(cString[0])== 'T') && (toupper(cString[1])== 'O')) ret = kMANUFACTURER_TOSHIBA; + if ((toupper(cString[0])== 'U') && (toupper(cString[1])== 'I')) + ret = kMANUFACTURER_UIH; + if ((toupper(cString[0])== 'B') && (toupper(cString[1])== 'R')) + ret = kMANUFACTURER_BRUKER; //#ifdef _MSC_VER free(cString); //#endif @@ -1083,6 +1139,7 @@ lPos += 8; //skip 8 bytes of data, 32-bit lnTag plus 77 00 00 0 TCSAtag tagCSA; TCSAitem itemCSA; + bool is3D = false; int itemsOK; float lFloats[7]; for (int lT = 1; lT <= lnTag; lT++) { @@ -1133,6 +1190,8 @@ CSA->sliceMeasurementDuration = csaMultiFloat (&buff[lPos], 3,lFloats, &itemsOK); else if (strcmp(tagCSA.name, "BandwidthPerPixelPhaseEncode") == 0) CSA->bandwidthPerPixelPhaseEncode = csaMultiFloat (&buff[lPos], 3,lFloats, &itemsOK); + else if ((strcmp(tagCSA.name, "Actual3DImaPartNumber") == 0) && (tagCSA.nitems > 0) ) + is3D = true; else if ((strcmp(tagCSA.name, "MosaicRefAcqTimes") == 0) && (tagCSA.nitems > 3) ){ float * sliceTimes = (float *)malloc(sizeof(float) * (tagCSA.nitems + 1)); csaMultiFloat (&buff[lPos], tagCSA.nitems,sliceTimes, &itemsOK); @@ -1223,7 +1282,6 @@ //int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, int isVerbose) { int readProtocolDataBlockGE(unsigned char *buff, int lLength, struct TCSAdata *CSA, int isVerbose) { - return EXIT_SUCCESS; } @@ -1359,6 +1417,9 @@ h->dim[2] = d.xyzDim[2]; h->dim[3] = d.xyzDim[3]; h->dim[4] = d.xyzDim[4]; + h->dim[5] = 1; + h->dim[6] = 1; + h->dim[7] = 1; if (h->dim[4] < 2) h->dim[0] = 3; else @@ -1411,10 +1472,10 @@ void changeExt (char *file_name, const char* ext) { char *p_extension; p_extension = strrchr(file_name, '.'); + //if ((p_extension > file_name) && (strlen(ext) < 1)) + // p_extension--; if (p_extension) - { strcpy(++p_extension, ext); - } } //changeExt() void cleanStr(char* lOut) { @@ -1821,8 +1882,7 @@ d.angulation[2] = cols[kAngulationAPs]; d.angulation[3] = cols[kAngulationFHs]; d.sliceOrient = (int) cols[kSliceOrients]; - //printMessage(">>>> %d\n", d.sliceOrient); - d.TE = cols[kTEcho]; + d.TE = cols[kTEcho]; d.echoNum = cols[kEcho]; d.TI = cols[kInversionDelayMs]; d.intenIntercept = cols[kRI]; @@ -2252,20 +2312,25 @@ } //nii_ImgBytes() //unsigned char * nii_demosaic(unsigned char* inImg, struct nifti_1_header *hdr, int nMosaicSlices, int ProtocolSliceNumber1) { -unsigned char * nii_demosaic(unsigned char* inImg, struct nifti_1_header *hdr, int nMosaicSlices) { +unsigned char * nii_demosaic(unsigned char* inImg, struct nifti_1_header *hdr, int nMosaicSlices, bool isUIH) { //demosaic http://nipy.org/nibabel/dicom/dicom_mosaic.html if (nMosaicSlices < 2) return inImg; //Byte inImg[ [img length] ]; //[img getBytes:&inImg length:[img length]]; - int nRowCol = (int) ceil(sqrt((double) nMosaicSlices)); - int colBytes = hdr->dim[1]/nRowCol * hdr->bitpix/8; + int nCol = (int) ceil(sqrt((double) nMosaicSlices)); + int nRow = nCol; + //n.b. Siemens store 20 images as 5x5 grid, UIH as 5rows, 4 Col https://github.com/rordenlab/dcm2niix/issues/225 + if (isUIH) + nRow = ceil((float)nMosaicSlices/(float)nCol); + //printf("%d = %dx%d\n", nMosaicSlices, nCol, nRow); + int colBytes = hdr->dim[1]/nCol * hdr->bitpix/8; int lineBytes = hdr->dim[1] * hdr->bitpix/8; - int rowBytes = hdr->dim[1] * hdr->dim[2]/nRowCol * hdr->bitpix/8; + int rowBytes = hdr->dim[1] * hdr->dim[2]/nRow * hdr->bitpix/8; int col = 0; int row = 0; int lOutPos = 0; - hdr->dim[1] = hdr->dim[1]/nRowCol; - hdr->dim[2] = hdr->dim[2]/nRowCol; + hdr->dim[1] = hdr->dim[1]/nCol; + hdr->dim[2] = hdr->dim[2]/nRow; hdr->dim[3] = nMosaicSlices; size_t imgsz = nii_ImgBytes(*hdr); unsigned char *outImg = (unsigned char *)malloc(imgsz); @@ -2277,26 +2342,11 @@ lOutPos +=colBytes; } col ++; - if (col >= nRowCol) { + if (col >= nCol) { row ++; col = 0; } //start new column } //for m = each mosaic slice - /* //we now provide a warning once per series rather than once per volume (see nii_dicom_batch) - if (ProtocolSliceNumber1 > 1) { - printWarning("Weird CSA 'ProtocolSliceNumber': SPATIAL AND DTI TRANSFORMS UNTESTED\n"); - }*/ - /*if ((ProtocolSliceNumber1 > 1) && (hdr->dim[3] > 1)) { //exceptionally rare: reverse order of slices - now handled in matrix... - int sliceBytes = hdr->dim[1] * hdr->dim[2] * hdr->bitpix/8; - memcpy(&inImg[0], &outImg[0],sliceBytes*hdr->dim[3]); //copy data with reversed order dest, src, bytes - int lOutPos = sliceBytes * (hdr->dim[3]-1); - int lPos = 0; - for (int m=0; m < nMosaicSlices; m++) { - memcpy( &outImg[lOutPos], &inImg[lPos], sliceBytes); - lPos += sliceBytes; - lOutPos -= sliceBytes; - } - }*/ free(inImg); return outImg; } // nii_demosaic() @@ -2673,9 +2723,9 @@ int fromSlice = dti4D->sliceOrder[i]; //if (i < 10) printMessage(" ===> Moving slice from/to positions\t%d\t%d\n", i, toSlice); //printMessage(" ===> Moving slice from/to positions\t%d\t%d\n", fromSlice, i); - if ((i < 0) || (fromSlice >= dim3to7)) + if ((i < 0) || (fromSlice >= dim3to7)) { printError("Re-ordered slice out-of-volume %d\n", fromSlice); - else if (i != fromSlice) { + } else if (i != fromSlice) { uint64_t inPos = fromSlice * sliceBytes; uint64_t outPos = i * sliceBytes; memcpy( &bImg[outPos], &outImg[inPos], sliceBytes); @@ -3408,7 +3458,7 @@ img = nii_rgb2planar(img, hdr, dcm.isPlanarRGB);//do this BEFORE Y-Flip, or RGB order can be flipped dcm.isPlanarRGB = true; if (dcm.CSA.mosaicSlices > 1) { - img = nii_demosaic(img, hdr, dcm.CSA.mosaicSlices); //, dcm.CSA.protocolSliceNumber1); + img = nii_demosaic(img, hdr, dcm.CSA.mosaicSlices, (dcm.manufacturer == kMANUFACTURER_UIH)); //, dcm.CSA.protocolSliceNumber1); /* we will do this in nii_dicom_batch #ifdef obsolete_mosaic_flip img = nii_flipImgY(img, hdr); #endif*/ @@ -3564,19 +3614,19 @@ } //set_directionality0018_9075() void set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf) { - //int dcmStrInt (int lByteLength, unsigned char lBuffer[]) {//read float stored as a string - ptvd->_dtiV[0] = dcmStrInt(lLength, inbuf); - if (ptvd->_dtiV[0] > 10000) - printWarning("GE B-Value implausible. see https://github.com/rordenlab/dcm2niix/issues/149\n"); + //see Series 16 https://github.com/nikadon/cc-dcm2bids-wrapper/tree/master/dicom-qa-examples/ge-mr750-dwi-b-vals#table b750 = 1000000750\8\0\0 b1500 = 1000001500\8\0\0 + int bVal = dcmStrInt(lLength, inbuf); + bVal = (bVal % 10000); + ptvd->_dtiV[0] = bVal; + //printf("(0043,1039) '%s' Slop_int_6 -->%d \n", inbuf, bVal); //dd.CSA.numDti = 1; // Always true for GE. - _update_tvd(ptvd); } //set_bValGE() // axis: 0 -> x, 1 -> y , 2 -> z void set_diffusion_directionGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf, const int axis){ ptvd->_dtiV[axis + 1] = dcmStrFloat(lLength, inbuf); - + //printf("(0019,10bb..d) v[%d]=%g\n", axis, ptvd->_dtiV[axis + 1]); _update_tvd(ptvd); }//set_diffusion_directionGE() @@ -3668,6 +3718,38 @@ bool isImaginary; }; +void getFileName( char *pathParent, const char *path) //if path is c:\d1\d2 then filename is 'd2' +{ + const char *filename = strrchr(path, '/'); //UNIX + if (filename == 0) { + filename = strrchr(path, '\\'); //Windows + if (filename == NULL) filename = strrchr(path, ':'); //Windows + } + //const char *filename = strrchr(path, kPathSeparator); //x + if (filename == NULL) {//no path separator + strcpy(pathParent,path); + return; + } + filename++; + strncpy(pathParent,filename, kDICOMStr-1); + //strcpy(pathParent,filename); //<- this can cause overflow if filename longer than kDICOMStr +} + +#ifdef USING_R + +// True iff dcm1 sorts *before* dcm2 +bool compareTDCMdim (const TDCMdim &dcm1, const TDCMdim &dcm2) { + for (int i=MAX_NUMBER_OF_DIMENSIONS-1; i >=0; i--){ + if(dcm1.dimIdx[i] < dcm2.dimIdx[i]) + return true; + else if(dcm1.dimIdx[i] > dcm2.dimIdx[i]) + return false; + } + return false; +} //compareTDCMdim() + +#else + int compareTDCMdim(void const *item1, void const *item2) { struct TDCMdim const *dcm1 = (const struct TDCMdim *)item1; struct TDCMdim const *dcm2 = (const struct TDCMdim *)item2; @@ -3682,6 +3764,8 @@ return 0; } //compareTDCMdim() +#endif // USING_R + struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D) { struct TDICOMdata d = clear_dicom_data(); d.imageNum = 0; //not set @@ -3758,7 +3842,7 @@ #define kUnused 0x0001+(0x0001 << 16 ) #define kStart 0x0002+(0x0000 << 16 ) #define kTransferSyntax 0x0002+(0x0010 << 16) -//#define kImplementationVersionName 0x0002+(0x0013 << 16) +#define kImplementationVersionName 0x0002+(0x0013 << 16) #define kSourceApplicationEntityTitle 0x0002+(0x0016 << 16 ) //#define kSpecificCharacterSet 0x0008+(0x0005 << 16 ) //someday we should handle foreign characters... #define kImageTypeTag 0x0008+(0x0008 << 16 ) @@ -3766,7 +3850,8 @@ #define kAcquisitionDate 0x0008+(0x0022 << 16 ) #define kAcquisitionDateTime 0x0008+(0x002A << 16 ) #define kStudyTime 0x0008+(0x0030 << 16 ) -#define kAcquisitionTime 0x0008+(0x0032 << 16 ) +#define kAcquisitionTime 0x0008+(0x0032 << 16 ) //TM +#define kContentTime 0x0008+(0x0033 << 16 ) //TM #define kModality 0x0008+(0x0060 << 16 ) //CS #define kManufacturer 0x0008+(0x0070 << 16 ) #define kInstitutionName 0x0008+(0x0080 << 16 ) @@ -3794,6 +3879,8 @@ #define kZThick 0x0018+(0x0050 << 16 ) #define kTR 0x0018+(0x0080 << 16 ) #define kTE 0x0018+(0x0081 << 16 ) +#define kImagingFrequency 0x0018+(0x0084 << 16 ) //DS +#define kTriggerTime 0x0018+(0x1060 << 16 ) //DS //#define kEffectiveTE 0x0018+(0x9082 << 16 ) const uint32_t kEffectiveTE = 0x0018+ (0x9082 << 16); #define kTI 0x0018+(0x0082 << 16) // Inversion time @@ -3812,27 +3899,34 @@ #define kRadionuclidePositronFraction 0x0018+(0x1076<< 16 ) #define kGantryTilt 0x0018+(0x1120 << 16 ) #define kXRayExposure 0x0018+(0x1152 << 16 ) +#define kReceiveCoilName 0x0018+(0x1250 << 16 ) // SH #define kAcquisitionMatrix 0x0018+(0x1310 << 16 ) //US #define kFlipAngle 0x0018+(0x1314 << 16 ) #define kInPlanePhaseEncodingDirection 0x0018+(0x1312<< 16 ) //CS #define kSAR 0x0018+(0x1316 << 16 ) //'DS' 'SAR' #define kPatientOrient 0x0018+(0x5100<< 16 ) //0018,5100. patient orientation - 'HFS' +#define kAcquisitionDuration 0x0018+uint32_t(0x9073<< 16 ) //FD +//#define kFrameAcquisitionDateTime 0x0018+uint32_t(0x9074<< 16 ) //DT "20181019212528.232500" #define kDiffusionDirectionality 0x0018+uint32_t(0x9075<< 16 ) // NONE, ISOTROPIC, or DIRECTIONAL //#define kDiffusionBFactorSiemens 0x0019+(0x100C<< 16 ) // 0019;000C;SIEMENS MR HEADER ;B_value #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 kImagingFrequency2 0x0018+uint32_t(0x9098 << 16 ) //FD #define kMREchoSequence 0x0018+uint32_t(0x9114<< 16 ) //SQ +#define kMRAcquisitionPhaseEncodingStepsInPlane 0x0018+uint32_t(0x9231<< 16 ) //US #define kNumberOfImagesInMosaic 0x0019+(0x100A<< 16 ) //US NumberOfImagesInMosaic #define kDwellTime 0x0019+(0x1018<< 16 ) //IS in NSec, see https://github.com/rordenlab/dcm2niix/issues/127 +#define kNumberOfDiffusionDirectionGE 0x0019+(0x10E0<< 16) ///DS NumberOfDiffusionDirection:UserData24 #define kLastScanLoc 0x0019+(0x101B<< 16 ) -#define kDiffusionDirectionGEX 0x0019+(0x10BB<< 16 ) //DS -#define kDiffusionDirectionGEY 0x0019+(0x10BC<< 16 ) //DS -#define kDiffusionDirectionGEZ 0x0019+(0x10BD<< 16 ) //DS +#define kDiffusionDirectionGEX 0x0019+(0x10BB<< 16 ) //DS phase diffusion direction +#define kDiffusionDirectionGEY 0x0019+(0x10BC<< 16 ) //DS frequency diffusion direction +#define kDiffusionDirectionGEZ 0x0019+(0x10BD<< 16 ) //DS slice diffusion direction #define kSharedFunctionalGroupsSequence 0x5200+uint32_t(0x9229<< 16 ) // SQ #define kPerFrameFunctionalGroupsSequence 0x5200+uint32_t(0x9230<< 16 ) // SQ #define kBandwidthPerPixelPhaseEncode 0x0019+(0x1028<< 16 ) //FD +//#define kRawDataRunNumberGE 0x0019+(0x10a2<< 16 ) //SL #define kStudyID 0x0020+(0x0010 << 16 ) #define kSeriesNum 0x0020+(0x0011 << 16 ) #define kAcquNum 0x0020+(0x0012 << 16 ) @@ -3845,28 +3939,41 @@ #define kOrientation 0x0020+(0x0037 << 16 ) #define kImagesInAcquisition 0x0020+(0x1002 << 16 ) //IS #define kImageComments 0x0020+(0x4000<< 16 )// '0020' '4000' 'LT' 'ImageComments' +#define kFrameContentSequence 0x0020+uint32_t(0x9111<< 16 ) //SQ #define kTriggerDelayTime 0x0020+uint32_t(0x9153<< 16 ) //FD #define kDimensionIndexValues 0x0020+uint32_t(0x9157<< 16 ) // UL n-dimensional index of frame. #define kInStackPositionNumber 0x0020+uint32_t(0x9057<< 16 ) // UL can help determine slices in volume -#define kLocationsInAcquisitionGE 0x0021+(0x104F<< 16 )// 'SS' 'LocationsInAcquisitionGE' -#define kRTIA_timer 0x0021+(0x105E<< 16 )// 'DS' -#define kProtocolDataBlockGE 0x0025+(0x101B<< 16 )// 'OB' +//Private Group 21 as Used by Siemens: +#define kSequenceVariant21 0x0021+(0x105B<< 16 )//CS +#define kPATModeText 0x0021+(0x1009<< 16 )//LO, see kImaPATModeText +#define kTimeAfterStart 0x0021+(0x1104<< 16 )//DS +#define kPhaseEncodingDirectionPositive 0x0021+(0x111C<< 16 )//IS +//#define kRealDwellTime 0x0021+(0x1142<< 16 )//IS +#define kBandwidthPerPixelPhaseEncode21 0x0021+(0x1153<< 16 )//FD +#define kCoilElements 0x0021+(0x114F<< 16 )//LO +#define kAcquisitionMatrixText21 0x0021+(0x1158 << 16 ) //SH +//Private Group 21 as used by GE: +#define kLocationsInAcquisitionGE 0x0021+(0x104F<< 16 )//SS 'LocationsInAcquisitionGE' +#define kRTIA_timer 0x0021+(0x105E<< 16 )//DS +#define kProtocolDataBlockGE 0x0025+(0x101B<< 16 )//OB #define kSamplesPerPixel 0x0028+(0x0002 << 16 ) #define kPhotometricInterpretation 0x0028+(0x0004 << 16 ) #define kPlanarRGB 0x0028+(0x0006 << 16 ) #define kDim3 0x0028+(0x0008 << 16 ) //number of frames - for Philips this is Dim3*Dim4 #define kDim2 0x0028+(0x0010 << 16 ) #define kDim1 0x0028+(0x0011 << 16 ) -#define kXYSpacing 0x0028+(0x0030 << 16 ) //'0028' '0030' 'DS' 'PixelSpacing' +#define kXYSpacing 0x0028+(0x0030 << 16 ) //DS 'PixelSpacing' #define kBitsAllocated 0x0028+(0x0100 << 16 ) -#define kBitsStored 0x0028+(0x0101 << 16 )//'0028' '0101' 'US' 'BitsStored' -#define kIsSigned 0x0028+(0x0103 << 16 ) +#define kBitsStored 0x0028+(0x0101 << 16 )//US 'BitsStored' +#define kIsSigned 0x0028+(0x0103 << 16 ) //PixelRepresentation #define kIntercept 0x0028+(0x1052 << 16 ) #define kSlope 0x0028+(0x1053 << 16 ) +//#define kSpectroscopyDataPointColumns 0x0028+(0x9002 << 16 ) //IS #define kGeiisFlag 0x0029+(0x0010 << 16 ) //warn user if dreaded GEIIS was used to process image #define kCSAImageHeaderInfo 0x0029+(0x1010 << 16 ) #define kCSASeriesHeaderInfo 0x0029+(0x1020 << 16 ) - //#define kObjectGraphics 0x0029+(0x1210 << 16 ) //0029,1210 syngoPlatformOOGInfo Object Oriented Graphics +#define kStudyComments 0x0032+(0x4000<< 16 )//LT StudyComments +//#define kObjectGraphics 0x0029+(0x1210 << 16 ) //0029,1210 syngoPlatformOOGInfo Object Oriented Graphics #define kProcedureStepDescription 0x0040+(0x0254 << 16 ) #define kRealWorldIntercept 0x0040+uint32_t(0x9224 << 16 ) //IS dicm2nii's SlopInt_6_9 #define kRealWorldSlope 0x0040+uint32_t(0x9225 << 16 ) //IS dicm2nii's SlopInt_6_9 @@ -3882,7 +3989,14 @@ // #define kSeriesType 0x0054+(0x1000 << 16 ) #define kDoseCalibrationFactor 0x0054+(0x1322<< 16 ) #define kPETImageIndex 0x0054+(0x1330<< 16 ) +#define kPEDirectionDisplayedUIH 0x0065+(0x1005<< 16 )//SH +#define kDiffusion_bValueUIH 0x0065+(0x1009<< 16 ) //FD +#define kParallelInformationUIH 0x0065+(0x100D<< 16 ) //SH +#define kNumberOfImagesInGridUIH 0x0065+(0x1050<< 16 ) //DS +#define kMRVFrameSequenceUIH 0x0065+(0x1050<< 16 ) //SQ +#define kDiffusionGradientDirectionUIH 0x0065+(0x1037<< 16 ) //FD #define kIconImageSequence 0x0088+(0x0200 << 16 ) +#define kElscintIcon 0x07a3+(0x10ce << 16 ) //see kGeiisFlag and https://github.com/rordenlab/dcm2niix/issues/239 #define kPMSCT_RLE1 0x07a1+(0x100a << 16 ) //Elscint/Philips compression #define kDiffusionBFactor 0x2001+(0x1003 << 16 )// FL #define kSliceNumberMrPhilips 0x2001+(0x100A << 16 ) //IS Slice_Number_MR @@ -3906,6 +4020,7 @@ #define kMRImageDiffBValueNumber 0x2005+(0x1412 << 16) //IS //#define kMRImageGradientOrientationNumber 0x2005+(0x1413 << 16) //IS #define kWaveformSq 0x5400+(0x0100 << 16) +#define kSpectroscopyData 0x5600+(0x0020 << 16) //OF #define kImageStart 0x7FE0+(0x0010 << 16 ) #define kImageStartFloat 0x7FE0+(0x0008 << 16 ) #define kImageStartDouble 0x7FE0+(0x0009 << 16 ) @@ -3914,8 +4029,10 @@ uint32_t kSequenceDelimitationItemTag = 0xFFFE +(0xE0DD << 16 ); double TE = 0.0; //most recent echo time recorded bool is2005140FSQ = false; + double contentTime = 0.0; int philMRImageDiffBValueNumber = 0; int sqDepth = 0; + int acquisitionTimesGE_UIH = 0; int sqDepth00189114 = -1; int nDimIndxVal = -1; //tracks Philips kDimensionIndexValues int locationsInAcquisitionGE = 0; @@ -3953,6 +4070,9 @@ int encapsulatedDataFragmentStart = 0; //position of first FFFE,E000 for compressed images int encapsulatedDataImageStart = 0; //position of 7FE0,0010 for compressed images (where actual image start should be start of first fragment) bool isOrient = false; + //bool isDcm4Che = false; + bool isMoCo = false; + bool isInterpolated = false; bool isIconImageSequence = false; bool isSwitchToImplicitVR = false; bool isSwitchToBigEndian = false; @@ -3967,6 +4087,7 @@ bool isReal = false; bool isImaginary = false; bool isMagnitude = false; + d.seriesNum = -1; float patientPositionPrivate[4] = {NAN, NAN, NAN, NAN}; float patientPosition[4] = {NAN, NAN, NAN, NAN}; //used to compute slice direction for Philips 4D //float patientPositionPublic[4] = {NAN, NAN, NAN, NAN}; //used to compute slice direction for Philips 4D @@ -3977,7 +4098,12 @@ // philDTI[i].V[0] = -1; //array for storing DimensionIndexValues int numDimensionIndexValues = 0; +#ifdef USING_R + // Allocating a large array on the stack, as below, vexes valgrind and may cause overflow + std::vector dcmDim(kMaxSlice2D); +#else TDCMdim dcmDim[kMaxSlice2D]; +#endif for (int i = 0; i < kMaxSlice2D; i++) { dcmDim[i].diskPos = i; for (int j = 0; j < MAX_NUMBER_OF_DIMENSIONS; j++) @@ -4036,12 +4162,13 @@ is2005140FSQ = false; 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_PHILIPS) && (sqDepth00189114 >= sqDepth)) { + if (( nDimIndxVal > 0) && ((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]); if (inStackPositionNumber > 0) { //for images without SliceNumberMrPhilips (2001,100A) int sliceNumber = inStackPositionNumber; + //printf("slice %d \n", sliceNumber); if ((sliceNumber == 1) && (!isnan(patientPosition[1])) ) { for (int k = 0; k < 4; k++) patientPositionStartPhilips[k] = patientPosition[k]; @@ -4331,6 +4458,15 @@ if((slen < 6) || (strstr(impTxt, "OSIRIX") == NULL) ) break; printError("OSIRIX Detected\n"); break; }*/ + case kImplementationVersionName: { + char impTxt[kDICOMStr]; + dcmStr (lLength, &buffer[lPos], impTxt); + int slen = (int) strlen(impTxt); + //if ((slen > 5) && (strstr(impTxt, "dcm4che") != NULL) ) + // isDcm4Che = true; + if((slen < 5) || (strstr(impTxt, "XA10A") == NULL) ) break; + d.isXA10A = true; + break; } case kSourceApplicationEntityTitle: { char saeTxt[kDICOMStr]; dcmStr (lLength, &buffer[lPos], saeTxt); @@ -4338,13 +4474,32 @@ if((slen < 5) || (strstr(saeTxt, "oasis") == NULL) ) break; d.isSegamiOasis = true; break; } - case kImageTypeTag: + case kImageTypeTag: { dcmStr (lLength, &buffer[lPos], d.imageType); int slen; slen = (int) strlen(d.imageType); + if((slen > 5) && strstr(d.imageType, "_MOCO_") ) { + //d.isDerived = true; //this would have 'i- y' skip MoCo images + isMoCo = true; + } + if((slen > 5) && strstr(d.imageType, "_ADC_") ) + d.isDerived = true; + if((slen > 5) && strstr(d.imageType, "_TRACEW_") ) + d.isDerived = true; + if((slen > 5) && strstr(d.imageType, "_TRACE_") ) + d.isDerived = true; + if((slen > 5) && strstr(d.imageType, "_FA_") ) + d.isDerived = true; //if (strcmp(transferSyntax, "ORIGINAL_PRIMARY_M_ND_MOSAIC") == 0) if((slen > 5) && !strcmp(d.imageType + slen - 6, "MOSAIC") ) isMosaic = true; + //const char* prefix = "MOSAIC"; + const char *pos = strstr(d.imageType, "MOSAIC"); + //const char p = (const char *) d.imageType; + //p = (const char) strstr(d.imageType, "MOSAIC"); + //const char* p = strstr(d.imageType, "MOSAIC"); + if (pos != NULL) + isMosaic = true; //isNonImage 0008,0008 = DERIVED,CSAPARALLEL,POSDISP // sometime ComplexImageComponent 0008,9208 is missing - see ADNI data // attempt to detect non-images, see https://github.com/scitran/data/blob/a516fdc39d75a6e4ac75d0e179e18f3a5fc3c0af/scitran/data/medimg/dcm/mr/siemens.py @@ -4379,7 +4534,7 @@ //if((slen > 4) && (strstr(typestr, "DIS2D") != NULL) ) // d.isNonImage = true; //not mutually exclusive: possible for Philips enhanced DICOM to store BOTH magnitude and phase in the same image - break; + break; } case kAcquisitionDate: char acquisitionDateTxt[kDICOMStr]; dcmStr (lLength, &buffer[lPos], acquisitionDateTxt); @@ -4445,6 +4600,15 @@ char acquisitionTimeTxt[kDICOMStr]; dcmStr (lLength, &buffer[lPos], acquisitionTimeTxt); d.acquisitionTime = atof(acquisitionTimeTxt); + if (d.manufacturer != kMANUFACTURER_UIH) break; + //UIH slice timing- do not use for Siemens as Siemens de-identification can corrupt this field https://github.com/rordenlab/dcm2niix/issues/236 + d.CSA.sliceTiming[acquisitionTimesGE_UIH] = d.acquisitionTime; + acquisitionTimesGE_UIH ++; + break; + case kContentTime : + char contentTimeTxt[kDICOMStr]; + dcmStr (lLength, &buffer[lPos], contentTimeTxt); + contentTime = atof(contentTimeTxt); break; case kStudyTime : dcmStr (lLength, &buffer[lPos], d.studyTime); @@ -4457,7 +4621,7 @@ dcmStr (lLength, &buffer[lPos], aotTxt); int slen = (int) strlen(aotTxt); if((slen < 9) || (strstr(aotTxt, "QUADRUPED") == NULL) ) break; - printError("Anatomical Orientation Type (0010,2210) is QUADRUPED: rotate coordinates accordingly"); + printError("Anatomical Orientation Type (0010,2210) is QUADRUPED: rotate coordinates accordingly\n"); break; } case kPatientID : dcmStr (lLength, &buffer[lPos], d.patientID); @@ -4499,6 +4663,9 @@ } case kSoftwareVersions : { dcmStr (lLength, &buffer[lPos], d.softwareVersions); + int slen = (int) strlen(d.softwareVersions); + if((slen < 5) || (strstr(d.softwareVersions, "XA10") == NULL) ) break; + d.isXA10A = true; break; } case kProtocolName : { @@ -4508,6 +4675,16 @@ case kPatientOrient : dcmStr (lLength, &buffer[lPos], d.patientOrient); break; + case kAcquisitionDuration: + //n.b. used differently by different vendors https://github.com/rordenlab/dcm2niix/issues/225 + d.acquisitionDuration = dcmFloatDouble(lLength, &buffer[lPos],d.isLittleEndian); + break; + //in theory, 0018,9074 could provide XA10 slice time information, but scrambled by XA10 de-identification: better to use 0021,1104 + //case kFrameAcquisitionDateTime: { + // char dateTime[kDICOMStr]; + // dcmStr (lLength, &buffer[lPos], dateTime); + // printf("%s\n", dateTime); + //} case kDiffusionDirectionality : {// 0018, 9075 set_directionality0018_9075(&volDiffusion, (&buffer[lPos])); if ((d.manufacturer != kMANUFACTURER_PHILIPS) || (lLength < 10)) break; @@ -4517,9 +4694,13 @@ isPhilipsDerived = true; break; } case kMREchoSequence : + if (d.manufacturer != kMANUFACTURER_BRUKER) break; if (sqDepth == 0) sqDepth = 1; //should not happen, in case faulty anonymization sqDepth00189114 = sqDepth - 1; break; + case kMRAcquisitionPhaseEncodingStepsInPlane : + d.phaseEncodingLines = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); + break; case kNumberOfImagesInMosaic : if (d.manufacturer == kMANUFACTURER_SIEMENS) numberOfImagesInMosaic = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); @@ -4527,6 +4708,11 @@ case kDwellTime : d.dwellTime = dcmStrInt(lLength, &buffer[lPos]); break; + case kNumberOfDiffusionDirectionGE : { + if (d.manufacturer != kMANUFACTURER_GE) break; + float f = dcmStrFloat(lLength, &buffer[lPos]); + d.numberOfDiffusionDirectionGE = round(f); + break; } case kLastScanLoc : d.lastScanLoc = dcmStrFloat(lLength, &buffer[lPos]); break; @@ -4550,6 +4736,17 @@ case kBandwidthPerPixelPhaseEncode: d.bandwidthPerPixelPhaseEncode = dcmFloatDouble(lLength, &buffer[lPos],d.isLittleEndian); break; + //GE bug: multiple echos can create identical instance numbers + // in theory, one could detect as kRawDataRunNumberGE varies + // sliceN of echoE will have the same value for all timepoints + // this value does not appear indexed + // different echoes record same echo time. + // use multiEchoSortGEDICOM.py to salvage + //case kRawDataRunNumberGE : + // if (d.manufacturer != kMANUFACTURER_GE) + // break; + // d.rawDataRunNumberGE = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); + // break; case kStudyInstanceUID : // 0020, 000D dcmStr (lLength, &buffer[lPos], d.studyInstanceUID); break; @@ -4595,7 +4792,6 @@ break; case kSeriesNum: d.seriesNum = dcmStrInt(lLength, &buffer[lPos]); - break; case kAcquNum: d.acquNum = dcmStrInt(lLength, &buffer[lPos]); @@ -4606,10 +4802,16 @@ if (d.imageNum < 1) d.imageNum = dcmStrInt(lLength, &buffer[lPos]); //Philips renames each image again in 2001,9000, which can lead to duplicates break; case kInStackPositionNumber: - if (d.manufacturer != kMANUFACTURER_PHILIPS) break; + if ((d.manufacturer != kMANUFACTURER_PHILIPS) && (d.manufacturer != kMANUFACTURER_BRUKER)) break; inStackPositionNumber = dcmInt(4,&buffer[lPos],d.isLittleEndian); + //printf("<%d>\n",inStackPositionNumber); if (inStackPositionNumber > maxInStackPositionNumber) maxInStackPositionNumber = inStackPositionNumber; break; + case kFrameContentSequence : + //if (!(d.manufacturer == kMANUFACTURER_BRUKER)) break; //see https://github.com/rordenlab/dcm2niix/issues/241 + if (sqDepth == 0) sqDepth = 1; //should not happen, in case faulty anonymization + sqDepth00189114 = sqDepth - 1; + break; case kTriggerDelayTime: { //0x0020+uint32_t(0x9153<< 16 ) //FD if (d.manufacturer != kMANUFACTURER_PHILIPS) break; //if (isVerbose < 2) break; @@ -4655,13 +4857,58 @@ case kImageComments: dcmStr (lLength, &buffer[lPos], d.imageComments, true); break; + //group 21: siemens + //g21 + case kPATModeText : { //e.g. Siemens iPAT x2 listed as "p2" + char accelStr[kDICOMStr]; + dcmStr (lLength, &buffer[lPos], accelStr); + char *ptr; + dcmStrDigitsOnlyKey('p', accelStr); //e.g. if "p2s4" return "2", if "s4" return "" + d.accelFactPE = (float)strtof(accelStr, &ptr); + if (*ptr != '\0') + d.accelFactPE = 0.0; + //between slice accel + dcmStr (lLength, &buffer[lPos], accelStr); + dcmStrDigitsOnlyKey('s', accelStr); //e.g. if "p2s4" return "4", if "p2" return "" + multiBandFactor = (int)strtol(accelStr, &ptr, 10); + if (*ptr != '\0') + multiBandFactor = 0.0; + //printMessage("p%gs%d\n", d.accelFactPE, multiBandFactor); + break; } + case kTimeAfterStart: + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + if (acquisitionTimesGE_UIH >= kMaxEPI3D) break; + d.CSA.sliceTiming[acquisitionTimesGE_UIH] = dcmStrFloat(lLength, &buffer[lPos]); + //printf("%d %g\n", acquisitionTimesGE_UIH, d.CSA.sliceTiming[acquisitionTimesGE_UIH]); + acquisitionTimesGE_UIH ++; + break; + case kPhaseEncodingDirectionPositive: { + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + int ph = dcmStrInt(lLength, &buffer[lPos]); + if (ph == 0) d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; + if (ph == 1) d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; + break; } + //case kRealDwellTime : //https://github.com/rordenlab/dcm2niix/issues/240 + // if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + // d.dwellTime = dcmStrInt(lLength, &buffer[lPos]); + // break; + case kBandwidthPerPixelPhaseEncode21: + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + d.bandwidthPerPixelPhaseEncode = dcmFloatDouble(lLength, &buffer[lPos],d.isLittleEndian); + break; + case kCoilElements: + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + dcmStr (lLength, &buffer[lPos], d.coilElements); + break; + //group 21: GE case kLocationsInAcquisitionGE: locationsInAcquisitionGE = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); break; case kRTIA_timer: if (d.manufacturer != kMANUFACTURER_GE) break; //see dicm2nii slice timing from 0021,105E DS RTIA_timer - // = dcmStrFloat(lLength, &buffer[lPos]); //RefAcqTimes = t/10; end % in ms + d.rtia_timerGE = dcmStrFloat(lLength, &buffer[lPos]); //RefAcqTimes = t/10; end % in ms + //printf("%s\t%g\n", fname, d.rtia_timerGE); break; case kProtocolDataBlockGE : if (d.manufacturer != kMANUFACTURER_GE) break; @@ -4675,6 +4922,46 @@ case kPETImageIndex : PETImageIndex = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); break; + case kPEDirectionDisplayedUIH : + if (d.manufacturer != kMANUFACTURER_UIH) break; + dcmStr (lLength, &buffer[lPos], d.phaseEncodingDirectionDisplayedUIH); + break; + case kDiffusion_bValueUIH : { + if (d.manufacturer != kMANUFACTURER_UIH) break; + float v[4]; + dcmMultiFloatDouble(lLength, &buffer[lPos], 1, v, d.isLittleEndian); + d.CSA.dtiV[0] = v[0]; + d.CSA.numDti = 1; + //printf("%d>>>%g\n", lPos, v[0]); + break; } + case kParallelInformationUIH: {//SENSE factor (0065,100d) SH [F:2S] + if (d.manufacturer != kMANUFACTURER_UIH) break; + char accelStr[kDICOMStr]; + dcmStr (lLength, &buffer[lPos], accelStr); + //char *ptr; + dcmStrDigitsDotOnlyKey(':', accelStr); //e.g. if "p2s4" return "2", if "s4" return "" + d.accelFactPE = atof(accelStr); + break; } + case kNumberOfImagesInGridUIH : + if (d.manufacturer != kMANUFACTURER_UIH) break; + d.numberOfImagesInGridUIH = dcmStrFloat(lLength, &buffer[lPos]); + d.CSA.mosaicSlices = d.numberOfImagesInGridUIH; + break; + case kDiffusionGradientDirectionUIH : { //0065,1037 + //0.03712929804225321\-0.5522387869760447\-0.8328587749392602 + if (d.manufacturer != kMANUFACTURER_UIH) break; + float v[4]; + dcmMultiFloatDouble(lLength, &buffer[lPos], 3, v, d.isLittleEndian); + //dcmMultiFloat(lLength, (char*)&buffer[lPos], 3, v); + //printf(">>>%g %g %g\n", v[0], v[1], v[2]); + d.CSA.dtiV[1] = v[0]; + d.CSA.dtiV[2] = v[1]; + d.CSA.dtiV[3] = v[2]; + //vRLPhilips = v[0]; + //vAPPhilips = v[1]; + //vFHPhilips = v[2]; + break; } + case kBitsAllocated : d.bitsAllocated = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); break; @@ -4692,6 +4979,17 @@ if (d.TE <= 0.0) d.TE = TE; break; + case kImagingFrequency : + d.imagingFrequency = dcmStrFloat(lLength, &buffer[lPos]); + break; + case kTriggerTime: + //untested method to detect slice timing for GE PSD “epi” with multiphase option + // will not work for current PSD “epiRT” (BrainWave RT, fMRI/DTI package provided by Medical Numerics) + if (d.manufacturer != kMANUFACTURER_GE) break; + d.CSA.sliceTiming[acquisitionTimesGE_UIH] = dcmStrFloat(lLength, &buffer[lPos]); + //printf("%g\n", d.CSA.sliceTiming[acquisitionTimesGE_UIH]); + acquisitionTimesGE_UIH ++; + break; case kEffectiveTE : { TE = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); if (d.TE <= 0.0) @@ -4761,9 +5059,17 @@ d.TE = dcmStrFloat(lLength, &buffer[lPos]); } break; + case kReceiveCoilName : + dcmStr (lLength, &buffer[lPos], d.coilName); + if (strlen(d.coilName) < 1) break; + d.coilCrc =(long)abs( (long)mz_crc32((unsigned char*) &d.coilName, strlen(d.coilName))); + break; case kSlope : d.intenScale = dcmStrFloat(lLength, &buffer[lPos]); break; + //case kSpectroscopyDataPointColumns : + // d.xyzDim[4] = dcmInt(4,&buffer[lPos],d.isLittleEndian); + // break; case kPhilipsSlope : if ((lLength == 4) && (d.manufacturer == kMANUFACTURER_PHILIPS)) d.intenScalePhilips = dcmFloat(lLength, &buffer[lPos],d.isLittleEndian); @@ -4775,27 +5081,35 @@ d.xyzMM[3] = dcmStrFloat(lLength, &buffer[lPos]); d.zThick = d.xyzMM[3]; break; - case kAcquisitionMatrixText : { + case kAcquisitionMatrixText21: + //fall through to kAcquisitionMatrixText + case kAcquisitionMatrixText : { if (d.manufacturer == kMANUFACTURER_SIEMENS) { char matStr[kDICOMStr]; dcmStr (lLength, &buffer[lPos], matStr); char* pPosition = strchr(matStr, 'I'); if (pPosition != NULL) - printWarning("interpolated data may exhibit Gibbs ringing and be unsuitable for dwidenoise/mrdegibbs.\n"); + isInterpolated = true; } break; } case kCoilSiemens : { if (d.manufacturer == kMANUFACTURER_SIEMENS) { //see if image from single coil "H12" or an array "HEA;HEP" - char coilStr[kDICOMStr]; - dcmStr (lLength, &buffer[lPos], coilStr); - if (strlen(coilStr) < 1) break; - if (coilStr[0] == 'C') break; //kludge as Nova 32-channel defaults to "C:A32" https://github.com/rordenlab/dcm2niix/issues/187 - char *ptr; - dcmStrDigitsOnly(coilStr); - d.coilNum = (int)strtol(coilStr, &ptr, 10); - if (*ptr != '\0') - d.coilNum = 0; + //char coilStr[kDICOMStr]; + //int coilNum; + dcmStr (lLength, &buffer[lPos], d.coilName); + if (strlen(d.coilName) < 1) break; + //printf("-->%s\n", coilStr); + //d.coilName = coilStr; + //if (coilStr[0] == 'C') break; //kludge as Nova 32-channel defaults to "C:A32" https://github.com/rordenlab/dcm2niix/issues/187 + //char *ptr; + //dcmStrDigitsOnly(coilStr); + //coilNum = (int)strtol(coilStr, &ptr, 10); + d.coilCrc =(long)abs( (long)mz_crc32((unsigned char*) &d.coilName, strlen(d.coilName))); + + //printf("%d:%s\n", d.coilNum, coilStr); + //if (*ptr != '\0') + // d.coilNum = 0; } break; } case kImaPATModeText : { //e.g. Siemens iPAT x2 listed as "p2" @@ -4812,8 +5126,7 @@ multiBandFactor = (int)strtol(accelStr, &ptr, 10); if (*ptr != '\0') multiBandFactor = 0.0; - //printMessage("p%gs%d\n", d.accelFactPE, multiBandFactor); - break; } + break; } case kLocationsInAcquisition : d.locationsInAcquisition = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); break; @@ -4843,6 +5156,9 @@ dcmStr (lLength, &buffer[lPos], d.scanningSequence); break; } + case kSequenceVariant21 : + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; //see GE dataset in dcm_qa_nih + //fall through... case kSequenceVariant : { dcmStr (lLength, &buffer[lPos], d.sequenceVariant); break; @@ -4887,6 +5203,10 @@ else d.sliceOrient = kSliceOrientTra; //transverse (axial) break; } + case kElscintIcon : + printWarning("Assuming icon SQ 07a3,10ce.\n"); + isIconImageSequence = true; + break; case kPMSCT_RLE1 : if (d.compressionScheme != kCompressPMSCT_RLE1) break; d.imageStart = (int)lPos + (int)lFileOffset; @@ -4976,6 +5296,9 @@ //case kSliceNumberMrPhilips : // sliceNumberMrPhilips3D = dcmStrInt(lLength, &buffer[lPos]); // break; + case kImagingFrequency2 : + d.imagingFrequency = dcmFloatDouble(lLength, &buffer[lPos], d.isLittleEndian); + break; case kSliceNumberMrPhilips : { if (d.manufacturer != kMANUFACTURER_PHILIPS) break; @@ -5065,6 +5388,10 @@ d.imageStart = 1; //abort!!! printMessage("Skipping DICOM (audio not image) '%s'\n", fname); break; + case kSpectroscopyData: //kSpectroscopyDataPointColumns + printMessage("Skipping Spectroscopy DICOM '%s'\n", fname); + d.imageStart = (int)lPos + (int)lFileOffset; + break; case kCSAImageHeaderInfo: readCSAImageHeader(&buffer[lPos], lLength, &d.CSA, isVerbose); //, dti4D); if (!d.isHasPhase) @@ -5093,19 +5420,23 @@ break; case kUserDefineDataGE: { //0043,102A if ((d.manufacturer != kMANUFACTURER_GE) || (lLength < 128)) break; - //#define MY_DEBUG_GE // <- uncomment this to use following code to infer GE phase encoding direction + #define MY_DEBUG_GE // <- uncomment this to use following code to infer GE phase encoding direction #ifdef MY_DEBUG_GE - int isVerboseX = 2; //for debugging only - in standard release we will enable user defined "isVerbose" - //int isVerboseX = isVerbose; + int isVerboseX = isVerbose; //for debugging only - in standard release we will enable user defined "isVerbose" + //int isVerboseX = 2; if (isVerboseX > 1) printMessage(" UserDefineDataGE file offset/length %ld %u\n", lFileOffset+lPos, lLength); if (lLength < 916) { //minimum size is hdr_offset=0, read 0x0394 printMessage(" GE header too small to be valid (A)\n"); break; } //debug code to export binary data - // FILE *pFile = fopen("ge.bin", "wb"); - // fwrite(&buffer[lPos], 1, lLength, pFile); - // fclose (pFile); + /* + char str[kDICOMStr]; + sprintf(str, "%s_ge.bin",fname); + FILE *pFile = fopen(str, "wb"); + fwrite(&buffer[lPos], 1, lLength, pFile); + fclose (pFile); + */ if ((size_t)(lPos + lLength) > MaxBufferSz) { //we could re-read the buffer in this case, however in practice GE headers are concise so we never see this issue printMessage(" GE header overflows buffer\n"); @@ -5127,55 +5458,48 @@ //char const *hdr = &buffer[lPos + hdr_offset]; char *hdr = (char *)&buffer[lPos + hdr_offset]; int epi_chk_off = 0x003a; - int flag1_off = 0x0030; - int flag2_off = 0x0394; + int pepolar_off = 0x0030; + int kydir_off = 0x0394; if (version >= 25.002) { hdr += 0x004c; - flag2_off -= 0x008c; + kydir_off -= 0x008c; } + //int seqOrInter =dcmInt(2,(unsigned char*)(hdr + pepolar_off-638),true); + //int seqOrInter2 =dcmInt(2,(unsigned char*)(hdr + kydir_off-638),true); + //printf("%d %d<<<\n", seqOrInter,seqOrInter2); //check if EPI if (true) { //int check = *(short const *)(hdr + epi_chk_off) & 0x800; int check =dcmInt(2,(unsigned char*)hdr + epi_chk_off,true) & 0x800; if (check == 0) { - printf("%s: Warning: Data is not EPI\n", fname); - continue; + if (isVerboseX > 1) printMessage("%s: Warning: Data is not EPI\n", fname); + break; } } //Check for PE polarity - // int flag1 = *(short const *)(hdr + flag1_off) & 0x0004; + // int flag1 = *(short const *)(hdr + pepolar_off) & 0x0004; //Check for ky direction (view order) - // int flag2 = *(int const *)(hdr + flag2_off); - int flag1 = dcmInt(2,(unsigned char*)hdr + flag1_off,true) & 0x0004; + // int flag2 = *(int const *)(hdr + kydir_off); + int phasePolarityFlag = dcmInt(2,(unsigned char*)hdr + pepolar_off,true) & 0x0004; //Check for ky direction (view order) - int flag2 = dcmInt(2,(unsigned char*)hdr + flag2_off,true); - if (isVerboseX > 1) printMessage(" flags %d %d\n", flag1, flag2); - switch (flag2) { - case 0: - case 2: - if ((flag1 && !flag2) || (!flag1 && flag2)) { - d.phaseEncodingGE = kGE_PHASE_DIRECTION_BOTTOM_UP; - if (isVerboseX > 1) printMessage(" GE_PHASE_DIRECTION_BOTTOM_UP\n"); - - } - else { - d.phaseEncodingGE = kGE_PHASE_DIRECTION_TOP_DOWN; - if (isVerboseX > 1) printMessage(" GE_PHASE_DIRECTION_TOP_DOWN\n"); - } - break; - case 1: - if (flag1) { - d.phaseEncodingGE = kGE_PHASE_DIRECTION_CENTER_OUT_REV; - if (isVerboseX > 1) printMessage(" GE_PHASE_DIRECTION_CENTER_OUT_REV\n"); - } - else { - d.phaseEncodingGE = kGE_PHASE_DIRECTION_CENTER_OUT; - if (isVerboseX > 1) printMessage(" GE_PHASE_DIRECTION_CENTER_OUT\n"); - } - break; - default: - if (isVerboseX > 1) printMessage(" GE_PHASE_DIRECTION_UNKNOWN\n"); + int sliceOrderFlag = dcmInt(2,(unsigned char*)hdr + kydir_off,true); + if (isVerboseX > 1) + printMessage(" GE phasePolarity/sliceOrder flags %d %d\n", phasePolarityFlag, sliceOrderFlag); + if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_FLIPPED) + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; + if (phasePolarityFlag == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; + if (sliceOrderFlag == kGE_SLICE_ORDER_BOTTOM_UP) { + //https://cfmriweb.ucsd.edu/Howto/3T/operatingtips.html + if (d.phaseEncodingGE == kGE_PHASE_ENCODING_POLARITY_UNFLIPPED) + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_FLIPPED; + else + d.phaseEncodingGE = kGE_PHASE_ENCODING_POLARITY_UNFLIPPED; } + //if (sliceOrderFlag == kGE_SLICE_ORDER_TOP_DOWN) + // d.sliceOrderGE = kGE_SLICE_ORDER_TOP_DOWN; + //if (sliceOrderFlag == kGE_SLICE_ORDER_BOTTOM_UP) + // d.sliceOrderGE = kGE_SLICE_ORDER_BOTTOM_UP; #endif break; } @@ -5194,6 +5518,12 @@ //geiisBug = true; //compressed thumbnails do not follow transfer syntax! GE should not re-use pulbic tags for these proprietary images http://sonca.kasshin.net/gdcm/Doc/GE_ImageThumbnails } break; + case kStudyComments: { + //char commentStr[kDICOMStr]; + //dcmStr (lLength, &buffer[lPos], commentStr); + //printf(">> %s\n", commentStr); + break; + } case kProcedureStepDescription: dcmStr (lLength, &buffer[lPos], d.procedureStepDescription); break; @@ -5228,7 +5558,7 @@ int imgBytes = (d.xyzDim[1] * d.xyzDim[2] * int(d.bitsAllocated / 8)); if (imgBytes == lLength) isIconImageSequence = false; - if (sqDepth < 1) printWarning("Assuming 7FE0,0010 to an icon not the main image\n"); + if (sqDepth < 1) printWarning("Assuming 7FE0,0010 refers to an icon not the main image\n"); } if ((d.compressionScheme == kCompressNone ) && (!isIconImageSequence)) //do not exit for proprietary thumbnails @@ -5356,15 +5686,21 @@ d.locationsInAcquisition = locationsInAcquisitionPhilips; if ((d.manufacturer == kMANUFACTURER_GE) && (imagesInAcquisition > 0)) d.locationsInAcquisition = imagesInAcquisition; //e.g. if 72 slices acquired but interpolated as 144 + if ((d.manufacturer == kMANUFACTURER_GE) && (d.locationsInAcquisition > 0) && (locationsInAcquisitionGE > 0) && (d.locationsInAcquisition != locationsInAcquisitionGE) ) { + //printMessage("Check number of slices, discrepancy between tags (0054,0081; 0020,1002; 0021,104F)\n"); + if (d.locationsInAcquisition < locationsInAcquisitionGE) d.locationsInAcquisition = locationsInAcquisitionGE; + } if ((d.manufacturer == kMANUFACTURER_GE) && (d.locationsInAcquisition == 0)) d.locationsInAcquisition = locationsInAcquisitionGE; - if (d.zSpacing > 0) + if (d.zSpacing > 0.0) d.xyzMM[3] = d.zSpacing; //use zSpacing if provided: depending on vendor, kZThick may or may not include a slice gap //printMessage("patientPositions = %d XYZT = %d slicePerVol = %d numberOfDynamicScans %d\n",patientPositionNum,d.xyzDim[3], d.locationsInAcquisition, d.numberOfDynamicScans); if ((d.manufacturer == kMANUFACTURER_PHILIPS) && (patientPositionNum > d.xyzDim[3])) printMessage("Please check slice thicknesses: Philips R3.2.2 bug can disrupt estimation (%d positions reported for %d slices)\n",patientPositionNum, d.xyzDim[3]); //Philips reported different positions for each slice! if ((d.imageStart > 144) && (d.xyzDim[1] > 1) && (d.xyzDim[2] > 1)) d.isValid = true; + //if ((d.imageStart > 144) && (d.xyzDim[1] >= 1) && (d.xyzDim[2] >= 1) && (d.xyzDim[4] > 1)) //Spectroscopy + // d.isValid = true; if ((d.xyzMM[1] > FLT_EPSILON) && (d.xyzMM[2] < FLT_EPSILON)) { printMessage("Please check voxel size\n"); d.xyzMM[2] = d.xyzMM[1]; @@ -5373,7 +5709,6 @@ printMessage("Please check voxel size\n"); d.xyzMM[1] = d.xyzMM[2]; } - if ((d.xyzMM[3] < FLT_EPSILON)) { printMessage("Unable to determine slice thickness: please check voxel size\n"); d.xyzMM[3] = 1.0; @@ -5395,16 +5730,23 @@ } if ((numberOfImagesInMosaic > 1) && (d.CSA.mosaicSlices < 1)) d.CSA.mosaicSlices = numberOfImagesInMosaic; + if (d.isXA10A) d.manufacturer = kMANUFACTURER_SIEMENS; //XA10A mosaics omit Manufacturer 0008,0070! if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (isMosaic) && (d.CSA.mosaicSlices < 1) && (d.phaseEncodingSteps > 0) && ((d.xyzDim[1] % d.phaseEncodingSteps) == 0) && ((d.xyzDim[2] % d.phaseEncodingSteps) == 0) ) { d.CSA.mosaicSlices = (d.xyzDim[1] / d.phaseEncodingSteps) * (d.xyzDim[2] / d.phaseEncodingSteps); printWarning("Mosaic inferred without CSA header (check number of slices and spatial orientation)\n"); } + if ((d.isXA10A) && (isMosaic) && (d.CSA.mosaicSlices < 1)) + d.CSA.mosaicSlices = -1; //mark as bogus DICOM + if ((!d.isXA10A) && (isMosaic) && (d.CSA.mosaicSlices < 1)) //See Erlangen Vida dataset - never reports "XA10" but mosaics have no attributes + printWarning("0008,0008=MOSAIC but number of slices not specified: %s\n", fname); if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.dtiV[1] < -1.0) && (d.CSA.dtiV[2] < -1.0) && (d.CSA.dtiV[3] < -1.0)) d.CSA.dtiV[0] = 0; //SiemensTrio-Syngo2004A reports B=0 images as having impossible b-vectors. if ((d.manufacturer == kMANUFACTURER_GE) && (strlen(d.seriesDescription) > 1)) //GE uses a generic session name here: do not overwrite kProtocolNameGE strcpy(d.protocolName, d.seriesDescription); if ((strlen(d.protocolName) < 1) && (strlen(d.seriesDescription) > 1)) strcpy(d.protocolName, d.seriesDescription); + if ((strlen(d.protocolName) > 1) && (isMoCo)) + strcat (d.protocolName,"_MoCo"); //disambiguate MoCo https://github.com/neurolabusc/MRIcroGL/issues/31 if ((strlen(d.protocolName) < 1) && (strlen(d.sequenceName) > 1) && (d.manufacturer != kMANUFACTURER_SIEMENS)) strcpy(d.protocolName, d.sequenceName); //protocolName (0018,1030) optional, sequence name (0018,0024) is not a good substitute for Siemens as it can vary per volume: *ep_b0 *ep_b1000#1, *ep_b1000#2, etc https://www.nitrc.org/forum/forum.php?thread_id=8771&forum_id=4703 // if (!isOrient) { @@ -5431,7 +5773,6 @@ d.xyzDim[3] = d.numberOfDynamicScans; }*/ - if (numberOfFrames == 0) numberOfFrames = d.xyzDim[3]; if ((locationsInAcquisitionPhilips > 0) && ((d.xyzDim[3] % locationsInAcquisitionPhilips) == 0)) { d.xyzDim[4] = d.xyzDim[3] / locationsInAcquisitionPhilips; @@ -5459,11 +5800,10 @@ if (!isnan(patientPositionEndPhilips[1])) printMessage(" patient position end (0020,0032)\t%g\t%g\t%g\n", patientPositionEndPhilips[1],patientPositionEndPhilips[2],patientPositionEndPhilips[3]); printMessage(" orient (0020,0037)\t%g\t%g\t%g\t%g\t%g\t%g\n", d.orient[1],d.orient[2],d.orient[3], d.orient[4],d.orient[5],d.orient[6]); - printMessage(" acq %d img %d ser %ld dim %dx%dx%dx%d mm %gx%gx%g offset %d loc %d valid %d ph %d mag %d nDTI %d 3d %d bits %d littleEndian %d echo %d coil %d TE %g TR %g\n",d.acquNum,d.imageNum,d.seriesNum,d.xyzDim[1],d.xyzDim[2],d.xyzDim[3], d.xyzDim[4],d.xyzMM[1],d.xyzMM[2],d.xyzMM[3],d.imageStart, d.locationsInAcquisition, d.isValid, d.isHasPhase, d.isHasMagnitude, d.CSA.numDti, d.is3DAcq, d.bitsAllocated, d.isLittleEndian, d.echoNum, d.coilNum, d.TE, d.TR); + printMessage(" acq %d img %d ser %ld dim %dx%dx%dx%d mm %gx%gx%g offset %d loc %d valid %d ph %d mag %d nDTI %d 3d %d bits %d littleEndian %d echo %d coilCRC %d TE %g TR %g\n",d.acquNum,d.imageNum,d.seriesNum,d.xyzDim[1],d.xyzDim[2],d.xyzDim[3], d.xyzDim[4],d.xyzMM[1],d.xyzMM[2],d.xyzMM[3],d.imageStart, d.locationsInAcquisition, d.isValid, d.isHasPhase, d.isHasMagnitude, d.CSA.numDti, d.is3DAcq, d.bitsAllocated, d.isLittleEndian, d.echoNum, d.coilCrc, d.TE, d.TR); //if (d.CSA.dtiV[0] > 0) // printMessage(" DWI bxyz %g %g %g %g\n", d.CSA.dtiV[0], d.CSA.dtiV[1], d.CSA.dtiV[2], d.CSA.dtiV[3]); } - if ((numDimensionIndexValues > 1) && (numDimensionIndexValues == numberOfFrames)) { //Philips enhanced datasets can have custom slice orders and pack images with different TE, Phase/Magnitude/Etc. if (isVerbose > 1) { // @@ -5482,8 +5822,14 @@ if (mn[i] != mx[i]) printMessage(" Dimension %d Range: %d..%d\n", i, mn[i], mx[i]); } //verbose > 1 + if (d.manufacturer != kMANUFACTURER_BRUKER) { //only single sample Bruker - perhaps use 0020,9057 to identify if space or time is 3rd dimension //sort dimensions - qsort(dcmDim, numberOfFrames, sizeof(struct TDCMdim), compareTDCMdim); +#ifdef USING_R + std::sort(dcmDim.begin(), dcmDim.begin() + numberOfFrames, compareTDCMdim); +#else + qsort(dcmDim, numberOfFrames, sizeof(struct TDCMdim), compareTDCMdim); +#endif + } //for (int i = 0; i < numberOfFrames; i++) // printf("%d -> %d %d %d %d\n", i, dcmDim[i].diskPos, dcmDim[i].dimIdx[1], dcmDim[i].dimIdx[2], dcmDim[i].dimIdx[3]); for (int i = 0; i < numberOfFrames; i++) @@ -5532,7 +5878,16 @@ printMessage(" %d TE=%g Slope=%g Inter=%g PhilipsScale=%g Phase=%d\n", i, dti4D->TE[i], dti4D->intenScale[i], dti4D->intenIntercept[i], dti4D->intenScalePhilips[i], dti4D->isPhase[i] ); } } - } + if ((d.xyzDim[3] == maxInStackPositionNumber) && (maxInStackPositionNumber > 1) && (d.zSpacing <= 0.0)) { + float dx = sqrt( pow(d.patientPosition[1]-d.patientPositionLast[1],2)+ + pow(d.patientPosition[2]-d.patientPositionLast[2],2)+ + pow(d.patientPosition[3]-d.patientPositionLast[3],2)); + dx = dx / (maxInStackPositionNumber - 1); + if ((dx > 0.0) && (!isSameFloatGE(dx, d.xyzMM[3])) ) //patientPosition has some rounding error + d.xyzMM[3] = dx; + } //d.zSpacing <= 0.0: Bruker does not populate 0018,0088 https://github.com/rordenlab/dcm2niix/issues/241 + } //if numDimensionIndexValues > 1 : enhanced DICOM + /* //Attempt to append ADC printMessage("CXC grad %g %d %d\n", philDTI[0].V[0], maxGradNum, d.xyzDim[4]); if ((maxGradNum > 1) && ((maxGradNum+1) == d.xyzDim[4]) ) { @@ -5582,13 +5937,24 @@ // for examples see https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging d.seriesNum += (philMRImageDiffBValueNumber*1000); } + //if (contentTime != 0.0) && (numDimensionIndexValues < (MAX_NUMBER_OF_DIMENSIONS - 1)){ + // long timeCRC = abs( (long)mz_crc32((unsigned char*) &contentTime, sizeof(double))); + //} + if ((isInterpolated) && (d.imageNum <= 1)) + printWarning("interpolated protocol '%s' may be unsuitable for dwidenoise/mrdegibbs. %s\n", d.protocolName, fname); + if (numDimensionIndexValues < MAX_NUMBER_OF_DIMENSIONS) //https://github.com/rordenlab/dcm2niix/issues/221 + d.dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS-1] = (uint32_t)abs( (long)mz_crc32((unsigned char*) &d.seriesInstanceUID, strlen(d.seriesInstanceUID))); + if (d.seriesNum < 1) //https://github.com/rordenlab/dcm2niix/issues/218 + d.seriesNum = (long)abs( (long)mz_crc32((unsigned char*) &d.seriesInstanceUID, strlen(d.seriesInstanceUID))); + getFileName(d.imageBaseName, fname); if (multiBandFactor > d.CSA.multiBandFactor) d.CSA.multiBandFactor = multiBandFactor; //SMS reported in 0051,1011 but not CSA header #ifndef myLoadWholeFileToReadHeader fclose(file); #endif - //printMessage("buffer usage %d %d %d\n",d.imageStart, lPos+lFileOffset, MaxBufferSz); - return d; + //printf("%g\t\t%g\t%g\t%g\t%s\n", d.CSA.dtiV[0], d.CSA.dtiV[1], d.CSA.dtiV[2], d.CSA.dtiV[3], fname); + //printMessage("buffer usage %d %d %d\n",d.imageStart, lPos+lFileOffset, MaxBufferSz); + return d; } // readDICOM() struct TDICOMdata readDICOM(char * fname) { diff -Nru dcm2niix-1.0.20180622/console/nii_dicom.h dcm2niix-1.0.20181125/console/nii_dicom.h --- dcm2niix-1.0.20180622/console/nii_dicom.h 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_dicom.h 2018-11-25 21:24:07.000000000 +0000 @@ -2,7 +2,7 @@ #include #include #include "nifti1_io_core.h" -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif @@ -43,7 +43,7 @@ #define kCCsuf " CompilerNA" //unknown compiler! #endif -#define kDCMvers "v1.0.20180622" kJP2suf kLSsuf kCCsuf +#define kDCMvers "v1.0.20181125 " kJP2suf kLSsuf kCCsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic static const int kMaxDTI4D = 18000; //maximum number of DTI directions for 4D (Philips) images, also maximum number of 3D slices for Philips 3D and 4D images @@ -56,6 +56,8 @@ #define kMANUFACTURER_GE 2 #define kMANUFACTURER_PHILIPS 3 #define kMANUFACTURER_TOSHIBA 4 +#define kMANUFACTURER_UIH 5 +#define kMANUFACTURER_BRUKER 6 //note: note a complete modality list, e.g. XA,PX, etc #define kMODALITY_UNKNOWN 0 @@ -66,11 +68,16 @@ #define kMODALITY_US 5 //GE phase encoding -#define kGE_PHASE_DIRECTION_UNKNOWN 0 -#define kGE_PHASE_DIRECTION_BOTTOM_UP 1 -#define kGE_PHASE_DIRECTION_TOP_DOWN 2 -#define kGE_PHASE_DIRECTION_CENTER_OUT_REV 3 -#define kGE_PHASE_DIRECTION_CENTER_OUT 4 +#define kGE_PHASE_ENCODING_POLARITY_UNKNOWN -1 +#define kGE_PHASE_ENCODING_POLARITY_UNFLIPPED 0 +#define kGE_PHASE_ENCODING_POLARITY_FLIPPED 4 +#define kGE_SLICE_ORDER_UNKNOWN -1 +#define kGE_SLICE_ORDER_TOP_DOWN 0 +#define kGE_SLICE_ORDER_BOTTOM_UP 2 + + +//#define kGE_PHASE_DIRECTION_CENTER_OUT_REV 3 +//#define kGE_PHASE_DIRECTION_CENTER_OUT 4 #define kEXIT_NO_VALID_FILES_FOUND 2 static const int kSliceOrientUnknown = 0; @@ -98,7 +105,6 @@ // Maximum number of dimensions for .dimensionIndexValues, i.e. possibly the // number of axes in the output .nii. static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; - struct TDTI { float V[4]; //int totalSlicesIn4DOrder; @@ -149,31 +155,27 @@ struct TDICOMdata { long seriesNum; int xyzDim[5]; - //numberOfDynamicScans, patientPositionNumPhilips - //patientPositionSequentialRepeats,patientPositionRepeats, - //maxGradDynVol, gradDynVol, - int phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, echoTrainLength, coilNum, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, compressionScheme; - float patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4]; + uint32_t coilCrc; + int numberOfImagesInGridUIH, numberOfDiffusionDirectionGE, phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, echoTrainLength, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, compressionScheme; + float imagingFrequency, patientWeight, zSpacing, zThick, pixelBandwidth, SAR, phaseFieldofView, accelFactPE, flipAngle, fieldStrength, TE, TI, TR, intenScale, intenIntercept, intenScalePhilips, gantryTilt, lastScanLoc, angulation[4]; float orient[7], patientPosition[4], patientPositionLast[4], xyzMM[4], stackOffcentre[4]; - float radionuclidePositronFraction, radionuclideTotalDose, radionuclideHalfLife, doseCalibrationFactor; //PET ISOTOPE MODULE ATTRIBUTES (C.8-57) + float rtia_timerGE, radionuclidePositronFraction, radionuclideTotalDose, radionuclideHalfLife, doseCalibrationFactor; //PET ISOTOPE MODULE ATTRIBUTES (C.8-57) float ecat_isotope_halflife, ecat_dosage; - double triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; - //char mrAcquisitionType[kDICOMStr] - char scanOptions[kDICOMStr], stationName[kDICOMStr], softwareVersions[kDICOMStr], deviceSerialNumber[kDICOMStr], institutionAddress[kDICOMStr], institutionName[kDICOMStr], referringPhysicianName[kDICOMStr], seriesInstanceUID[kDICOMStr], studyInstanceUID[kDICOMStr], bodyPartExamined[kDICOMStr], procedureStepDescription[kDICOMStr], imageType[kDICOMStr], institutionalDepartmentName[kDICOMStr], manufacturersModelName[kDICOMStr], patientID[kDICOMStr], patientOrient[kDICOMStr], patientName[kDICOMStr],seriesDescription[kDICOMStr], studyID[kDICOMStr], sequenceName[kDICOMStr], protocolName[kDICOMStr],sequenceVariant[kDICOMStr],scanningSequence[kDICOMStr], patientBirthDate[kDICOMStr], patientAge[kDICOMStr], studyDate[kDICOMStr],studyTime[kDICOMStr]; + double acquisitionDuration, triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; + char coilElements[kDICOMStr], coilName[kDICOMStr], phaseEncodingDirectionDisplayedUIH[kDICOMStr], imageBaseName[kDICOMStr], scanOptions[kDICOMStr], stationName[kDICOMStr], softwareVersions[kDICOMStr], deviceSerialNumber[kDICOMStr], institutionAddress[kDICOMStr], institutionName[kDICOMStr], referringPhysicianName[kDICOMStr], seriesInstanceUID[kDICOMStr], studyInstanceUID[kDICOMStr], bodyPartExamined[kDICOMStr], procedureStepDescription[kDICOMStr], imageType[kDICOMStr], institutionalDepartmentName[kDICOMStr], manufacturersModelName[kDICOMStr], patientID[kDICOMStr], patientOrient[kDICOMStr], patientName[kDICOMStr],seriesDescription[kDICOMStr], studyID[kDICOMStr], sequenceName[kDICOMStr], protocolName[kDICOMStr],sequenceVariant[kDICOMStr],scanningSequence[kDICOMStr], patientBirthDate[kDICOMStr], patientAge[kDICOMStr], studyDate[kDICOMStr],studyTime[kDICOMStr]; char imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; struct TCSAdata CSA; - //isSlicesSpatiallySequentialPhilips - bool isSegamiOasis, isScaleOrTEVaries, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; + bool isCoilVaries, isNonParallelSlices, isSegamiOasis, isXA10A, isScaleOrTEVaries, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; char phaseEncodingRC, patientSex; - //uint32_t *totalSlicesIn4DOrder; //Reordering array for Philips slices }; size_t nii_ImgBytes(struct nifti_1_header hdr); int isSameFloatGE (float a, float b); + void getFileName( char *pathParent, const char *path); struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D); struct TDICOMdata readDICOM(char * fname); - struct TDICOMdata clear_dicom_data(); + struct TDICOMdata clear_dicom_data(void); struct TDICOMdata nii_readParRec (char * parname, int isVerbose, struct TDTI4D *dti4D, bool isReadPhase); unsigned char * nii_flipY(unsigned char* bImg, struct nifti_1_header *h); unsigned char * nii_flipZ(unsigned char* bImg, struct nifti_1_header *h); diff -Nru dcm2niix-1.0.20180622/console/nii_ortho.cpp dcm2niix-1.0.20181125/console/nii_ortho.cpp --- dcm2niix-1.0.20180622/console/nii_ortho.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_ortho.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -1,4 +1,4 @@ -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif #include "nifti1_io_core.h" diff -Nru dcm2niix-1.0.20180622/console/nii_ortho.h dcm2niix-1.0.20181125/console/nii_ortho.h --- dcm2niix-1.0.20180622/console/nii_ortho.h 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/nii_ortho.h 2018-11-25 21:24:07.000000000 +0000 @@ -5,7 +5,7 @@ extern "C" { #endif -#ifndef HAVE_R +#ifndef USING_R #include "nifti1.h" #endif diff -Nru dcm2niix-1.0.20180622/console/print.h dcm2niix-1.0.20181125/console/print.h --- dcm2niix-1.0.20180622/console/print.h 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/print.h 2018-11-25 21:24:07.000000000 +0000 @@ -8,7 +8,7 @@ #ifndef _R_PRINT_H_ #define _R_PRINT_H_ #include - #ifdef HAVE_R + #ifdef USING_R #define R_USE_C99_IN_CXX #include #define printMessage(...) { Rprintf("[dcm2niix info] "); Rprintf(__VA_ARGS__); } @@ -48,5 +48,5 @@ // #define printError(...) ({ printMessage("Error: "); printMessage(__VA_ARGS__);}) #define printWarning(...) do {printMessage("Warning: "); printMessage(__VA_ARGS__);} while(0) - #endif //HAVE_R + #endif //USING_R #endif //_R_PRINT_H_ diff -Nru dcm2niix-1.0.20180622/console/ujpeg.cpp dcm2niix-1.0.20181125/console/ujpeg.cpp --- dcm2niix-1.0.20180622/console/ujpeg.cpp 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/console/ujpeg.cpp 2018-11-25 21:24:07.000000000 +0000 @@ -347,9 +347,19 @@ #define W6 1108 #define W7 565 +#ifdef USING_R +NJ_INLINE int shiftLeft(int i, int p) { + unsigned u = *(unsigned *)(&i); + u <<= p; + return int(u); +} +#else +#define shiftLeft(i,p) (i) << (p) +#endif + NJ_INLINE void njRowIDCT(int* blk) { int x0, x1, x2, x3, x4, x5, x6, x7, x8; - if (!((x1 = blk[4] << 11) + if (!((x1 = shiftLeft(blk[4], 11)) | (x2 = blk[6]) | (x3 = blk[2]) | (x4 = blk[1]) @@ -357,10 +367,11 @@ | (x6 = blk[5]) | (x7 = blk[3]))) { - blk[0] = blk[1] = blk[2] = blk[3] = blk[4] = blk[5] = blk[6] = blk[7] = blk[0] << 3; + blk[0] = blk[1] = blk[2] = blk[3] = blk[4] = blk[5] = blk[6] = blk[7] = shiftLeft(blk[0], 3); return; } - x0 = (blk[0] << 11) + 128; + x0 = shiftLeft(blk[0], 11); + x0 += 128; x8 = W7 * (x4 + x5); x4 = x8 + (W1 - W7) * x4; x5 = x8 - (W1 + W7) * x5; @@ -394,7 +405,7 @@ NJ_INLINE void njColIDCT(const int* blk, unsigned char *out, int stride) { int x0, x1, x2, x3, x4, x5, x6, x7, x8; - if (!((x1 = blk[8*4] << 8) + if (!((x1 = shiftLeft(blk[8*4], 8)) | (x2 = blk[8*6]) | (x3 = blk[8*2]) | (x4 = blk[8*1]) @@ -409,7 +420,8 @@ } return; } - x0 = (blk[0] << 8) + 8192; + x0 = shiftLeft(blk[0], 8); + x0 += 8192; x8 = W7 * (x4 + x5) + 4; x4 = (x8 + (W1 - W7) * x4) >> 3; x5 = (x8 - (W1 + W7) * x5) >> 3; @@ -449,14 +461,14 @@ if (!bits) return 0; while (nj.bufbits < bits) { if (nj.size <= 0) { - nj.buf = (nj.buf << 8) | 0xFF; + nj.buf = shiftLeft(nj.buf, 8) | 0xFF; nj.bufbits += 8; continue; } newbyte = *nj.pos++; nj.size--; nj.bufbits += 8; - nj.buf = (nj.buf << 8) | newbyte; + nj.buf = shiftLeft(nj.buf, 8) | newbyte; if (newbyte == 0xFF) { if (nj.size) { unsigned char marker = *nj.pos++; @@ -470,7 +482,7 @@ if ((marker & 0xF8) != 0xD0) nj.error = NJ_SYNTAX_ERROR; else { - nj.buf = (nj.buf << 8) | marker; + nj.buf = shiftLeft(nj.buf, 8) | marker; nj.bufbits += 8; } } @@ -556,16 +568,16 @@ c = nj.comp; c->ssx = c->ssy = ssxmax = ssymax = 1; } - nj.mbsizex = ssxmax << 3; - nj.mbsizey = ssymax << 3; + nj.mbsizex = shiftLeft(ssxmax, 3); + nj.mbsizey = shiftLeft(ssymax, 3); nj.mbwidth = (nj.width + nj.mbsizex - 1) / nj.mbsizex; nj.mbheight = (nj.height + nj.mbsizey - 1) / nj.mbsizey; for (i = 0, c = nj.comp; i < nj.ncomp; ++i, ++c) { c->width = (nj.width * c->ssx + ssxmax - 1) / ssxmax; c->height = (nj.height * c->ssy + ssymax - 1) / ssymax; - c->stride = nj.mbwidth * c->ssx << 3; + c->stride = nj.mbwidth * shiftLeft(c->ssx, 3); if (((c->width < 3) && (c->ssx != ssxmax)) || ((c->height < 3) && (c->ssy != ssymax))) njThrow(NJ_UNSUPPORTED); - if (!(c->pixels = (unsigned char*) njAllocMem(c->stride * nj.mbheight * c->ssy << 3))) njThrow(NJ_OUT_OF_MEM); + if (!(c->pixels = (unsigned char*) njAllocMem(c->stride * nj.mbheight * shiftLeft(c->ssy, 3)))) njThrow(NJ_OUT_OF_MEM); } if (nj.ncomp == 3) { nj.rgb = (unsigned char*) njAllocMem(nj.width * nj.height * nj.ncomp); @@ -595,7 +607,7 @@ currcnt = counts[codelen - 1]; if (!currcnt) continue; if (nj.length < currcnt) njThrow(NJ_SYNTAX_ERROR); - remain -= currcnt << (16 - codelen); + remain -= shiftLeft(currcnt, 16 - codelen); if (remain < 0) njThrow(NJ_SYNTAX_ERROR); for (i = 0; i < currcnt; ++i) { unsigned char code = nj.pos[i]; @@ -651,7 +663,7 @@ if (!bits) return 0; value = njGetBits(bits); if (value < (1 << (bits - 1))) - value += ((-1) << bits) + 1; + value += (shiftLeft(-1, bits)) + 1; return value; } @@ -697,7 +709,7 @@ for (i = 0, c = nj.comp; i < nj.ncomp; ++i, ++c) for (sby = 0; sby < c->ssy; ++sby) for (sbx = 0; sbx < c->ssx; ++sbx) { - njDecodeBlock(c, &c->pixels[((mby * c->ssy + sby) * c->stride + mbx * c->ssx + sbx) << 3]); + njDecodeBlock(c, &c->pixels[shiftLeft((mby * c->ssy + sby) * c->stride + mbx * c->ssx + sbx, 3)]); njCheckError(); } if (++mbx >= nj.mbwidth) { diff -Nru dcm2niix-1.0.20180622/debian/changelog dcm2niix-1.0.20181125/debian/changelog --- dcm2niix-1.0.20180622/debian/changelog 2018-12-11 05:12:15.000000000 +0000 +++ dcm2niix-1.0.20181125/debian/changelog 2019-01-30 09:37:36.000000000 +0000 @@ -1,14 +1,11 @@ -dcm2niix (1.0.20180622-1build2) disco; urgency=medium +dcm2niix (1.0.20181125-1) unstable; urgency=medium - * No change rebuild against libyaml-cpp0.6 + * Team upload. + * New upstream version + * debhelper 12 + * Standards-Version: 4.3.0 - -- Christopher James Halse Rogers Tue, 11 Dec 2018 16:12:15 +1100 - -dcm2niix (1.0.20180622-1build1) disco; urgency=high - - * No change rebuild against yaml-cpp abi break. - - -- Julian Andres Klode Wed, 21 Nov 2018 18:04:56 +0100 + -- Andreas Tille Wed, 30 Jan 2019 10:37:36 +0100 dcm2niix (1.0.20180622-1) unstable; urgency=medium diff -Nru dcm2niix-1.0.20180622/debian/compat dcm2niix-1.0.20181125/debian/compat --- dcm2niix-1.0.20180622/debian/compat 2018-09-18 12:33:27.000000000 +0000 +++ dcm2niix-1.0.20181125/debian/compat 2019-01-30 09:37:36.000000000 +0000 @@ -1 +1 @@ -11 +12 diff -Nru dcm2niix-1.0.20180622/debian/control dcm2niix-1.0.20181125/debian/control --- dcm2niix-1.0.20180622/debian/control 2018-11-21 17:04:56.000000000 +0000 +++ dcm2niix-1.0.20181125/debian/control 2019-01-30 09:37:36.000000000 +0000 @@ -1,18 +1,17 @@ Source: dcm2niix -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Debian Med Packaging Team +Maintainer: Debian Med Packaging Team Uploaders: Ghislain Antony Vaillant Section: science Priority: optional Build-Depends: cmake, - debhelper (>= 11), + debhelper (>= 12~), libopenjp2-7-dev, libturbojpeg0-dev, libyaml-cpp-dev, pkg-config, python3-sphinx, zlib1g-dev -Standards-Version: 4.2.1 +Standards-Version: 4.3.0 Vcs-Browser: https://salsa.debian.org/med-team/dcm2niix Vcs-Git: https://salsa.debian.org/med-team/dcm2niix.git Homepage: https://github.com/rordenlab/dcm2niix diff -Nru dcm2niix-1.0.20180622/docs/source/conf.py dcm2niix-1.0.20181125/docs/source/conf.py --- dcm2niix-1.0.20180622/docs/source/conf.py 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/docs/source/conf.py 2018-11-25 21:24:07.000000000 +0000 @@ -302,7 +302,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('dcm2niix', 'dcm2niix', u'DICOM to NifTI converter', - ['This manual was developed and is maintained by Ghislain Antony Vaillant '], 1), + ['This manual was developed by Ghislain Antony Vaillant and is maintained by the community'], 1), ('dcm2niibatch', 'dcm2niibatch', u'DICOM to NifTI batch converter', ['This manual was developed and is maintained by Benjamin Irving '], 1) ] diff -Nru dcm2niix-1.0.20180622/docs/source/dcm2niix.rst dcm2niix-1.0.20181125/docs/source/dcm2niix.rst --- dcm2niix-1.0.20180622/docs/source/dcm2niix.rst 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/docs/source/dcm2niix.rst 2018-11-25 21:24:07.000000000 +0000 @@ -26,14 +26,16 @@ -1..-9 gz compression level (1=fastest..9=smallest, default 6) --b Save additional BIDS metadata to a side-car .json file. +-b Save additional BIDS metadata to a side-car .json file. + The "i"nput-only option reads DICOMs but saves neither BIDS nor NIfTI. -ba anonymize BIDS -f Format string for the output filename(s). The following specifiers are supported: - - %a, antenna (coil) number + - %a, antenna (coil) name + - %b, basename (filename of 1st DICOM file) - %c, comments - %d, description - %e, echo number @@ -44,6 +46,7 @@ - %m, manufacturer - %n, patient name - %p, protocol + - %r, instance number (of 1st DICOM file) - %s, series number - %t, time - %u, acquisition number @@ -55,6 +58,8 @@ -i Ignore derived, localizer and 2D images. +-l Losslessly scale 16-bit integers to use maximal dynamic range. + -m Merge slices from the same series regardless of study time, echo, coil, orientation, etc... @@ -67,14 +72,18 @@ -p Use Philips precise float (rather than display) scaling. +-r Rename instead of convert DICOMs. Useful for organizing images. + -s Convert a single file only. --t Save patient details. +-t Save patient details as text notes. + +-u Update check: attempts to see if newer version is available. -v Enable verbose output. "n" for succinct, "y" for verbose, "h" for high verbosity --x Crop images. +-x Crop images. This will attempt to remove excess neck from 3D acquisitions. -z Desired compression method. The "y"es option uses the external program pigz if available. The "i" option compresses the image @@ -86,3 +95,4 @@ Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. +The dcm2niix project is distributed under the BSD 2-Clause License. \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/FILENAMING.md dcm2niix-1.0.20181125/FILENAMING.md --- dcm2niix-1.0.20180622/FILENAMING.md 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/FILENAMING.md 2018-11-25 21:24:07.000000000 +0000 @@ -1,14 +1,60 @@ ## About +DICOM files tend to have bizarre file names, for example based on the instance UID, e.g. `MR.1.3.12.2.1107.5.2.32.35131.2014031013003871821190579`. In addition, DICOM images are often 2D slices or 3D volumes that we will combine into a single unified NIfTI file. On the other hand, some enhanced DICOM images save different reconstructions (e.g. phase and magnitude) of the same image that we will want to save as separate NIfTI files. Therefore, dcm2niix attempts to provide a sensible file naming scheme. + +## Basics + +You request the output filename with the `-f` argument. For example, consider you convert files with `dcm2niix -f %s_%p`: in this case an image from series 3 with the protocol name `T1` will be saved as `3_T1.nii`. Here are the available parameters for file names: + + - %a=antenna (coil) name (from Siemens 0051,100F) + - %b=basename (filename of first DICOM) + - %c=comments (from 0020,4000) + - %d=description (from 0008,103E) + - %e=echo number (from 0018,0086) + - %f=folder name (name of folder containing first DICOM) + - %i=ID of patient (from 0010,0020) + - %j=seriesInstanceUID (from 0020,000E) + - %k=studyInstanceUID (from 0020,000D) + - %m=manufacturer short name (from 0008,0070: GE, Ph, Si, To, UI, NA) + - %n=name of patient (from 0010,0010) + - %p=protocol name (from 0018,1030) + - %r=instance number (from 0020,0013) + - %s=series number (from 0020,0011) + - %t=time of study (from 0008,0020 and 0008,0030) + - %u=acquisition number (from 0020,0012) + - %v=vendor long name (from 0008,0070: GE, Philips, Siemens, Toshiba, UIH, NA) + - %x=study ID (from 0020,0010) + - %z=sequence name (from 0018,0024) + +## Filename Post-fixes: Image Disambiguation + In general dcm2niix creates images with 3D dimensions, or 4 dimensions when the 4th dimension is time (fMRI) or gradient number (DWI). It will use the following extensions to disambiguate additional dimensions from the same series: - - _c1.._cN coil ID (only seen if saved independently for each coil) - - _e2..eN additional echoes (the first echo is implicit) + - _cNx.._cNz where C* refers to the coil name (typically only seen for uncombined data, where a separate image is generated for each antenna) + - _e1..eN echo number for multi-echo sequences - _ph phase map - _imaginary imaginary component of complex image - _real real component of complex image - _phMag rare case where phase and magnitude are saved as the 4th dimension - _t If the trigger delay time (0020,9153) is non-zero, it will be recorded in the filename. For example, the files "T1_t178.nii" and "T1_t511" suggests that the T1 scan was acquired with two cardiac trigger delays (178 and 511ms after the last R-peak). - - _ADC Philips specific case DWI image where derived isotropic, ADC or trace volume was appended to the series. Since this image will disrupt subsequent processing, and because subsequent processing (dwidenoise, topup, eddy) will yield better derived images, dcm2niix will also create an additional image without this volume. Therefore, the _ADC file should typically be discarded. If you want dcm2niix to discard these useless derived images , use the ignore feature ('-i y'). + - _ADC Philips specific case. A DWI image where derived isotropic, ADC or trace volume was appended to the series. Since this image will disrupt subsequent processing, and because subsequent processing (dwidenoise, topup, eddy) will yield better derived images, dcm2niix will also create an additional image without this volume. Therefore, the _ADC file should typically be discarded. If you want dcm2niix to discard these useless derived images, use the ignore feature ('-i y'). - _Eq is specific to [CT scans](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Computed_Tomography_.28CT.2C_CAT.29). These scans can be acquired with variable distance between the slices of a 3D volume. NIfTI asumes all 2D slices that form a 3D stack are equidistant. Therefore, dcm2niix reslices the input data to generate an equidistant volume. - _Tilt is specific to [CT scans](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Computed_Tomography_.28CT.2C_CAT.29). These scans can be acquired with a gantry tilt that causes a skew that can not be stored in a NIfTI qForm. Therefore, the slices are resampled to remove the effect of tilt. + - _MoCo is appended to the ProtocolName if Image Type (0008,0008) includes the term 'MOCO'. This helps disambiguate Siemens fMRI runs where both motion corrected and raw data is stored for a single session. + +## Special Characters + +[Some characters are not permitted](https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names) in filenames. The following characters will be replaced with underscorces (`_`). Note that the forbidden characters vary between operating systems (Linux only forbids the forward slash, MacOS forbids forward slash and colon, while Windows forbids any of the characters listed below). To ensure that files can be easily copied between file systems, [dcm2niix restricts filenames to characters allowed by Windows](https://github.com/rordenlab/dcm2niix/issues/237). + +### List of Forbidden Characters (based on Windows) +``` +< (less than) +> (greater than) +: (colon - sometimes works, but is actually NTFS Alternate Data Streams) +" (double quote) +/ (forward slash) +\ (backslash) +| (vertical bar or pipe) +? (question mark) +* (asterisk) +``` \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/GE/README.md dcm2niix-1.0.20181125/GE/README.md --- dcm2niix-1.0.20180622/GE/README.md 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/GE/README.md 2018-11-25 21:24:07.000000000 +0000 @@ -1,124 +1,37 @@ ## About -dcm2niix attempts to convert GE DICOM format images to NIfTI. +dcm2niix attempts to convert GE DICOM format images to NIfTI. The current generation DICOM files generated be GE equipment is quite impoverished relative to other vendors. Therefore, the amount of information dcm2niix is able to extract is relatively limited. Hopefully, in the future GE will provide more details that are critical for brain scientists. ## Diffusion Tensor Notes -As noted by Jaemin Shin (GE), the GE convention for reported diffusion gradient direction has always been in “MR physics” logical coordinate, i.e Freq (X), Phase (Y), Slice (Z). Note that this is neither “with reference to the scanner bore” (like Siemens or Philips) nor “with reference to the imaging plane” (as expected by FSL tools). This is the main source of confusion. This explains why the dcm2niix function geCorrectBvecs() checks whether the DICOM tag In-plane Phase Encoding Direction (0018,1312) is 'ROW' or 'COL'. In addition, it will generate the warning 'reorienting for ROW phase-encoding untested' if you acquire DTI data with the phase encoding in the ROW direction. If you do test this feature, please report your findings as a Github issue. +The [NA-MIC Wiki](https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:DICOM_for_DWI_and_DTI#Private_vendor:_GE) provides a nice description of the GE diffusion tags. In brief, the B-value is stored as the first element in the array of 0043,1039. The DICOM elements 0019,10bb, 0019,10bc and 0019,10bd provide the gradient direction relative to the frequency, phase and slice. As noted by Jaemin Shin (GE), the GE convention for reported diffusion gradient direction has always been in “MR physics” logical coordinate, i.e Freq (X), Phase (Y), Slice (Z). Note that this is neither “with reference to the scanner bore” (like Siemens or Philips) nor “with reference to the imaging plane” (as expected by FSL tools). This is the main source of confusion. This explains why the dcm2niix function geCorrectBvecs() checks whether the DICOM tag In-plane Phase Encoding Direction (0018,1312) is 'ROW' or 'COL'. In addition, it will generate the warning 'reorienting for ROW phase-encoding untested' if you acquire DTI data with the phase encoding in the ROW direction. If you do test this feature, please report your findings as a Github issue. Assuming you have COL phase encoding, dcm2niix should provide [FSL format](http://justinblaber.org/brief-introduction-to-dwmri/) [bvec files](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FDT/FAQ#What_conventions_do_the_bvecs_use.3F). -## dcm2niix Notes +## Slice Timing -In addition to reading the +Knowing the relative timing of the acquisition for each 2D slice in a 3D volume is useful for [slice time correction](https://www.mccauslandcenter.sc.edu/crnl/tools/stc) of both fMRI and DTI data. Unfortunately, current GE software does not provide a consistent way to record this. +[Some sequences](https://afni.nimh.nih.gov/afni/community/board/read.php?1,154006) encode the RTIA Timer (0021,105E) element. For example, [this dataset DV24](https://github.com/nikadon/cc-dcm2bids-wrapper/tree/master/dicom-qa-examples/ge-mr750-slice-timing) includes timing data, while [this DV26 dataset does not](https://github.com/neurolabusc/dcm_qa_nih). Even with the sequences that do encode the RTIA Timer, there is some debate regarding the accuracy of this element. In the example listed, the slice times are clearly wrong in the first volume. Therefore, dcm2niix always estimates slice times based on the 2nd volume in a time series. -In addition to the public DICOM tags, dcm2niix attempts to decode the proprietary GE Protocol Data Block (0025,101B). This is essentially a [GZip format](http://www.onicos.com/staff/iz/formats/gzip.html) file embedded inside the DICOM header. Here are comments regarding the usage of this data block: +In general, fMRI acquired using GE product sequence (PSD) “epi” with the multiphase option will store slice timing in the Trigger Time (DICOM 0018,1060) element. The current version of dcm2niix ignores this field, as no examples are available. In contrast, the popular PSD “epiRT” (BrainWave RT, fMRI/DTI package provided by Medical Numerics) does not save this tag (though in some cases it saves the RTIA Timer). Examples are [available](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Slice_timing_correction) for both the “epiRT” and “epi” sequences. - - The VIEWORDER tag is used to set the polarity of the BIDS tag PhaseEncodingDirection, with VIEWORDER of 1 suggesting bottom up phase encoding. - - The SLICEORDER tag is used to set the SliceTiming for the BIDS tag PhaseEncodingDirection, with a SLICEORDER of 1 suggesting interleaved acquisition. Note that current versions of dcm2niix do not detect multiband for GE datasets. Therefore, the slice timing reported in the BIDS header will be incorrect for multiband acquisitions. - - There are reports that newer versions of GE equipement (e.g. DISCOVERY MR750 / 24\MX\MR Software release:DV24.0_R01_1344.a) are now storing an [XML](https://groups.google.com/forum/#!msg/comp.protocols.dicom/mxnCkv8A-i4/W_uc6SxLwHQJ) file within the Protocolo Data Block (compressed). Since the developers of dcm2niix have not had access to any of these files, dcm2niix should generate a warning when it encounters any of these images. - -``` -POSITION "Supine" -ENTRY "Head First" -CLINICALCOIL "C-GE_32Ch Head" -COIL "32Ch Head" -COILCOMPONENT "32 Ch Head Coil" -PLANE "AXIAL" -SEDESC "Axial rsfMRI (Eyes Open)" -HOS "0" -IMODE "2D" -PSEQ "Gradient Echo" -IOPT "MPh, EPI" -PLUG "9" -IEC_ACCEPT "ON" -FILTCHOICE "None" -BWRT "-1" -TRICKSIMG "1" -TAG_SPACE "7" -TAG_TYPE "None" -USERCV0 "1.00" -USERCV_MASK "1" -USERCV_MASK2 "0" -NUMBVALUE "1" -REOPT "1" -FLIPANG "90" -TE "30.0" -NECHO "1" -TR "3000.0" -NUMSHOTS "1" -BPMMODE "0" -AUTOTRGTYPE "0" -INITSTATE "0" -PSDTRIG "0" -SLICEORDER "1" -VIEWORDER "1" -TRREST "0" -TRACTIVE "0" -SLPERLOC "200" -ACQORDER "0" -DELACQ "Minimum" -DELACQNOAV "2" -SEPSERIES "0" -AUTOTRIGWIN "0" -FOV "22.0" -SLTHICK "3.4" -SPC "0.0" -GRXOPT "0" -SLOC1 "R0.5" -SLOC2 "A27.0" -SLOC3 "I63.3" -ELOC1 "R0.5" -ELOC2 "A27.0" -ELOC3 "S96.5" -FOVCNT1 "R0.5" -FOVCNT2 "A27.0" -NOSLC "48" -SL3PLANE "0" -SL3PLANE1 "0" -SL3PLANE2 "0" -SL3PLANE3 "0" -SPCPERPLANE1 "0.0" -SPCPERPLANE2 "0.0" -SPCPERPLANE3 "0.0" -MATRIXX "64" -MATRIXY "64" -SWAPPF "R/L" -NEX "1.00" -CONTRAST "No" -CONTAM "Yes " -TBLDELTA "0.00" -PHASEFOV "1.00" -AUTOSHIM "Off" -PHASECORR "Yes" -PAUSEDELMASKACQ "1" -GRIP_SLGROUP1 "0.500000 26.985256 16.641255 0.000000 0.000000 1.000000 0.000000 1.000000 0.000000 1.000000 0.000000 0.000000 48 0.000000 1 0.000000 0" -GRIP_NUMSLGROUPS "1" -GRIP_TRACKER "0" -GRIP_SPECTRO "0" -GRIP_NUMPSCVOL "0" -GRIP_PSCVOL1 "0" -GRIP_PSCVOL2 "0" -GRIP_PSCVOLFOV "0.000000" -GRIP_PSCVOLTHICK "0.000000" -GRIP_IRBAND_A "0" -GRIP_IRBAND_B "0" -AUTOSUBOPTIONS "0" -AUTOSCIC "0" -AUTOVOICE "0" -PRESETDELAY "0.0" -MASKPHASE "0" -MASKPAUSE "0" -GRXLOCSAVE "0" -AUTOCOIL "0" -ONETOUCHREG "0" -TEMPORALPHASES "4" -MEGFREQ "60" -DRIVERAMP "50" -MEGDIR "4" -DRIVERFREQ "60" -RFDRIVEMODE "Quadrature" -INRANGETR "0" -NAVPSCPAUSE "0" -EXCITATIONMODE "Selective" -ANATOMY "SRT%5CNone%5CT-A0100" -``` \ No newline at end of file +## User Define Data GE (0043,102A) + +This private element of the DICOM header is used to determine the phase encoding polarity. Specifically, we need to know the "Ky traversal direction" (top-down, or bottom up) and the phase encoding polarity. Unfortunately, this data is stored in a complicated, proprietary structure, that has changed with different releases of GE software. [Click here to see the definition for this structure](https://github.com/ScottHaileRobertson/GE-MRI-Tools/blob/master/GePackage/%2BGE/%2BPfile/%2BHeader/%2BRDB15/rdbm.h). + +## 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. Total readout time is influence by parallel acceleration factor, bandwidth, number of EPI lines, and partial Fourier. Not all of these parameters are available from the GE DICOM images, so a user needs to check the scanner console. + +## GE Protocol Data Block + +In addition to the public DICOM tags, previous versions of dcm2niix attempted to decode the proprietary GE Protocol Data Block (0025,101B). This is essentially a [GZip format](http://www.onicos.com/staff/iz/formats/gzip.html) file embedded inside the DICOM header. Unfortunately, this data seems to be [unreliable](https://github.com/rordenlab/dcm2niix/issues/163) and therefore this strategy is not used anymore. The notes below regarding the usage of this data block are provided for historical purposes. + + - The VIEWORDER tag is used to set the polarity of the BIDS tag PhaseEncodingDirection, with VIEWORDER of 1 suggesting bottom up phase encoding. Unfortunately, users can separately reverse the phase encoding direction making this tag unreliable. + - The SLICEORDER tag could be used to set the SliceTiming for the BIDS tag PhaseEncodingDirection, with a SLICEORDER of 1 suggesting interleaved acquisition. + - There are reports that newer versions of GE equipement (e.g. DISCOVERY MR750 / 24\MX\MR Software release:DV24.0_R01_1344.a) are now storing an [XML](https://groups.google.com/forum/#!msg/comp.protocols.dicom/mxnCkv8A-i4/W_uc6SxLwHQJ) file within the Protocolo Data Block (compressed). In theory this might also provide useful information. + +## Sample Datasets + + - [A validation dataset for dcm2niix commits](https://github.com/neurolabusc/dcm_qa_nih). + - [Examples of phase encoding polarity, slice timing and diffusion gradients](https://github.com/nikadon/cc-dcm2bids-wrapper/tree/master/dicom-qa-examples/). + - The dcm2niix (wiki)[https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage] includes examples of diffusion data, slice timing, and other variations. \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/.gitmodules dcm2niix-1.0.20181125/.gitmodules --- dcm2niix-1.0.20180622/.gitmodules 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/.gitmodules 2018-11-25 21:24:07.000000000 +0000 @@ -1,3 +1,9 @@ [submodule "dcm_qa"] path = dcm_qa - url = http://github.com/neurolabusc/dcm_qa + url = https://github.com/neurolabusc/dcm_qa.git +[submodule "dcm_qa_nih"] + path = dcm_qa_nih + url = https://github.com/neurolabusc/dcm_qa_nih.git +[submodule "dcm_qa_uih"] + path = dcm_qa_uih + url = https://github.com/neurolabusc/dcm_qa_uih.git diff -Nru dcm2niix-1.0.20180622/Philips/README.md dcm2niix-1.0.20181125/Philips/README.md --- dcm2niix-1.0.20180622/Philips/README.md 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/Philips/README.md 2018-11-25 21:24:07.000000000 +0000 @@ -44,3 +44,9 @@ ## General variations Prior versions of dcm2niix used different methods to sort images. However, these have proved unreliable The undocumented tags SliceNumberMrPhilips (2001,100A). In theory, InStackPositionNumber (0020,9057) should be present in all enhanced files, but has not proved reliable (perhaps not in older Philips images or DICOM images that were modified after leaving the scanner). MRImageGradientOrientationNumber (2005,1413) is complicated by the inclusion of derived images. Therefore, current versions of dcm2niix do not generally depend on any of these. + +## Sample Datasets + + - [National Alliance for Medical Image Computing (NAMIC) samples](http://www.insight-journal.org/midas/collection/view/194) + - [Unusual Philips Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Unusual_MRI). + - [Diffusion Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging). \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/README.md dcm2niix-1.0.20181125/README.md --- dcm2niix-1.0.20180622/README.md 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/README.md 2018-11-25 21:24:07.000000000 +0000 @@ -34,7 +34,18 @@ [See the BATCH.md file for instructions on using the batch processing version](./BATCH.md). -## Build +## Install + +There are a couple ways to install dcm2niix + - [Github Releases](https://github.com/rordenlab/dcm2niix/releases) provides the latest compiled executables. This is an excellent option for MacOS and Windows users. However, the provided Linux executable requires a recent version of Linux, so the provided Unix executable is not suitable for all distributions. + - [MRIcroGL](https://github.com/neurolabusc/MRIcroGL/releases) includes dcm2niix that can be run from the command line or from the graphical user interface (select the Import menu item). The Linux version of dcm2niix is compiled on a holy build box, so it should run on any Linux distribution. + - If you have a MacOS computer with Homebrew you can run `brew install dcm2niix`. + - If you have Conda, [`conda install -c conda-forge dcm2niix`](https://anaconda.org/conda-forge/dcm2niix) on Linux, MacOS or Windows. + - On Debian Linux computers you can run `sudo apt-get install dcm2niix`. + +## Build from source + +It is often easier to download and install a precompiled version. However, you can also build from source. ### Build command line version with cmake (Linux, MacOS, Windows) @@ -46,6 +57,8 @@ **Basic build:** ```bash +git clone https://github.com/rordenlab/dcm2niix.git +cd dcm2niix mkdir build && cd build cmake .. make @@ -59,9 +72,7 @@ As noted in the `Image Conversion and Compression Support` section, the software provides many optional modules with enhanced features. A common choice might be to include support for JPEG2000, [JPEG-LS](https://github.com/team-charls/charls) (this option requires a c++14 compiler), as well as using the high performance Cloudflare zlib library (this option requires a CPU built after 2008). To build with these options simply request them when configuring cmake: ```bash -mkdir build && cd build cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON .. -make ``` **optional batch processing version:** @@ -74,16 +85,15 @@ ## Alternatives - - [Valerio Luccio's dinifti](http://cbi.nyu.edu/software/dinifti.php) is focused on conversion of Siemens data. + - [dinifti](http://cbi.nyu.edu/software/dinifti.php) is focused on conversion of Siemens data. - [dcm2nii](http://www.mccauslandcenter.sc.edu/mricro/mricron/dcm2nii.htm) is the predecessor of dcm2niix. It is deprecated for modern images, but does handle image formats that predate DICOM (proprietary Elscint, GE and Siemens formats). - [DWIConvert](https://github.com/BRAINSia/BRAINSTools/tree/master/DWIConvert) converts DICOM images to NRRD and NIfTI formats. - - [Xiangrui Li's dicm2nii](http://www.mathworks.com/matlabcentral/fileexchange/42997-dicom-to-nifti-converter) is written in Matlab. The Matlab language makes this very scriptable. + - [dicm2nii](http://www.mathworks.com/matlabcentral/fileexchange/42997-dicom-to-nifti-converter) is written in Matlab. The Matlab language makes this very scriptable. - [dicom2nifti](https://github.com/icometrix/dicom2nifti) uses the scriptable Python wrapper utilizes the [high performance GDCMCONV](http://gdcm.sourceforge.net/wiki/index.php/Gdcmconv) executables. - [MRtrix mrconvert](http://mrtrix.readthedocs.io/en/latest/reference/commands/mrconvert.html) is a useful general purpose image converter and handles DTI data well. It is an outstanding tool for modern Philips enhanced images. - - [Jolinda Smith's mcverter](http://lcni.uoregon.edu/%7Ejolinda/MRIConvert/) has great support for various vendors. + - [mcverter](http://lcni.uoregon.edu/%7Ejolinda/MRIConvert/) has great support for various vendors. - [mri_convert](https://surfer.nmr.mgh.harvard.edu/pub/docs/html/mri_convert.help.xml.html) is part of the popular FreeSurfer package. In my limited experience this tool works well for GE and Siemens data, but fails with Philips 4D datasets. - [SPM12](http://www.fil.ion.ucl.ac.uk/spm/software/spm12/) is one of the most popular tools in the field. It includes DICOM to NIfTI conversion. Being based on Matlab it is easy to script. - - [R2A_GUI](http://r2agui.sourceforge.net/) is a Matlab script that converts Philips PAR/REC images to NIfTI. ## Links diff -Nru dcm2niix-1.0.20180622/RENAMING.md dcm2niix-1.0.20181125/RENAMING.md --- dcm2niix-1.0.20180622/RENAMING.md 1970-01-01 00:00:00.000000000 +0000 +++ dcm2niix-1.0.20181125/RENAMING.md 2018-11-25 21:24:07.000000000 +0000 @@ -0,0 +1,29 @@ +## About + +dcm2niix is primarily designed to convert DICOM images into NIfTI images. However, it does include a primitive ability to organize and rename DICOM images without converting them. This can be useful, as DICOM images are often stored with random file names that make it difficult to recognize the images. + +## Limitation + +dcm2niix renames and copies the DICOM images, but the current version does not copy or create a new [DICOMDIR](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_F.2.2.2.html) file. Most users ignore these files. However, you should not use this featire if you wish to preserve your DICOMDIR files. + +Note that this feature only copies your DICOM images with a new filename. It does not modify the contents of the DICOM header. This means it will not compress or anonymize your files. Free tools for these functions include [dcmcjpeg](https://dicom.offis.de/dcmtk.php.en), [gdcmanon](http://gdcm.sourceforge.net/html/gdcmanon.html) and [gdcmconv](http://gdcm.sourceforge.net/html/gdcmconv.html). + +In addition, this tool assumes that the DICOM images can be uniquely identified by the filenaming argument you provide. + +## Usage + +The command line argument `-r y` instructs dcm2niix to rename your DICOM files rather than convert them. It does not delete your DICOM images, but rather creates a copy with the organization specified by the [filenaming argument `-f`](FILENAMING.md). Here is an example where the DICOM images will be sorted into folders, with the folder name reflecting the study time (`%t`) and series number (`%s`), each DICOM image will be named based on the image number (`%r`) which will be padded with zeros to fill 5 characters. + - `dcm2niix -r y -f %t_%s/%5r.dcm -o ~/out ~/in` +Therefore, the 9th DICOM image from series 3 acquired on 4 February 2012 would be saved as ~/out/20120204084424_3/00009.dcm. + +It is very important that your file naming disambiguates all your images. For example, consider a naming scheme that only used the image number (`-f %r.dcm`) and was applied to multiple series (each which had an image number 1,2,...). When there are naming conflicts, dcm2niix will terminate with an error message, e.g. `Error: File naming conflict. Existing file /home/c/dcm/1.dcm`. + +A special situation is the fieldmaps generated by Siemens scanners. Users often acquire gradient-echo fieldmaps so they can undistort EPI images. These fieldmaps acquire two (or more) echoes. Unfortunately, Siemens will give each of these echoes an identical series and image number. DICOM tools that are unaware of this often [overwrite](https://neurostars.org/t/field-mapping-siemens-scanners-dcm2niix-output-2-bids/2075/7) some of the images from each echo. To combat this situation, dcm2niix will add the post-fix `_e2` to the second echo. Therefore, if you converted a series with `-f %s_%4r` your fieldmap might generate files named `5_0001.dcm` and `5_0001_e2.dcm`. Note you could also explicitly number each echo (`-f %s_%4r_%e`), though in this case all your series (not just the fieldmaps) will have the echo appended. +## Alternatives + +An advantage of using dcm2niix is simplicity: it is a free, single file executable that you can [download](https://github.com/rordenlab/dcm2niix/releases) that is available for MacOS, Linux and Windows that you can run from the command line. On the other hand, this simplicity means it is fairly inflexible. You may want to consider a DICOM renamer built in your favorite scripting language. + + - [dicom-rename is a Python script](https://github.com/joshy/dicom-rename). + - [dicomsort is a Python script](https://github.com/pieper/dicomsort). + - [rename_dir is a Matlab script that requires the proprietary Image Processing Toolbox](https://gist.github.com/htygithub/ad3597577e1de004e9f5). + - [dicm2nii includes the Matlab script rename_dicm that does not require any additional toolboxes](https://github.com/xiangruili/dicm2nii). \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/Siemens/README.md dcm2niix-1.0.20181125/Siemens/README.md --- dcm2niix-1.0.20180622/Siemens/README.md 1970-01-01 00:00:00.000000000 +0000 +++ dcm2niix-1.0.20181125/Siemens/README.md 2018-11-25 21:24:07.000000000 +0000 @@ -0,0 +1,33 @@ +## About + +dcm2niix attempts to convert Siemens DICOM format images to NIfTI. This page describes some vendor-specific details. + +## Vida XA10A + +The Siemens Vida introduced the new XA10A DICOM format. Users are strongly encouraged to export data using the "Enhanced" format and to not use any of the "Anonymize" features on the console. The consequences of these options is discussed in detail in [issue 236](https://github.com/rordenlab/dcm2niix/issues/236). In brief, the Vida can export to enhanced, mosaic or classic 2D. Note that the mosaics are considered secondary capture images intended for quality assurance only. The mosaic scans lack several "Type 1" DICOM properties, necessarily limiting conversion. The non-mosaic 2D enhanced DICOMs are compact and efficient, but appear to have limited details relative to the enhanced output. Finally, each of the formats (enhanced, mosaic, classic) can be exported as anonymized. The Siemens console anonymization of current XA10A (Fall 2018) strips many useful tags. Siemens suggests "the use an offline/in-house anonymization software instead." Another limitation of the current XA10A format is that it retains no versioning details for software and hardware stepping, despite the fact that the data format is rapidly evolving. If you use a Vida, you are strongly encouraged to log every hardware or software upgrade to allow future analyses to identify and regress out any effects of these modifications. Since the XA10A format does not have a CSA header, dcm2niix will attempt to use the new private DICOM tags to populate the BIDS file. These tags are described in [issue 240](https://github.com/rordenlab/dcm2niix/issues/240). + +## CSA Header + +Many crucial Siemens parameters are stored in the [proprietary CSA header](http://nipy.org/nibabel/dicom/siemens_csa.html). This has a binary section that allows quick reading for many useful parameters. It also includes an ASCII text portion that includes a lot of information but is slow to parse and poorly curated. + +## Slice Timing + +The CSA header provides [slice timing](https://www.mccauslandcenter.sc.edu/crnl/tools/stc), and therefore dcm2niix should provide accurate slice timing information for non-XA10 datasets. For archival studies, be aware that some sequences [incorrectly reported slice timing](https://github.com/rordenlab/dcm2niix/issues/126). + +## 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. The [Siemens validation dataset](https://github.com/neurolabusc/dcm_qa/tree/master/In/TotalReadoutTime) demonstrates that dcm2niix can accurately report these parameters - the included notes and spreadsheet describe this in more detail. + +## Diffusion Tensor Notes + +Diffusion specific parameters are described on the [NA-MIC](https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:DICOM_for_DWI_and_DTI#Private_vendor:_Siemens) website. Gradient vectors are reported with respect to the scanner bore, and dcm2niix will attempt to re-orient these to [FSL format](http://justinblaber.org/brief-introduction-to-dwmri/) [bvec files](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FDT/FAQ#What_conventions_do_the_bvecs_use.3F). + +## Sample Datasets + + - [Slice timing dataset](httphttps://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Slice_timing_corrections://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage). + - [A validation dataset for dcm2niix commits](https://github.com/neurolabusc/dcm_qa). + - [A mixture of GE and Siemens data](https://github.com/neurolabusc/dcm_qa_nih). + - [DTI examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging). + - [Archival (old) examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Archival_MRI). + - [Unusual examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Unusual_MRI). + diff -Nru dcm2niix-1.0.20180622/SuperBuild/SuperBuild.cmake dcm2niix-1.0.20181125/SuperBuild/SuperBuild.cmake --- dcm2niix-1.0.20180622/SuperBuild/SuperBuild.cmake 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/SuperBuild/SuperBuild.cmake 2018-11-25 21:24:07.000000000 +0000 @@ -60,21 +60,24 @@ if(OpenJPEG_DIR) set(OpenJPEG_DIR "${OpenJPEG_DIR}" CACHE PATH "Path to OpenJPEG configuration file" FORCE) - message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") + message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") else() find_package(PkgConfig) if(PKG_CONFIG_FOUND) pkg_check_modules(OPENJPEG libopenjp2) endif() - if(OPENJPEG_FOUND) - set(OpenJPEG_DIR ${OPENJPEG_LIBDIR}/openjepg-2.1 CACHE PATH "Path to OpenJPEG configuration file" FORCE) - message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") + if(OPENJPEG_FOUND AND NOT ${OPENJPEG_INCLUDE_DIRS} MATCHES "gdcmopenjpeg") + set(OpenJPEG_DIR ${OPENJPEG_LIBDIR}/openjpeg-2.1 CACHE PATH "Path to OpenJPEG configuration file" FORCE) + message("-- Using OpenJPEG library from ${OpenJPEG_DIR}") else() + if(${OPENJPEG_INCLUDE_DIRS} MATCHES "gdcmopenjpeg") + message("-- Unable to use GDCM's internal OpenJPEG") + endif() include(${CMAKE_SOURCE_DIR}/SuperBuild/External-OPENJPEG.cmake) list(APPEND DEPENDENCIES openjpeg) set(BUILD_OPENJPEG TRUE) - message("-- Will build OpenJPEG library from github") + message("-- Will build OpenJPEG library from github") endif() endif() endif() @@ -84,7 +87,7 @@ if(YAML-CPP_DIR) set(YAML-CPP_DIR ${YAML-CPP_DIR} CACHE PATH "Path to yaml-cpp configuration file" FORCE) - message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") + message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") else() find_package(PkgConfig) if(PKG_CONFIG_FOUND) @@ -94,12 +97,12 @@ # Build from github if not found or version < 0.5.3 if(YAML-CPP_FOUND AND NOT (YAML-CPP_VERSION VERSION_LESS "0.5.3")) set(YAML-CPP_DIR ${YAML-CPP_LIBDIR}/cmake/yaml-cpp CACHE PATH "Path to yaml-cpp configuration file" FORCE) - message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") + message("-- Using yaml-cpp library from ${YAML-CPP_DIR}") else() include(${CMAKE_SOURCE_DIR}/SuperBuild/External-YAML-CPP.cmake) list(APPEND DEPENDENCIES yaml-cpp) set(BUILD_YAML-CPP TRUE) - message("-- Will build yaml-cpp library from github") + message("-- Will build yaml-cpp library from github") endif() endif() endif() @@ -111,7 +114,7 @@ include(${CMAKE_SOURCE_DIR}/SuperBuild/External-CLOUDFLARE-ZLIB.cmake) list(APPEND DEPENDENCIES zlib) set(BUILD_CLOUDFLARE-ZLIB TRUE) - message("-- Will build Cloudflare zlib from github") + message("-- Will build Cloudflare zlib from github") elseif(${ZLIB_IMPLEMENTATION} STREQUAL "Custom") set(ZLIB_ROOT ${ZLIB_ROOT} CACHE PATH "Specify custom zlib root directory.") if(NOT ZLIB_ROOT) diff -Nru dcm2niix-1.0.20180622/.travis.yml dcm2niix-1.0.20181125/.travis.yml --- dcm2niix-1.0.20180622/.travis.yml 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/.travis.yml 2018-11-25 21:24:07.000000000 +0000 @@ -23,11 +23,15 @@ before_install: - eval "${MATRIX_EVAL}" - git submodule update --init --depth=3 dcm_qa + - git submodule update --init --depth=3 dcm_qa_nih + - git submodule update --init --depth=3 dcm_qa_uih script: - mkdir build && cd build && cmake -DBATCH_VERSION=ON -DUSE_OPENJPEG=ON -DUSE_JPEGLS=true -DZLIB_IMPLEMENTATION=Cloudflare .. && make && cd - - export PATH=$PWD/build/bin:$PATH - cd dcm_qa && ./batch.sh && cd - + - cd dcm_qa_nih && ./batch.sh && cd - + - cd dcm_qa_uih && ./batch.sh && cd - before_deploy: - export DATE=`date +%-d-%b-%Y` diff -Nru dcm2niix-1.0.20180622/UIH/README.md dcm2niix-1.0.20181125/UIH/README.md --- dcm2niix-1.0.20180622/UIH/README.md 1970-01-01 00:00:00.000000000 +0000 +++ dcm2niix-1.0.20181125/UIH/README.md 2018-11-25 21:24:07.000000000 +0000 @@ -0,0 +1,64 @@ +## About + +dcm2niix attempts to convert UIH DICOM format images to NIfTI. + +## Notes + +Shan C Young provided the [following information](https://github.com/rordenlab/dcm2niix/issues/225), which is used by dcm2niix to generate NIfTI and BIDS format files. + +UIH supports two ways of archiving the DWI/DTI and fMRI data. One way is one DICOM file per slice and the other is one dicom file per volume (UIH refers to this as GRID format, similar to the Siemens Mosaic format). The private tags used in the images are shown in the following table. + + +Tag ID | Tag Name | VR | VM | Description | Sample +-- | -- | -- | -- | -- | -- +0061,1002 | Generate Private | US | 1 | Flag to generate private format file | 1 +**0061,4002** | **FOV** | SH | 1 | FOV(mm) | 224*224 +0065,1000 | MeasurmentUID | UL | 1 | Measurement UID of Protocol | 12547865 +0065,1002 | ImageOrientationDisplayed | SH | 1 | Image Orientation Displayed | Sag or Sag>Cor +0065,1003 | ReceiveCoil | LO | 1 | Receive Coil Information | H 8 +0065,1004 | Interpolation | SH | 1 | Interpolation | I +0065,1005 | PE Direction Displayed | SH | 1 | Phase encoding diretion displayed | A->P or H->F +0065,1006 | Slice Group ID | IS | 1 | Slice Group ID | 1 +0065,1007 | Uprotocol | OB | 1 | Uprotocol value |   +0065,1009 | BActualValue | FD | 1 | Actual B-Value from sequence | 1000.0 +**0065,100A** | **BUserValue** | FD | 1 | User Choose B-Value from UI | 1000.0 +**0065,100B** | **Block Size** | DS | 1 | Size of the paradigm/block | 10 +**0065,100C** | **Experimental status** | SH | 1 | fMRI | rest/active +0065,100D | Parallel Information | SH | 1 | ratio of parallel acquisition and acceleration |   +0065,100F | Slice Position | SH | 1 | Slice location displayed on the screen | H23.4 +0065,1011 | Sections | SH | 1 |   |   +0065,1013 | InPlaneRotAngle | FD(°) | 1 | Rotation angle in the plane | -0.5936 +0065,1014 | SliceNormalVector | DS | 3 | Normal vector of the slice | 0\0\1 +0065,1015 | SliceCenterPosition | DS | 3 | Center position of the slice | 0\0\0 +0065,1016 | PixelRotateModel | UL | 1 | Pixel Rotation Model | 4 +0065,1017 | SAR Model | LO | 1 | Calculation model of SAR value | Normal:WHBST +0065,1018 | dB/dt Model | LO | 1 | Calculation model of dB/dt | Normal +0065,1023 | TablePosition | LO | 1 | Table Position | 0 +0065,1025 | Slice Gap | DS | 1 | Slice Gap | 0.0 +0065,1029 | AcquisitionDuration | SH | 1 | Acquisition Duration | 0.03 +0065,102B | ApplicationCategory | LT | 1 | Application names available | DTI\Func +0065,102C | RepeatitionIndex | IS | 1 |   | 0 +**0065,102D** | **SequenceDisplayName** | ST | 1 | Sequence display name | Epi_dti_b0 +0065,102E | NoiseDecovarFlag | LO | 1 | Noise decorrelation flag | PreWhite +0065,102F | ScaleFactor | FL | 1 | scale factor | 2.125 +0065,1031 | MRSequenceVariant | SH | 1 | SequenceVariant |   +0065,1032 | MRKSpaceFilter | SH | 1 | K space filter |   +0065,1033 | MRTableMode | SH | 1 | Table mode | Fix +0065,1036 | MRDiscoParameter | OB | 1 |   |   +**0065,1037** | **MRDiffusionGradOrientation** | FD | 3 | Diffusion gradient orientation | 0\0\0 +0065,1038 | MRPerfusionNoiseLevel | FD | 1 | epi_dwi/perfusion noise level | 40 +0065,1039 | MRGradRange | SH | 6 | linear range of gradient | 0.0\157\0.0\157\0.0\125 +**0065,1050** | **MR Number Of Slice In Volume** | DS | 1 | Number Of Frames In a Volume,Columns of each frame: cols =ceil(sqrt(total)) ; Rows of each frame: rows =ceil(total/cols) ; appeared when image type (00080008) has VFRAME | 27 +0065,1051 | MR VFrame Sequence | SQ | 1 | 1 |   + ->0008,0022 | Acquisition Date | DA | 1 |   |   + ->0008,0032 | Acquisition Time | TM | 1 |   |   + ->0008,002A | Acquisition DateTime | DT | 1 |   |   + ->0020,0032 | ImagePosition(Patient) | DS | 3 |   |   + ->00201041 | Slice Location | DS | 1 |   |   + ->0018,9073 | Acquisition Duration | FD | 1 |   |   + ->0065,100C | MRExperimental Status | SH | 1 |   | rest/active + +## Sample Datasets + + - [UIH has provided a reference dataset](https://1drv.ms/f/s!Avf7THyflzj1gnO37GL2I8Hk-0MV). + - [A validation dataset for dcm2niix commits](https://github.com/neurolabusc/dcm_qa_uih). \ No newline at end of file diff -Nru dcm2niix-1.0.20180622/VERSIONS.md dcm2niix-1.0.20181125/VERSIONS.md --- dcm2niix-1.0.20180622/VERSIONS.md 2018-06-27 18:09:11.000000000 +0000 +++ dcm2niix-1.0.20181125/VERSIONS.md 2018-11-25 21:24:07.000000000 +0000 @@ -1,7 +1,14 @@ ## Versions +14-November-2018 + - [GE images provide more BIDS tags](https://github.com/rordenlab/dcm2niix/issues/163). + - [Bruker enhanced DICOM support](https://github.com/rordenlab/dcm2niix/issues/241). + - [Siemens Vida XA10 support](https://github.com/rordenlab/dcm2niix/issues/240). Note that Vida DICOM data is crippled [if the user exports as mosaics or anonymized/reduced](https://github.com/rordenlab/dcm2niix/issues/236). + - [UIH enhanced DICOM support](https://github.com/rordenlab/dcm2niix/issues/225). + - New DICOM [renaming](RENAMING.md) feature. + 22-June-2018 - - [fix issues where 6-June-2018 release could save Enhanced DICOM Philips bvec/bval with different order than .nii images](https://github.com/rordenlab/dcm2niix/issues/201). + - [Fix issues where 6-June-2018 release could save Enhanced DICOM Philips bvec/bval with different order than .nii images](https://github.com/rordenlab/dcm2niix/issues/201). 6-June-2018 - [Improved Philips PAR/REC support](https://github.com/rordenlab/dcm2niix/issues/171)