From 015be34925ff8d184ad0681c26013750a98caf96 Mon Sep 17 00:00:00 2001 From: Chris Rorden Date: Sun, 25 Nov 2018 13:37:48 -0500 Subject: [PATCH 1/2] gdcmanon removes protocol name (0018,1030), for Siemens we can rescue from CSA header (0029,1020) --- console/main_console.cpp | 2 +- console/nii_dicom.cpp | 2 +- console/nii_dicom.h | 5 +++-- console/nii_dicom_batch.cpp | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/console/main_console.cpp b/console/main_console.cpp index 4c75bb96..8a56179d 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -85,7 +85,7 @@ void showHelp(const char * argv[], struct TDCMopts opts) { #else #define kQstr "" #endif - 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(" -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"); diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index da06539a..748a51db 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4938,7 +4938,7 @@ double TE = 0.0; //most recent echo time recorded if (d.manufacturer != kMANUFACTURER_UIH) break; char accelStr[kDICOMStr]; dcmStr (lLength, &buffer[lPos], accelStr); - char *ptr; + //char *ptr; dcmStrDigitsDotOnlyKey(':', accelStr); //e.g. if "p2s4" return "2", if "s4" return "" d.accelFactPE = atof(accelStr); break; } diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 71b4087b..65701cd6 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -43,7 +43,7 @@ extern "C" { #define kCCsuf " CompilerNA" //unknown compiler! #endif -#define kDCMvers "v1.0.20181114 " 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 @@ -155,7 +155,8 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; struct TDICOMdata { long seriesNum; int xyzDim[5]; - int numberOfImagesInGridUIH, numberOfDiffusionDirectionGE, phaseEncodingGE, protocolBlockStartGE, protocolBlockLengthGE, modality, dwellTime, effectiveEchoSpacingGE, phaseEncodingLines, phaseEncodingSteps, echoTrainLength, coilCrc, echoNum, sliceOrient, manufacturer, converted2NII, acquNum, imageNum, imageStart, imageBytes, bitsStored, bitsAllocated, samplesPerPixel,locationsInAcquisition, compressionScheme; + 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 rtia_timerGE, radionuclidePositronFraction, radionuclideTotalDose, radionuclideHalfLife, doseCalibrationFactor; //PET ISOTOPE MODULE ATTRIBUTES (C.8-57) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index badd3745..177ae770 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -734,6 +734,20 @@ void json_Float(FILE *fp, const char *sLabel, float sVal) { 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); + //printWarning(">>>>%s\n",filename); + //printWarning(">>>>%s\n", 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 @@ -3283,6 +3297,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc 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); From 054e4a06c963d05d626f4a9c1bb4db8466af7540 Mon Sep 17 00:00:00 2001 From: Chris Rorden Date: Sun, 25 Nov 2018 15:57:54 -0500 Subject: [PATCH 2/2] DICOM renaming of Siemens fieldmaps where different echoes use same instance number --- RENAMING.md | 6 ++++-- console/nii_dicom.cpp | 4 ++-- console/nii_dicom_batch.cpp | 34 +++++++++++++++++++++------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/RENAMING.md b/RENAMING.md index 58dfbfaa..c1af8f78 100644 --- a/RENAMING.md +++ b/RENAMING.md @@ -6,7 +6,7 @@ dcm2niix is primarily designed to convert DICOM images into NIfTI images. Howeve 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). +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. @@ -14,9 +14,11 @@ In addition, this tool assumes that the DICOM images can be uniquely identified 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. diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 748a51db..8cb945f6 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -1472,10 +1472,10 @@ mat33 nifti_mat33_reorder_cols( mat33 m, ivec3 v ) { 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) { diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 177ae770..1c436996 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -744,8 +744,6 @@ void rescueProtocolName(struct TDICOMdata *d, const char * filename) { 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); - //printWarning(">>>>%s\n",filename); - //printWarning(">>>>%s\n", protocolName); } void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename) { @@ -1790,7 +1788,7 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt isEchoReported = true; } if ((dcm.isNonParallelSlices) && (!isImageNumReported)) { - sprintf(newstr, "_i%05d", dcm.imageNum); + 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 @@ -3043,7 +3041,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc printMessage("Slice positions repeated, but number of slices (%d) not divisible by number of repeats (%d): missing images?\n", nConvert, nAcq); } } - //next options removed: featuresnow thoroughly detected in nii_loadDir() + //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; @@ -3915,22 +3913,22 @@ int copyFile (char * src_path, char * dst_path) { unsigned char buffer[BUFFSIZE]; FILE *fin = fopen(src_path, "rb"); if (fin == NULL) { - printf("Unable to open input %s\n", src_path); + printError("Check file permissions: Unable to open input %s\n", src_path); return EXIT_FAILURE; } if (is_fileexists(dst_path)) { - printf("Skipping existing file %s\n", dst_path); + printError("File naming conflict. Existing file %s\n", dst_path); return EXIT_FAILURE; } FILE *fou = fopen(dst_path, "wb"); if (fou == NULL) { - printf("Unable to open output %s\n", dst_path); + 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) { - printf("Unable to write %zu bytes to output %s\n", bytes, dst_path); + printError("Unable to write %zu bytes to output %s\n", bytes, dst_path); return EXIT_FAILURE; } } @@ -4021,6 +4019,8 @@ int nii_loadDir(struct TDCMopts* opts) { 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 @@ -4036,8 +4036,14 @@ int nii_loadDir(struct TDCMopts* opts) { //~ 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].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); - copyFile (nameList.str[i], outname); + 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; @@ -4106,10 +4112,10 @@ int nii_loadDir(struct TDCMopts* opts) { 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; - isNonParallelSlices = false; - isCoilVaries = false; - for (int j = i; j < (int)nDcm; j++) + 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]); @@ -4127,7 +4133,9 @@ int nii_loadDir(struct TDCMopts* opts) { 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)