diff --git a/GE/README.md b/GE/README.md index 4e22a097..85f8e7d4 100644 --- a/GE/README.md +++ b/GE/README.md @@ -10,9 +10,11 @@ The [NA-MIC Wiki](https://www.na-mic.org/wiki/NAMIC_Wiki:DTI:DICOM_for_DWI_and_D 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 DV24 dataset](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). Be aware that different versions of GE software appear to use different units for 0021,105E. The DV24 example is reported in seconds, while [14.0 uses 1/10000 seconds](https://github.com/rordenlab/dcm2niix/issues/286).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. +[Some sequences](https://afni.nimh.nih.gov/afni/community/board/read.php?1,154006) encode the RTIA Timer (0021,105E) element. For example, [this DV24 dataset](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). Be aware that different versions of GE software appear to use different units for 0021,105E. The DV24 example is reported in seconds, while [14.0 uses 1/10000 seconds](https://github.com/rordenlab/dcm2niix/issues/286). An example of the latter format can be found [here](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Archival_MRI). 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 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. +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. 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. + +If neither Trigger Time (DICOM 0018,1060) or RTIA Timer (0021,105E) store slice timing information, a final option is to decode the GE Protocol Data Block as described below. At best, this block only reports whether the acquisition was interleaved or sequential. As long as one assumes the acquisition was continuous (with no temporal gap between volumes, e.g. sparse images) on can use this value, the number of slices in the volume and the repetition time to infer slice times. ## User Define Data GE (0043,102A) diff --git a/Philips/README.md b/Philips/README.md index 8ef2bd38..0616acbf 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -18,7 +18,7 @@ Therefore, dcm2niix will ignore the IPP enclosed in 2005,140F unless no alternat ## Derived parametric maps stored with raw diffusion data -Some Philips diffusion DICOM images include derived image(s) along with the images. Other manufacturers save these derived images as a separate series number, and the DICOM standard seems ambiguous on whether it is allowable to mix raw and derived data in the same series (see PS 3.3-2008, C.7.6.1.1.2-3). In practice, many Philips diffusion images append [derived parametric maps](http://www.revisemri.com/blog/2008/diffusion-tensor-imaging/) with the original data. For example, ADC, Trace and Isotropic images can all be derived from the raw scans. As scientists, we want to discard these derived images, as they will disrupt data processing and we can generate better parametric maps after we have applied undistortion methods such as [Eddy and Topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide). The current version of dcm2niix uses the Diffusion Directionality (0018,9075) tag to detect B=0 unweighted ("NONE"), B-weighted ("DIRECTIONAL"), and derived ("ISOTROPIC") images. Note that the Dimension Index Values (0020,9157) tag provides an alternative approach to discriminate these images. Here are sample tags from a Philips enhanced image that includes and derived map (3rd dimension is "1" while the other images set this to "2"). +Some Philips diffusion DICOM images include derived image(s) along with the images. Other manufacturers save these derived images as a separate series number, and the DICOM standard seems ambiguous on whether it is allowable to mix raw and derived data in the same series (see PS 3.3-2008, C.7.6.1.1.2-3). In practice, many Philips diffusion images append [derived parametric maps](http://www.revisemri.com/blog/2008/diffusion-tensor-imaging/) with the original data. With Philips, appending the derived isotropic image is optional - it is only created for the 'clinical' DTI schemes for radiography analysis and is triggered if the first three vectors in the gradient table are the unit X,Y and Z vectors. For conventional DWI, the result is the conventional mean of the ADC X,Y,Z for DTI it the conventional mean of the 3 principle Eigen vectors. As scientists, we want to discard these derived images, as they will disrupt data processing and we can generate better parametric maps after we have applied undistortion methods such as [Eddy and Topup](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy/UsersGuide). The current version of dcm2niix uses the Diffusion Directionality (0018,9075) tag to detect B=0 unweighted ("NONE"), B-weighted ("DIRECTIONAL"), and derived ("ISOTROPIC") images. Note that the Dimension Index Values (0020,9157) tag provides an alternative approach to discriminate these images. Here are sample tags from a Philips enhanced image that includes and derived map (3rd dimension is "1" while the other images set this to "2"). ``` (0018,9075) CS [DIRECTIONAL] @@ -59,7 +59,7 @@ For modern Philips DICOMs, the current version of dcm2niix uses Dimension Index Philips DICOMs do not contain all the information desired by many neuroscientists. Due to this, the [BIDS](http://bids.neuroimaging.io/) files created by dcm2niix are impoverished relative to data from other vendors. This reflects a limitation in the Philips DICOMs, not dcm2niix. -[Slice timing correction](https://www.mccauslandcenter.sc.edu/crnl/tools/stc) can account for some variability in fMRI datasets. Unfortunately, Philips DICOM data [does not encode slice timing information](https://neurostars.org/t/heudiconv-no-extraction-of-slice-timing-data-based-on-philips-dicoms/2201/4). Therefore, dcm2niix is unable to populate the "SliceTiming" BIDS field. +[Slice timing correction](https://www.mccauslandcenter.sc.edu/crnl/tools/stc) can account for some variability in fMRI datasets. Unfortunately, Philips DICOM data [does not encode slice timing information](https://neurostars.org/t/heudiconv-no-extraction-of-slice-timing-data-based-on-philips-dicoms/2201/4). Therefore, dcm2niix is unable to populate the "SliceTiming" BIDS field. However, one can typically infer slice timing by recording the [mode and number of packages](https://en.wikibooks.org/w/index.php?title=SPM/Slice_Timing&stable=0#Philips_scanners) reported for the sequence on the scanner console. Likewise, the BIDS tag "PhaseEncodingDirection" allows tools like [eddy](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/eddy) and [TOPUP](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/topup) to undistort images. While the Philips DICOM header distinguishes the phase encoding axis (e.g. anterior-posterior vs left-right) it does not encode the polarity (A->P vs P->A). diff --git a/README.md b/README.md index 314e814c..b24c9bb0 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,11 @@ In rare case if cmake fails with the message like `"Generator: execution of make 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 +git clone https://github.com/rordenlab/dcm2niix.git +cd dcm2niix +mkdir build && cd build cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON .. +make ``` **optional batch processing version:** @@ -100,6 +104,7 @@ If you have any problems with the cmake build script described above or want to The following tools exploit dcm2niix + - [dcm2niix can help convert data from the Adolescent Brain Cognitive Development (ABCD) DICOM to BIDS](https://github.com/ABCD-STUDY/abcd-dicom2bids) - [bidsify](https://github.com/spinoza-rec/bidsify) is a Python project that uses dcm2niix to convert DICOM and Philips PAR/REC images to the BIDS standard. - [bidskit](https://github.com/jmtyszka/bidskit) uses dcm2niix to create [BIDS](http://bids.neuroimaging.io/) datasets. - [BioImage Suite Web Project](https://github.com/bioimagesuiteweb/bisweb) is a JavaScript project that uses dcm2niix for its DICOM conversion module. @@ -122,8 +127,9 @@ The following tools exploit dcm2niix - [neurodocker](https://github.com/kaczmarj/neurodocker) generates [custom](https://github.com/rordenlab/dcm2niix/issues/138) Dockerfiles given specific versions of neuroimaging software. - [NeuroElf](http://neuroelf.net) can use dcm2niix to convert DICOM images. - [nipype](https://github.com/nipy/nipype) can use dcm2niix to convert images. - - [PNL-nipype](https://github.com/pnlbwh/Dummy-PNL-nipype) is a Python script that can convert dcm2niix created NIfTI files to the popular NRRD format (including DWI gradient tables). + - [PNL-nipype](https://github.com/pnlbwh/Dummy-PNL-nipype) is a Python script that can convert dcm2niix created NIfTI files to the popular NRRD format (including DWI gradient tables). Note, recent versions of dcm2niix can directly convert DICOM images to NRRD. - [pydcm2niix is a Python module for working with dcm2niix](https://github.com/jstutters/pydcm2niix). - [pyBIDSconv provides a graphical format for converting DICOM images to the BIDS format](https://github.com/DrMichaelLindner/pyBIDSconv). It includes clever default heuristics for identifying Siemens scans. - [sci-tran dcm2niix](https://github.com/scitran-apps/dcm2niix) Flywheel Gear (docker). - - The [SlicerDcm2nii extension](https://github.com/Slicer/ExtensionsIndex/blob/master/SlicerDcm2nii.s4ext) is one method to import DICOM data into Slicer. \ No newline at end of file + - The [SlicerDcm2nii extension](https://github.com/Slicer/ExtensionsIndex/blob/master/SlicerDcm2nii.s4ext) is one method to import DICOM data into Slicer. + - [TractoR (Tracto­graphy with R) uses dcm2niix for image conversion](http://www.tractor-mri.org.uk/TractoR-and-DICOM). diff --git a/Siemens/README.md b/Siemens/README.md index 59a2bd8f..b857f505 100644 --- a/Siemens/README.md +++ b/Siemens/README.md @@ -22,13 +22,15 @@ Private Tags ``` +In theory, the public DICOM tag 'Frame Acquisition Date Time' (0018,9074) and the private tag 'Time After Start' (0021,1104) should each allow one to infer slice timing. The tag 0018,9074 uses the DT (date time) format, for example "20190621095520.330000" providing the YYYYYMMDDHHMMSS. Unfortunately, the Siemens de-identification routines will scramble these values, as time of data could be considered an identifiable attribute. The tag 0021,1104 is saved in DS (decimal string) format, for example "4.635" reporting the number of seconds since acquisition started. Be aware that some [Siemens Vida multi-band sequences](https://github.com/rordenlab/dcm2niix/issues/303) appear to fill these tags with the single-band times rather than the actual acquisition times. Therefore, neither of these two methods is perfectly reliable in determining slice timing. + ## 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. +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. Be aware that Siemens Vida scanners do not generate a CSA header. ## 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). +For Siemens images created with software versions B15 through E11, the proprietary [CSA Image Header (0029,1010)](https://nipy.org/nibabel/dicom/siemens_csa.html) contains the array MosaicRefAcqTimes that provides [slice timing](https://www.mccauslandcenter.sc.edu/crnl/tools/stc). Earlier Siemens Software (e.g. A25 through B13) sometimes populates the tag sSliceArray.ucMode in the [CSA Series Header (0029, 1020)](https://nipy.org/nibabel/dicom/siemens_csa.html) where the values [1, 2, and 4](https://github.com/xiangruili/dicm2nii/issues/18) correspond to Ascending, Descending and Interleaved acquisitions. Therefore dcm2niix typically is able to provide accurate slice timing information for non-Vida datasets (the prior section describes Vida slice timing issues seen with the XA software series). Some Siemens DICOMs stroe slice timings in the private tag [0019,1029](https://github.com/rordenlab/dcm2niix/issues/296). In theory, this could be used when the CSA header is missing. For archival studies, be aware that some sequences [incorrectly reported slice timing](https://github.com/rordenlab/dcm2niix/issues/126). The [SPM slice timing wiki](https://en.wikibooks.org/w/index.php?title=SPM/Slice_Timing&stable=0#Siemens_scanners) provides further information on Siemens slice timing. ## Total Readout Time diff --git a/console/main_console.cpp b/console/main_console.cpp index 7e044a87..9dd02cab 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -94,13 +94,14 @@ void showHelp(const char * argv[], struct TDCMopts opts) { char max16Ch = 'n'; if (opts.isMaximize16BitRange) max16Ch = 'y'; printf(" -l : losslessly scale 16-bit integers to use dynamic range (y/n, default %c)\n", max16Ch); - printf(" -m : merge 2D slices from same series regardless of study time, echo, coil, orientation, etc. (y/n, default n)\n"); + printf(" -m : merge 2D slices from same series regardless of echo, exposure, etc. (n/y or 0/1/2, default 2) [no, yes, auto]\n"); 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"); + printf(" --progress : report progress (y/n, default n)\n"); #if !defined(_WIN64) && !defined(_WIN32) //shell script for Unix only printf(" -u : up-to-date check\n"); #endif @@ -219,6 +220,18 @@ int checkUpToDate() { #endif //shell script for UNIX only +void showXML() { +//https://www.slicer.org/wiki/Documentation/Nightly/Developers/SlicerExecutionModel#XML_Schema + printf("\n"); + printf("\n"); + printf("dcm2niix\n"); + printf("DICOM importer\n"); + printf(" \n"); + printf(" At least one parameter\n"); + printf(" \n"); + printf("\n"); +} + //#define mydebugtest int main(int argc, const char * argv[]) { @@ -249,9 +262,19 @@ int main(int argc, const char * argv[]) int lastCommandArg = 0; while (i < (argc)) { //-1 as final parameter is DICOM directory if ((strlen(argv[i]) > 1) && (argv[i][0] == '-')) { //command - if (argv[i][1] == 'h') + if (argv[i][1] == 'h') { showHelp(argv, opts); - else if ((argv[i][1] == 'a') && ((i+1) < argc)) { //adjacent DICOMs + } else if (( ! strcmp(argv[1], "--progress")) && ((i+1) < argc)) { + i++; + if ((argv[i][0] == 'n') || (argv[i][0] == 'N') || (argv[i][0] == '0')) + opts.isProgress = 0; + else + opts.isProgress = 1; + if (argv[i][0] == '2') opts.isProgress = 2; //logorrheic + } else if ( ! strcmp(argv[1], "--xml")) { + showXML(); + return EXIT_SUCCESS; + } else if ((argv[i][1] == 'a') && ((i+1) < argc)) { //adjacent DICOMs i++; if (invalidParam(i, argv)) return 0; if ((argv[i][0] == 'n') || (argv[i][0] == 'N') || (argv[i][0] == '0')) @@ -337,9 +360,11 @@ int main(int argc, const char * argv[]) i++; if (invalidParam(i, argv)) return 0; if ((argv[i][0] == 'n') || (argv[i][0] == 'N') || (argv[i][0] == '0')) - opts.isForceStackSameSeries = false; + opts.isForceStackSameSeries = 0; if ((argv[i][0] == 'y') || (argv[i][0] == 'Y') || (argv[i][0] == '1')) - opts.isForceStackSameSeries = true; + opts.isForceStackSameSeries = 1; + if ((argv[i][0] == '2')) + opts.isForceStackSameSeries = 2; if ((argv[i][0] == 'o') || (argv[i][0] == 'O')) opts.isForceStackDCE = false; @@ -468,9 +493,9 @@ int main(int argc, const char * argv[]) if ((opts.isRenameNotConvert) && (!isOutNameSpecified)) { //sensible naming scheme for renaming option //strcpy(opts.filename,argv[i]); #if defined(_WIN64) || defined(_WIN32) - strcpy(opts.filename,"%t/%s_%p/%4r.dcm"); //nrrd or nhdr + strcpy(opts.filename,"%t\\%s_%p\\%4r.dcm"); //nrrd or nhdr (windows folders) #else - strcpy(opts.filename,"%t\\%s_%p\\%4r.dcm"); //nrrd or nhdr + strcpy(opts.filename,"%t/%s_%p/%4r.dcm"); //nrrd or nhdr (unix folders) #endif printf("renaming without output filename, assuming '-f %s'\n", opts.filename); } diff --git a/console/makefile b/console/makefile index 34d412f9..27c34705 100644 --- a/console/makefile +++ b/console/makefile @@ -19,7 +19,8 @@ endif ifneq ($(OS),Windows_NT) OS = $(shell uname) ifeq "$(OS)" "Darwin" - CFLAGS=-dead_strip -O3 + #CFLAGS=-dead_strip -O3 + CFLAGS= #CFLAGS=-O2 endif endif diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 28ac3d68..49777fca 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -335,10 +335,11 @@ int verify_slice_dir (struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1_ flip = ((sliceV.v[0]+sliceV.v[1]+sliceV.v[2]) < 0); //printMessage("verify slice dir %g %g %g\n",sliceV.v[0],sliceV.v[1],sliceV.v[2]); if (isVerbose) { //1st pass only - if (!d.isDerived) //do not warn user if image is derived + if (!d.isDerived) {//do not warn user if image is derived printWarning("Unable to determine slice direction: please check whether slices are flipped\n"); - else + } else { printWarning("Unable to determine slice direction: please check whether slices are flipped (derived image)\n"); + } } } if (flip) { @@ -514,11 +515,17 @@ mat44 xform_mat(struct TDICOMdata d) { } else if (true) { //SliceNormalVector TO DO printMessage("Not completed"); +#ifndef USING_R exit(2); +#endif return R44; } printMessage("Unable to determine spatial transform\n"); +#ifndef USING_R exit(1); +#else + return R44; +#endif } mat44 set_nii_header(struct TDICOMdata d) { @@ -653,9 +660,9 @@ int headerDcm2Nii2(struct TDICOMdata d, struct TDICOMdata d2, struct nifti_1_hea sprintf(dtxt, ";mb=%d", d.CSA.multiBandFactor); strcat(txt,dtxt); } - // GCC 8 warns about truncation using snprintf; using strncpy instead seems to keep it happy + // GCC 8 warns about truncation using snprintf // snprintf(h->descrip,80, "%s",txt); - strncpy(h->descrip, txt, 79); + memcpy(h->descrip, txt, 79); h->descrip[79] = '\0'; if (strlen(d.imageComments) > 0) @@ -788,6 +795,7 @@ struct TDICOMdata clear_dicom_data() { d.isSegamiOasis = false; //these images do not store spatial coordinates d.isGrayscaleSoftcopyPresentationState = false; d.isRawDataStorage = false; + d.isDiffusion = false; d.isVectorFromBMatrix = false; d.isStackableSeries = false; //combine DCE series https://github.com/rordenlab/dcm2niix/issues/252 d.isXA10A = false; //https://github.com/rordenlab/dcm2niix/issues/236 @@ -823,9 +831,9 @@ struct TDICOMdata clear_dicom_data() { strcpy(d.patientAge, ""); d.CSA.bandwidthPerPixelPhaseEncode = 0.0; d.CSA.mosaicSlices = 0; - d.CSA.sliceNormV[1] = 1.0; + d.CSA.sliceNormV[1] = 0.0; d.CSA.sliceNormV[2] = 0.0; - d.CSA.sliceNormV[3] = 0.0; + d.CSA.sliceNormV[3] = 1.0; //default Siemens Image Numbering is F>>H https://www.mccauslandcenter.sc.edu/crnl/tools/stc d.CSA.sliceOrder = NIFTI_SLICE_UNKNOWN; d.CSA.slice_start = 0; d.CSA.slice_end = 0; @@ -1014,6 +1022,14 @@ int dcmInt (int lByteLength, unsigned char lBuffer[], bool littleEndian) { //rea return lBuffer[3]+(lBuffer[2]<<8)+(lBuffer[1]<<16)+(lBuffer[0]<<24); //shortint vs word? } //dcmInt() + +uint32_t dcmAttributeTag (unsigned char lBuffer[], bool littleEndian) { + // read Attribute Tag (AT) value + // return in Group + (Element << 16) format + if (littleEndian) + return lBuffer[0]+(lBuffer[1]<<8)+(lBuffer[2]<<16)+(lBuffer[3]<<24); + return lBuffer[1]+(lBuffer[0]<<8)+(lBuffer[3]<<16)+(lBuffer[2]<<24); +} //dcmInt() /* //the code below trims strings after integer // does not appear required not http://en.cppreference.com/w/cpp/string/byte/atoi @@ -1147,7 +1163,86 @@ bool csaIsPhaseMap (unsigned char buff[], int nItems) { return false; } //csaIsPhaseMap() -//int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, int isVerbose, struct TDTI4D *dti4D) { +void checkSliceTimes(struct TCSAdata *CSA, int itemsOK, int isVerbose, bool is3DAcq) { + if ((is3DAcq) || (itemsOK < 1)) //we expect 3D sequences to be simultaneous + return; + if (itemsOK > kMaxEPI3D) { + printError("Please increase kMaxEPI3D and recompile\n"); + return; + } + float maxTimeValue, minTimeValue, timeValue1; + minTimeValue = CSA->sliceTiming[0]; + for (int z = 0; z < itemsOK; z++) + if (CSA->sliceTiming[z] < minTimeValue) + minTimeValue = CSA->sliceTiming[z]; + //CSA can report negative slice times + // https://neurostars.org/t/slice-timing-illegal-values-in-fmriprep/1516/8 + // Nov 1, 2018 wrote: + // If you have an interleaved dataset we can more definitively validate this formula (aka sliceTime(i) - min(sliceTimes())). + if (minTimeValue < 0) { + printWarning("Adjusting for negative MosaicRefAcqTimes (issue 271).\n"); + for (int z = 0; z < itemsOK; z++) + CSA->sliceTiming[z] = CSA->sliceTiming[z] - minTimeValue; + } + CSA->multiBandFactor = 1; + timeValue1 = CSA->sliceTiming[0]; + int nTimeZero = 0; + if (CSA->sliceTiming[0] == 0) + nTimeZero++; + int minTimeIndex = 0; + int maxTimeIndex = minTimeIndex; + minTimeValue = CSA->sliceTiming[0]; + maxTimeValue = minTimeValue; + if (isVerbose > 1) + printMessage(" sliceTimes %g\t", CSA->sliceTiming[0]); + for (int z = 1; z < itemsOK; z++) { //find index and value of fastest time + if (isVerbose > 1) + printMessage("%g\t", CSA->sliceTiming[z]); + if (CSA->sliceTiming[z] == 0) + nTimeZero++; + if (CSA->sliceTiming[z] < minTimeValue) { + minTimeValue = CSA->sliceTiming[z]; + minTimeIndex = (float) z; + } + if (CSA->sliceTiming[z] > maxTimeValue) { + maxTimeValue = CSA->sliceTiming[z]; + maxTimeIndex = (float) z; + } + if (CSA->sliceTiming[z] == timeValue1) CSA->multiBandFactor++; + } + if (isVerbose > 1) + printMessage("\n"); + CSA->slice_start = minTimeIndex; + CSA->slice_end = maxTimeIndex; + if (minTimeIndex == maxTimeIndex) { + if (isVerbose) + printMessage("No variability in slice times (3D EPI?)\n"); + } + if (nTimeZero < 2) { //not for multi-band, not 3D + if (minTimeIndex == 1) + CSA->sliceOrder = NIFTI_SLICE_ALT_INC2;// e.g. 3,1,4,2 + else if (minTimeIndex == (itemsOK-2)) + CSA->sliceOrder = NIFTI_SLICE_ALT_DEC2;// e.g. 2,4,1,3 or 5,2,4,1,3 + else if ((minTimeIndex == 0) && (CSA->sliceTiming[1] < CSA->sliceTiming[2])) + CSA->sliceOrder = NIFTI_SLICE_SEQ_INC; // e.g. 1,2,3,4 + else if ((minTimeIndex == 0) && (CSA->sliceTiming[1] > CSA->sliceTiming[2])) + CSA->sliceOrder = NIFTI_SLICE_ALT_INC; //e.g. 1,3,2,4 + else if ((minTimeIndex == (itemsOK-1)) && (CSA->sliceTiming[itemsOK-3] > CSA->sliceTiming[itemsOK-2])) + CSA->sliceOrder = NIFTI_SLICE_SEQ_DEC; //e.g. 4,3,2,1 or 5,4,3,2,1 + else if ((minTimeIndex == (itemsOK-1)) && (CSA->sliceTiming[itemsOK-3] < CSA->sliceTiming[itemsOK-2])) + CSA->sliceOrder = NIFTI_SLICE_ALT_DEC; //e.g. 4,2,3,1 or 3,5,2,4,1 + else { + if (!is3DAcq) //we expect 3D sequences to be simultaneous + printWarning("Unable to determine slice order from CSA tag MosaicRefAcqTimes\n"); + } + } + if ((CSA->sliceOrder != NIFTI_SLICE_UNKNOWN) && (nTimeZero > 1) && (nTimeZero < itemsOK)) { + if (isVerbose) + printMessage(" Multiband x%d sequence: setting slice order as UNKNOWN (instead of %d)\n", nTimeZero, CSA->sliceOrder); + CSA->sliceOrder = NIFTI_SLICE_UNKNOWN; + } +} //checkSliceTimes() + int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, int isVerbose, bool is3DAcq) { //see also http://afni.nimh.nih.gov/pub/dist/src/siemens_dicom_csa.c //printMessage("%c%c%c%c\n",buff[0],buff[1],buff[2],buff[3]); @@ -1203,98 +1298,25 @@ int readCSAImageHeader(unsigned char *buff, int lLength, struct TCSAdata *CSA, i CSA->sliceNormV[1] = csaMultiFloat (&buff[lPos], 3,lFloats, &itemsOK); CSA->sliceNormV[2] = lFloats[2]; CSA->sliceNormV[3] = lFloats[3]; - if (isVerbose) + if (isVerbose > 1) printMessage(" SliceNormalVector %f %f %f\n",CSA->sliceNormV[1],CSA->sliceNormV[2],CSA->sliceNormV[3]); } else if (strcmp(tagCSA.name, "SliceMeasurementDuration") == 0) 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, "MosaicRefAcqTimes") == 0) && (tagCSA.nitems > 3) ){ - float * sliceTimes = (float *)malloc(sizeof(float) * (tagCSA.nitems + 1)); - csaMultiFloat (&buff[lPos], tagCSA.nitems,sliceTimes, &itemsOK); - float maxTimeValue, minTimeValue, timeValue1; - for (int z = 0; z < kMaxEPI3D; z++) - CSA->sliceTiming[z] = -1.0; - minTimeValue = sliceTimes[1]; - if (itemsOK <= kMaxEPI3D) { - for (int z = 1; z <= itemsOK; z++) { - CSA->sliceTiming[z-1] = sliceTimes[z]; - if (sliceTimes[z] < minTimeValue) - minTimeValue = sliceTimes[z]; - } - } else - printError("Please increase kMaxEPI3D and recompile\n"); - //CSA can report negative slice times - // https://neurostars.org/t/slice-timing-illegal-values-in-fmriprep/1516/8 - // Nov 1, 2018 wrote: - // If you have an interleaved dataset we can more definitively validate this formula (aka sliceTime(i) - min(sliceTimes())). - if (minTimeValue < 0) { - printWarning("Adjusting for negative MosaicRefAcqTimes (issue 271).\n"); - for (int z = 1; z <= itemsOK; z++) { - sliceTimes[z] = sliceTimes[z] - minTimeValue; - CSA->sliceTiming[z-1] = sliceTimes[z]; - } - } - CSA->multiBandFactor = 1; - timeValue1 = sliceTimes[1]; - int nTimeZero = 0; - if (sliceTimes[1] == 0) - nTimeZero++; - int minTimeIndex = 1; - int maxTimeIndex = minTimeIndex; - minTimeValue = sliceTimes[1]; - maxTimeValue = minTimeValue; - if (isVerbose) - printMessage(" sliceTimes %g\t", sliceTimes[1]); - for (int z = 2; z <= itemsOK; z++) { //find index and value of fastest time - if (isVerbose) - printMessage("%g\t", sliceTimes[z]); - if (sliceTimes[z] == 0) - nTimeZero++; - if (sliceTimes[z] < minTimeValue) { - minTimeValue = sliceTimes[z]; - minTimeIndex = (float) z; - } - if (sliceTimes[z] > maxTimeValue) { - maxTimeValue = sliceTimes[z]; - maxTimeIndex = (float) z; - } - if (sliceTimes[z] == timeValue1) CSA->multiBandFactor++; - } - if (isVerbose) - printMessage("\n"); - CSA->slice_start = minTimeIndex -1; - CSA->slice_end = maxTimeIndex -1; - if (minTimeIndex == 2) - CSA->sliceOrder = NIFTI_SLICE_ALT_INC2;// e.g. 3,1,4,2 - else if (minTimeIndex == (itemsOK-1)) - CSA->sliceOrder = NIFTI_SLICE_ALT_DEC2;// e.g. 2,4,1,3 or 5,2,4,1,3 - else if ((minTimeIndex == 1) && (sliceTimes[2] < sliceTimes[3])) - CSA->sliceOrder = NIFTI_SLICE_SEQ_INC; // e.g. 1,2,3,4 - else if ((minTimeIndex == 1) && (sliceTimes[2] > sliceTimes[3])) - CSA->sliceOrder = NIFTI_SLICE_ALT_INC; //e.g. 1,3,2,4 - else if ((minTimeIndex == itemsOK) && (sliceTimes[itemsOK-2] > sliceTimes[itemsOK-1])) - CSA->sliceOrder = NIFTI_SLICE_SEQ_DEC; //e.g. 4,3,2,1 or 5,4,3,2,1 - else if ((minTimeIndex == itemsOK) && (sliceTimes[itemsOK-2] < sliceTimes[itemsOK-1])) - CSA->sliceOrder = NIFTI_SLICE_ALT_DEC; //e.g. 4,2,3,1 or 3,5,2,4,1 - else { - /*NSMutableArray *sliceTimesNS = [NSMutableArray arrayWithCapacity:tagCSA.nitems]; - for (int z = 1; z <= itemsOK; z++) - [sliceTimesNS addObject:[NSNumber numberWithFloat:sliceTimes[z]]]; - NSLog(@" Warning: unable to determine slice order for %lu slice mosaic: %@",(unsigned long)[sliceTimesNS count],sliceTimesNS ); - */ - if (!is3DAcq) //we expect 3D sequences to be simultaneous - printWarning("Unable to determine slice order from CSA tag MosaicRefAcqTimes\n"); - } - if ((CSA->sliceOrder != NIFTI_SLICE_UNKNOWN) && (nTimeZero > 1)) { - if (isVerbose) - printMessage(" Multiband x%d sequence: setting slice order as UNKNOWN (instead of %d)\n", nTimeZero, CSA->sliceOrder); - CSA->sliceOrder = NIFTI_SLICE_UNKNOWN; - + if (itemsOK > kMaxEPI3D) { + printError("Please increase kMaxEPI3D and recompile\n"); + } else { + float * sliceTimes = (float *)malloc(sizeof(float) * (tagCSA.nitems + 1)); + csaMultiFloat (&buff[lPos], tagCSA.nitems,sliceTimes, &itemsOK); + for (int z = 0; z < kMaxEPI3D; z++) + CSA->sliceTiming[z] = -1.0; + for (int z = 0; z < itemsOK; z++) + CSA->sliceTiming[z] = sliceTimes[z+1]; + free(sliceTimes); + checkSliceTimes(CSA, itemsOK, isVerbose, is3DAcq); } -//#ifdef _MSC_VER - free(sliceTimes); -//#endif } else if (strcmp(tagCSA.name, "ProtocolSliceNumber") == 0) CSA->protocolSliceNumber1 = (int) round (csaMultiFloat (&buff[lPos], 1,lFloats, &itemsOK)); else if (strcmp(tagCSA.name, "PhaseEncodingDirectionPositive") == 0) @@ -2462,13 +2484,9 @@ unsigned char * nii_flipImgZ(unsigned char* bImg, struct nifti_1_header *hdr){ int dim4to7 = 1; for (int i = 4; i < 8; i++) if (hdr->dim[i] > 1) dim4to7 = dim4to7 * hdr->dim[i]; - int sliceBytes = hdr->dim[1] * hdr->dim[2] * hdr->bitpix/8; + size_t sliceBytes = hdr->dim[1] * hdr->dim[2] * hdr->bitpix/8; size_t volBytes = sliceBytes * hdr->dim[3]; -//#ifdef _MSC_VER unsigned char * slice = (unsigned char *)malloc(sizeof(unsigned char) * (sliceBytes)); -//#else -// unsigned char slice[sliceBytes]; -//#endif for (int vol = 0; vol < dim4to7; vol++) { //for each 2D slice size_t slBottom = vol*volBytes; size_t slTop = ((vol+1)*volBytes)-sliceBytes; @@ -2481,9 +2499,7 @@ unsigned char * nii_flipImgZ(unsigned char* bImg, struct nifti_1_header *hdr){ slBottom += sliceBytes; } //for Z } //for each volume -//#ifdef _MSC_VER free(slice); -//#endif return bImg; } // nii_flipImgZ() @@ -2629,8 +2645,15 @@ unsigned char * nii_loadImgCore(char* imgname, struct nifti_1_header hdr, int bi } fseek(file, 0, SEEK_END); long fileLen=ftell(file); - if (fileLen < (imgszRead+hdr.vox_offset)) { - printMessage("File not large enough to store image data: %s\n", imgname); + if (fileLen < (imgszRead+(long) hdr.vox_offset)) { + //previously (fileLen < (imgszRead+hdr.vox_offset)) + // FileSize < (ImageSize+HeaderSize): 42399788 < (42398702+1086) + // FileSize < (ImageSize+HeaderSize): 42399788 < ( 42399792.00) + //note hdr.vox_offset is a float, and without a type-cast it can lead to unusual values + //https://www.nitrc.org/forum/message.php?msg_id=27155 + printMessage("FileSize < (ImageSize+HeaderSize): %lu < (%lu+%lu) \n", fileLen, imgszRead, (long)hdr.vox_offset); + //printMessage("FileSize < (ImageSize+HeaderSize): %lu < (%lu) \n", fileLen, imgszRead+(long)hdr.vox_offset); + printWarning("File not large enough to store image data: %s\n", imgname); return NULL; } fseek(file, (long) hdr.vox_offset, SEEK_SET); @@ -3636,7 +3659,7 @@ void set_directionality0018_9075(struct TVolumeDiffusion* ptvd, unsigned char* i void set_orientation0018_9089(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf, bool isLittleEndian); void set_isAtFirstPatientPosition_tvd(struct TVolumeDiffusion* ptvd, bool iafpp); -void set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf); +int set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf); void set_diffusion_directionPhilips(struct TVolumeDiffusion* ptvd, float vec, const int axis); void set_diffusion_directionGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf, int axis); void set_bVal(struct TVolumeDiffusion* ptvd, float b); @@ -3690,7 +3713,7 @@ void set_directionality0018_9075(struct TVolumeDiffusion* ptvd, unsigned char* i _update_tvd(ptvd); } //set_directionality0018_9075() -void set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf) { +int set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf) { //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); @@ -3698,6 +3721,7 @@ void set_bValGE(struct TVolumeDiffusion* ptvd, int lLength, unsigned char* inbuf //printf("(0043,1039) '%s' Slop_int_6 -->%d \n", inbuf, bVal); //dd.CSA.numDti = 1; // Always true for GE. _update_tvd(ptvd); + return bVal; } //set_bValGE() // axis: 0 -> x, 1 -> y , 2 -> z @@ -4000,6 +4024,7 @@ struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, stru #define kManufacturersModelName 0x0008+(0x1090 << 16 ) #define kDerivationDescription 0x0008+(0x2111 << 16 ) #define kComplexImageComponent (uint32_t) 0x0008+(0x9208 << 16 )//'0008' '9208' 'CS' 'ComplexImageComponent' +#define kAcquisitionContrast (uint32_t) 0x0008+(0x9209 << 16 )//'0008' '9209' 'CS' 'AcquisitionContrast' #define kPatientName 0x0010+(0x0010 << 16 ) #define kPatientID 0x0010+(0x0020 << 16 ) #define kAccessionNumber 0x0008+(0x0050 << 16 ) @@ -4066,6 +4091,7 @@ const uint32_t kEffectiveTE = 0x0018+ (0x9082 << 16); //https://nmrimaging.wordpress.com/2011/12/20/when-we-process/ // https://nciphub.org/groups/qindicom/wiki/DiffusionrelatedDICOMtags:experienceacrosssites?action=pdf #define kDiffusionBValueSiemens 0x0019+(0x100C<< 16 ) //IS +#define kSliceTimeSiemens 0x0019+(0x1029<< 16) ///FD #define kDiffusionGradientDirectionSiemens 0x0019+(0x100E<< 16 ) //FD #define kNumberOfDiffusionDirectionGE 0x0019+(0x10E0<< 16) ///DS NumberOfDiffusionDirection:UserData24 #define kLastScanLoc 0x0019+(0x101B<< 16 ) @@ -4092,6 +4118,7 @@ const uint32_t kEffectiveTE = 0x0018+ (0x9082 << 16); #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 kDimensionIndexPointer 0x0020+uint32_t(0x9165<< 16 ) //Private Group 21 as Used by Siemens: #define kSequenceVariant21 0x0021+(0x105B<< 16 )//CS #define kPATModeText 0x0021+(0x1009<< 16 )//LO, see kImaPATModeText @@ -4193,6 +4220,8 @@ double TE = 0.0; //most recent echo time recorded int locationsInAcquisitionGE = 0; int PETImageIndex = 0; int inStackPositionNumber = 0; + uint32_t dimensionIndexPointer[MAX_NUMBER_OF_DIMENSIONS]; + size_t dimensionIndexPointerCounter = 0; int maxInStackPositionNumber = 0; //int temporalPositionIdentifier = 0; int locationsInAcquisitionPhilips = 0; @@ -4214,6 +4243,16 @@ double TE = 0.0; //most recent echo time recorded groupElement = buffer[lPos] | (buffer[lPos+1] << 8) | (buffer[lPos+2] << 16) | (buffer[lPos+3] << 24); if (groupElement != kStart) printMessage("DICOM appears corrupt: first group:element should be 0x0002:0x0000 '%s'\n", fname); + } else { //no isPart10prefix - need to work out if this is explicit VR! + if (isVerbose > 1) + printMessage("DICOM preamble and prefix missing: this is not a valid DICOM image.\n"); + //See Toshiba Aquilion images from https://www.aliza-dicom-viewer.com/download/datasets + lLength = buffer[4] | (buffer[5] << 8) | (buffer[6] << 16) | (buffer[7] << 24); + if (lLength > fileLen) { + if (isVerbose > 1) + printMessage("Guessing this is an explicit VR image.\n"); + d.isExplicitVR = true; + } } char vr[2]; //float intenScalePhilips = 0.0; @@ -4227,6 +4266,7 @@ double TE = 0.0; //most recent echo time recorded bool isOrient = false; //bool isDcm4Che = false; bool isMoCo = false; + bool isPaletteColor = false; bool isInterpolated = false; bool isIconImageSequence = false; bool isSwitchToImplicitVR = false; @@ -4346,9 +4386,24 @@ double TE = 0.0; //most recent echo time recorded printError("Too many slices to track dimensions. Only up to %d are supported\n", kMaxSlice2D); break; } + uint32_t dimensionIndexOrder[MAX_NUMBER_OF_DIMENSIONS]; + for(size_t i = 0; i < nDimIndxVal; i++) + dimensionIndexOrder[i] = i; + + // Bruker Enhanced MR IOD: reorder dimensions to ensure InStackPositionNumber corresponds to the first one + // This will ensure correct ordering of slices in 4D datasets + if (d.manufacturer == kMANUFACTURER_BRUKER) { + for(size_t i = 1; i < dimensionIndexPointerCounter; i++){ + if (dimensionIndexPointer[i] == kInStackPositionNumber){ + //swap with first + dimensionIndexOrder[i] = 0; + dimensionIndexOrder[0] = i; + } + } + } int ndim = nDimIndxVal; for (int i = 0; i < ndim; i++) - dcmDim[numDimensionIndexValues].dimIdx[i] = d.dimensionIndexValues[i]; + dcmDim[numDimensionIndexValues].dimIdx[i] = d.dimensionIndexValues[dimensionIndexOrder[i]]; dcmDim[numDimensionIndexValues].TE = TE; dcmDim[numDimensionIndexValues].intenScale = d.intenScale; dcmDim[numDimensionIndexValues].intenIntercept = d.intenIntercept; @@ -4450,10 +4505,19 @@ double TE = 0.0; //most recent echo time recorded lLength = buffer[lPos+3] | (buffer[lPos+2] << 8) | (buffer[lPos+1] << 16) | (buffer[lPos] << 24); lPos += 4; } else if ( ((buffer[lPos] == 'U') && (buffer[lPos+1] == 'N')) + || ((buffer[lPos] == 'U') && (buffer[lPos+1] == 'C')) + || ((buffer[lPos] == 'U') && (buffer[lPos+1] == 'R')) || ((buffer[lPos] == 'U') && (buffer[lPos+1] == 'T')) + || ((buffer[lPos] == 'U') && (buffer[lPos+1] == 'V')) || ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'B')) + || ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'D')) + || ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'F')) + || ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'L')) + | ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'V')) || ((buffer[lPos] == 'O') && (buffer[lPos+1] == 'W')) + || ((buffer[lPos] == 'S') && (buffer[lPos+1] == 'V')) ) { //VR= UN, OB, OW, SQ || ((buffer[lPos] == 'S') && (buffer[lPos+1] == 'Q')) + //for example of UC/UR/UV/OD/OF/OL/OV/SV see VR conformance test https://www.aliza-dicom-viewer.com/download/datasets lPos = lPos + 4; //skip 2 byte VR string and 2 reserved bytes = 4 bytes if (d.isLittleEndian) lLength = buffer[lPos] | (buffer[lPos+1] << 8) | (buffer[lPos+2] << 16) | (buffer[lPos+3] << 24); @@ -4601,7 +4665,7 @@ double TE = 0.0; //most recent echo time recorded #if defined(myEnableJPEGLS) || defined(myEnableJPEGLS1) d.compressionScheme = kCompressJPEGLS; #else - printMessage("Unsupported transfer syntax '%s' (decode with 'dcmdjpls jpg.dcm raw.dcm' or 'gdcmconv -w jpg.dcm raw.dcm', or recompile dcm2niix with JPEGLS support)\n",transferSyntax); + printWarning("Unsupported transfer syntax '%s' (decode with 'dcmdjpls jpg.dcm raw.dcm' or 'gdcmconv -w jpg.dcm raw.dcm', or recompile dcm2niix with JPEGLS support)\n",transferSyntax); d.imageStart = 1;//abort as invalid (imageStart MUST be >128) #endif } else if (strcmp(transferSyntax, "1.3.46.670589.33.1.4.1") == 0) { @@ -4624,7 +4688,7 @@ double TE = 0.0; //most recent echo time recorded if (lLength < 1) //"1.2.840.10008.1.2" printWarning("Missing transfer syntax: assuming default (1.2.840.10008.1.2)\n"); else { - printMessage("Unsupported transfer syntax '%s' (see www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage)\n",transferSyntax); + printWarning("Unsupported transfer syntax '%s' (see www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage)\n",transferSyntax); d.imageStart = 1;//abort as invalid (imageStart MUST be >128) } } @@ -4783,6 +4847,13 @@ double TE = 0.0; //most recent echo time recorded if (isImaginary) d.isHasImaginary = true; if (isMagnitude) d.isHasMagnitude = true; break; + case kAcquisitionContrast: + char acqContrast[kDICOMStr]; + dcmStr(lLength, &buffer[lPos], acqContrast); + if (((int) strlen(acqContrast) > 8) && (strstr(acqContrast, "DIFFUSION") != NULL)) + d.isDiffusion = true; + break; + case kAcquisitionTime : char acquisitionTimeTxt[kDICOMStr]; dcmStr (lLength, &buffer[lPos], acquisitionTimeTxt); @@ -4872,9 +4943,11 @@ double TE = 0.0; //most recent echo time recorded break; //in theory, 0018,9074 could provide XA10 slice time information, but scrambled by XA10 de-identification: better to use 0021,1104 //case kFrameAcquisitionDateTime: { + // //(0018,9074) DT [20190621095516.140000] YYYYMMDDHHMMSS + // //see https://github.com/rordenlab/dcm2niix/issues/303 // char dateTime[kDICOMStr]; // dcmStr (lLength, &buffer[lPos], dateTime); - // printf("%s\n", dateTime); + // printf("%s\tkFrameAcquisitionDateTime\n", dateTime); //} case kDiffusionDirectionality : {// 0018, 9075 set_directionality0018_9075(&volDiffusion, (&buffer[lPos])); @@ -4904,6 +4977,23 @@ double TE = 0.0; //most recent echo time recorded d.CSA.dtiV[0] = dcmStrInt(lLength, &buffer[lPos]); d.CSA.numDti = 1; break; + case kSliceTimeSiemens : {//Array of FD (64-bit double) + if (d.manufacturer != kMANUFACTURER_SIEMENS) break; + if ((lLength < 8) || ((lLength % 8) != 0)) break; + int nSlicesTimes = lLength / 8; + if (nSlicesTimes > kMaxEPI3D) break; + d.CSA.mosaicSlices = nSlicesTimes; + //printf(">>>> %d\n", nSlicesTimes); + //issue 296: for images de-identified to remove readCSAImageHeader + for (int z = 0; z < nSlicesTimes; z++) + d.CSA.sliceTiming[z] = dcmFloatDouble(8, &buffer[lPos+(z*8)],d.isLittleEndian); + //for (int z = 0; z < nSlicesTimes; z++) + // printf("%d>>>%g\n", z+1, d.CSA.sliceTiming[z]); + checkSliceTimes(&d.CSA, nSlicesTimes, isVerbose, d.is3DAcq); + //d.CSA.dtiV[0] = dcmStrInt(lLength, &buffer[lPos]); + //d.CSA.numDti = 1; + break; } + case kDiffusionGradientDirectionSiemens : { if (d.manufacturer != kMANUFACTURER_SIEMENS) break; float v[4]; @@ -5017,6 +5107,9 @@ double TE = 0.0; //most recent echo time recorded //printf("<%d>\n",inStackPositionNumber); if (inStackPositionNumber > maxInStackPositionNumber) maxInStackPositionNumber = inStackPositionNumber; break; + case kDimensionIndexPointer: + dimensionIndexPointer[dimensionIndexPointerCounter++] = dcmAttributeTag(&buffer[lPos],d.isLittleEndian); + 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 @@ -5043,7 +5136,8 @@ double TE = 0.0; //most recent echo time recorded char interp[kDICOMStr]; dcmStr (lLength, &buffer[lPos], interp); if (strcmp(interp, "PALETTE_COLOR") == 0) - printError("Photometric Interpretation 'PALETTE COLOR' not supported\n"); + isPaletteColor = true; + //printError("Photometric Interpretation 'PALETTE COLOR' not supported\n"); break; } case kPlanarRGB: d.isPlanarRGB = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); @@ -5093,10 +5187,14 @@ double TE = 0.0; //most recent echo time recorded //printMessage("p%gs%d\n", d.accelFactPE, multiBandFactor); break; } case kTimeAfterStart: + //0021,1104 see https://github.com/rordenlab/dcm2niix/issues/303 + // 0021,1104 6@159630 DS 4.635 + // 0021,1104 2@161164 DS 0 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]); + d.CSA.sliceTiming[acquisitionTimesGE_UIH] *= 1000.0; //convert sec to msec + //printf("x\t%d\t%g\tkTimeAfterStart\n", acquisitionTimesGE_UIH, d.CSA.sliceTiming[acquisitionTimesGE_UIH]); acquisitionTimesGE_UIH ++; break; case kPhaseEncodingDirectionPositive: { @@ -5800,8 +5898,10 @@ double TE = 0.0; //most recent echo time recorded if (d.manufacturer == kMANUFACTURER_GE) d.effectiveEchoSpacingGE = dcmInt(lLength,&buffer[lPos],d.isLittleEndian); break; case kDiffusionBFactorGE : - if (d.manufacturer == kMANUFACTURER_GE) - set_bValGE(&volDiffusion, lLength, &buffer[lPos]); + if (d.manufacturer == kMANUFACTURER_GE) { + d.CSA.dtiV[0] = (float)set_bValGE(&volDiffusion, lLength, &buffer[lPos]); + d.CSA.numDti = 1; + } break; case kGeiisFlag: if ((lLength > 4) && (buffer[lPos]=='G') && (buffer[lPos+1]=='E') && (buffer[lPos+2]=='I') && (buffer[lPos+3]=='I')) { @@ -5881,6 +5981,7 @@ double TE = 0.0; //most recent echo time recorded break; } //switch/case for groupElement +#ifndef USING_R if (isVerbose > 1) { //dcm2niix i fast because it does not use a dictionary. // this is a very incomplete DICOM header report, and not a substitute for tools like dcmdump @@ -5941,6 +6042,7 @@ double TE = 0.0; //most recent echo time recorded printMessage("%s\n", str); //if (d.isExplicitVR) printMessage(" VR=%c%c\n", vr[0], vr[1]); } //printMessage(" tag=%04x,%04x length=%u pos=%ld %c%c nest=%d\n", groupElement & 65535,groupElement>>16, lLength, lPos,vr[0], vr[1], nest); +#endif lPos = lPos + (lLength); //printMessage("%d\n",d.imageStart); //printMessage(" DWI bxyz %g %g %g %g %d\n", d.CSA.dtiV[0], d.CSA.dtiV[1], d.CSA.dtiV[2], d.CSA.dtiV[3], d.CSA.numDti); @@ -5950,10 +6052,11 @@ double TE = 0.0; //most recent echo time recorded //printf("%d bval=%g bvec=%g %g %g<<<\n", d.CSA.numDti, d.CSA.dtiV[0], d.CSA.dtiV[1], d.CSA.dtiV[2], d.CSA.dtiV[3]); //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 (encapsulatedDataFragmentStart > 0) { - if (encapsulatedDataFragments > 1) + if (encapsulatedDataFragments > 1) { printError("Compressed image stored as %d fragments: decompress with gdcmconv, Osirix, dcmdjpeg or dcmjp2k %s\n", encapsulatedDataFragments, fname); - else + } else { d.imageStart = encapsulatedDataFragmentStart; + } } else if ((isEncapsulatedData) && (d.imageStart < 128)) { //http://www.dclunie.com/medical-image-faq/html/part6.html //Uncompressed data (unencapsulated) is sent in DICOM as a series of raw bytes or words (little or big endian) in the Value field of the Pixel Data element (7FE0,0010). Encapsulated data on the other hand is sent not as raw bytes or words but as Fragments contained in Items that are the Value field of Pixel Data @@ -5970,7 +6073,7 @@ double TE = 0.0; //most recent echo time recorded // 20161117131643.80000 -> date 20161117 time 131643.80000 //printMessage("acquisitionDateTime %s\n",acquisitionDateTimeTxt); char acquisitionDateTxt[kDICOMStr]; - strncpy(acquisitionDateTxt, acquisitionDateTimeTxt, kYYYYMMDDlen); + memcpy(acquisitionDateTxt, acquisitionDateTimeTxt, kYYYYMMDDlen); acquisitionDateTxt[kYYYYMMDDlen] = '\0'; // IMPORTANT! d.acquisitionDate = atof(acquisitionDateTxt); char acquisitionTimeTxt[kDICOMStr]; @@ -5986,8 +6089,8 @@ double TE = 0.0; //most recent echo time recorded 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 (isVerbose) printMessage("Check number of slices, discrepancy between tags (0020,1002; 0021,104F; 0054,0081) (%d vs %d) %s\n", locationsInAcquisitionGE, d.locationsInAcquisition, fname); + if (locationsInAcquisitionGE < d.locationsInAcquisition) d.locationsInAcquisition = locationsInAcquisitionGE; } if ((d.manufacturer == kMANUFACTURER_GE) && (d.locationsInAcquisition == 0)) d.locationsInAcquisition = locationsInAcquisitionGE; @@ -6022,6 +6125,11 @@ double TE = 0.0; //most recent echo time recorded // d.seriesNum = d.seriesNum + (100*coilNum); //if (d.echoNum > 1) //segment images with multiple echoes // d.seriesNum = d.seriesNum + (kEchoMult*d.echoNum); + if (isPaletteColor) { + d.isValid = false; + d.isDerived = true; //to my knowledge, palette images always derived + printWarning("Photometric Interpretation 'PALETTE COLOR' not supported\n"); + } if ((d.compressionScheme == kCompress50) && (d.bitsAllocated > 8) ) { //dcmcjpg with +ee can create .51 syntax images that are 8,12,16,24-bit: we can only decode 8/24-bit printError("Unable to decode %d-bit images with Transfer Syntax 1.2.840.10008.1.2.4.51, decompress with dcmdjpg or gdcmconv\n", d.bitsAllocated); @@ -6093,7 +6201,7 @@ if (d.isHasPhase) d.patientPositionLast[k] = patientPositionEndPhilips[k]; } } - if (!isnan(patientPositionStartPhilips[1])) //for Philips data without + if (!isnan(patientPositionStartPhilips[1])) //for Philips data without for (int k = 0; k < 4; k++) d.patientPosition[k] = patientPositionStartPhilips[k]; if (isVerbose) { @@ -6106,9 +6214,14 @@ if (d.isHasPhase) 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 ((d.xyzDim[1] > 1) && (d.xyzDim[2] > 1) && (d.imageStart < 132)) { - printError("Conversion aborted due to corrupt file: %s\n", fname); + if ((d.xyzDim[1] > 1) && (d.xyzDim[2] > 1) && (d.imageStart < 132) && (!d.isRawDataStorage)) { + //20190524: Philips MR 55.1 creates non-image files that report kDim1/kDim2 - we can detect them since 0008,0016 reports "RawDataStorage" + printError("Conversion aborted due to corrupt file: %s %d %d\n", fname, d.xyzDim[1], d.xyzDim[2]); +#ifdef USING_R + Rf_error("Irrecoverable error during conversion"); +#else exit (kEXIT_CORRUPT_FILE_FOUND); +#endif } if ((numDimensionIndexValues > 1) && (numDimensionIndexValues == numberOfFrames)) { //Philips enhanced datasets can have custom slice orders and pack images with different TE, Phase/Magnitude/Etc. @@ -6128,19 +6241,17 @@ if (d.isHasPhase) 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 #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("diskPos= %d dimIdx= %d %d %d %d TE= %g\n", i, dcmDim[i].diskPos, dcmDim[i].dimIdx[1], dcmDim[i].dimIdx[2], dcmDim[i].dimIdx[3], dti4D->TE[i]); for (int i = 0; i < numberOfFrames; i++) dti4D->sliceOrder[i] = dcmDim[i].diskPos; - if ((d.manufacturer != kMANUFACTURER_BRUKER) && (d.xyzDim[4] > 1) && (d.xyzDim[4] < kMaxDTI4D)) { //record variations in TE + if ( !(d.manufacturer == kMANUFACTURER_BRUKER && d.isDiffusion) && (d.xyzDim[4] > 1) && (d.xyzDim[4] < kMaxDTI4D)) { //record variations in TE d.isScaleOrTEVaries = false; bool isTEvaries = false; bool isScaleVaries = false; @@ -6257,8 +6368,9 @@ if (d.isHasPhase) d.isLocalizer = true; } //printf(">>%s\n", d.sequenceName); d.isValid = false; - if ((d.CSA.numDti > 0) && (d.manufacturer == kMANUFACTURER_GE) && (d.numberOfDiffusionDirectionGE < 1)) - d.CSA.numDti = 0; //https://github.com/rordenlab/dcm2niix/issues/264 + // Andrey Fedorov has requested keeping GE bvalues, see issue 264 + //if ((d.CSA.numDti > 0) && (d.manufacturer == kMANUFACTURER_GE) && (d.numberOfDiffusionDirectionGE < 1)) + // d.CSA.numDti = 0; //https://github.com/rordenlab/dcm2niix/issues/264 if ((!d.isLocalizer) && (isInterpolated) && (d.imageNum <= 1)) printWarning("interpolated protocol '%s' may be unsuitable for dwidenoise/mrdegibbs. %s\n", d.protocolName, fname); if ((numDimensionIndexValues+2) < MAX_NUMBER_OF_DIMENSIONS) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index eea3f6de..d6657087 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -43,7 +43,7 @@ extern "C" { #define kCCsuf " CompilerNA" //unknown compiler! #endif -#define kDCMdate "v1.0.20190410" +#define kDCMdate "v1.0.20190720" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic @@ -173,7 +173,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; char institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; struct TCSAdata CSA; - bool isVectorFromBMatrix, isRawDataStorage, isGrayscaleSoftcopyPresentationState, isStackableSeries, isCoilVaries, isNonParallelSlices, isSegamiOasis, isXA10A, isScaleOrTEVaries, isDerived, isXRay, isMultiEcho, isValid, is3DAcq, is2DAcq, isExplicitVR, isLittleEndian, isPlanarRGB, isSigned, isHasPhase, isHasImaginary, isHasReal, isHasMagnitude,isHasMixed, isFloat, isResampled, isLocalizer; + bool isDiffusion, isVectorFromBMatrix, isRawDataStorage, isGrayscaleSoftcopyPresentationState, isStackableSeries, 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; }; diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 76262f25..2da90327 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -33,7 +33,9 @@ #include "nifti1.h" #endif #include "nii_dicom_batch.h" +#ifndef USING_R #include "nii_foreign.h" +#endif #include "nii_dicom.h" #include //toupper #include @@ -80,6 +82,7 @@ struct TDCMsort { uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; }; + struct TSearchList { unsigned long numItems, maxItems; char **str; @@ -177,7 +180,7 @@ bool is_exe(const char* path) { //requires #include }// is_dir() #endif -void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx){ +void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isVerbose){ //0018,1312 phase encoding is either in row or column direction //0043,1039 (or 0043,a039). b value (as the first number in the string). //0019,10bb (or 0019,a0bb). phase diffusion direction @@ -219,9 +222,11 @@ void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx){ } if (sliceDir < 0) flp.v[2] = 1 - flp.v[2]; - 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"); + if ((isVerbose) || (!col)) { + 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) + printWarning("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]) @@ -270,7 +275,7 @@ void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx){ vx[i].V[v] = 0.0f; }// geCorrectBvecs() -void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx){ +void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isVerbose){ //see Matthew Robson's http://users.fmrib.ox.ac.uk/~robson/internal/Dicom2Nifti111.m //convert DTI vectors from scanner coordinates to image frame of reference //Uses 6 orient values from ImageOrientationPatient (0020,0037) @@ -323,7 +328,8 @@ void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI } else 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) || (d->manufacturer != kMANUFACTURER_PHILIPS)) { - printMessage("Saving %d DTI gradients. Validate vectors.\n", d->CSA.numDti); + if (isVerbose) + printMessage("Saving %d DTI gradients. Validate vectors.\n", d->CSA.numDti); } 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); if (d->manufacturer == kMANUFACTURER_BRUKER) @@ -360,14 +366,16 @@ void nii_saveText(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, fclose(fp); }// nii_saveText() +#ifndef USING_R #define myReadAsciiCsa +#endif #ifdef myReadAsciiCsa //read from the ASCII portion of the Siemens CSA series header // this is not recommended: poorly documented // it is better to stick to the binary portion of the Siemens CSA image header -#if defined(_WIN64) || defined(_WIN32) +#if defined(_WIN64) || defined(_WIN32) || defined(__sun) //https://opensource.apple.com/source/Libc/Libc-1044.1.2/string/FreeBSD/memmem.c /*- * Copyright (c) 2005 Pascal Gloor @@ -419,6 +427,19 @@ const void * memmem(const char *l, size_t l_len, const char *s, size_t s_len) { //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 readKeyN1(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)); + if (!keyPos) return -1; + int i = (int)strlen(key); + while( ( i< remLength) && (keyPos[i] != 0x0A) ) { + if( keyPos[i] >= '0' && keyPos[i] <= '9' ) + ret = (10 * ret) + keyPos[i] - '0'; + i++; + } + return ret; +} //readKeyN1() //return -1 if key not found + 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)); @@ -525,26 +546,32 @@ int phoenixOffsetCSASeriesHeader(unsigned char *buff, int lLength) { #define kMaxWipFree 64 typedef struct { + float delayTimeInTR, phaseOversampling, phaseResolution, txRefAmp; + int phaseEncodingLines, existUcImageNumb, ucMode, baseResolution, interp, partialFourier,echoSpacing, + difBipolar, parallelReductionFactorInPlane, refLinesPE; float alFree[kMaxWipFree] ; float adFree[kMaxWipFree]; float alTI[kMaxWipFree]; float dThickness, ulShape, sPositionDTra, sNormalDTra; -} TsWipMemBlock; +} TCsaAscii; -void siemensCsaAscii(const char * filename, TsWipMemBlock* sWipMemBlock, int csaOffset, int csaLength, float* delayTimeInTR, float* phaseOversampling, float* phaseResolution, float* txRefAmp, float* shimSetting, int* baseResolution, int* interp, int* partialFourier, int* echoSpacing, int* difBipolar, int* parallelReductionFactorInPlane, int* refLinesPE, char* coilID, char* consistencyInfo, char* coilElements, char* pulseSequenceDetails, char* fmriExternalInfo, char* protocolName, char* wipMemBlock) { +void siemensCsaAscii(const char * filename, TCsaAscii* csaAscii, int csaOffset, int csaLength, float* shimSetting, 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; - *phaseOversampling = 0.0; - *phaseResolution = 0.0; - *txRefAmp = 0.0; - *baseResolution = 0; - *interp = 0; - *partialFourier = 0; - *echoSpacing = 0; - *difBipolar = 0; //0=not assigned,1=bipolar,2=monopolar - *parallelReductionFactorInPlane = 0; - *refLinesPE = 0; + csaAscii->delayTimeInTR = -0.001; + csaAscii->phaseOversampling = 0.0; + csaAscii->phaseResolution = 0.0; + csaAscii->txRefAmp = 0.0; + csaAscii->phaseEncodingLines = 0; + csaAscii->existUcImageNumb = 0; + csaAscii->ucMode = -1; + csaAscii->baseResolution = 0; + csaAscii->interp = 0; + csaAscii->partialFourier = 0; + csaAscii->echoSpacing = 0; + csaAscii->difBipolar = 0; //0=not assigned,1=bipolar,2=monopolar + csaAscii->parallelReductionFactorInPlane = 0; + csaAscii->refLinesPE = 0; for (int i = 0; i < 8; i++) shimSetting[i] = 0.0; strcpy(coilID, ""); @@ -591,29 +618,38 @@ void siemensCsaAscii(const char * filename, TsWipMemBlock* sWipMemBlock, int csa if ((keyPosEnd) && ((keyPosEnd - keyPos) < csaLengthTrim)) //ignore binary data at end csaLengthTrim = (int)(keyPosEnd - keyPos); #endif + + char keyStrLns[] = "sKSpace.lPhaseEncodingLines"; + csaAscii->phaseEncodingLines = readKey(keyStrLns, keyPos, csaLengthTrim); + char keyStrUcImg[] = "sSliceArray.ucImageNumb"; + csaAscii->existUcImageNumb = readKey(keyStrUcImg, keyPos, csaLengthTrim); + char keyStrUcMode[] = "sSliceArray.ucMode"; + csaAscii->ucMode = readKeyN1(keyStrUcMode, keyPos, csaLengthTrim); + char keyStrBase[] = "sKSpace.lBaseResolution"; + csaAscii->baseResolution = readKey(keyStrBase, keyPos, csaLengthTrim); + char keyStrInterp[] = "sKSpace.uc2DInterpolation"; + csaAscii->interp = readKey(keyStrInterp, keyPos, csaLengthTrim); + char keyStrPF[] = "sKSpace.ucPhasePartialFourier"; + csaAscii->partialFourier = readKey(keyStrPF, keyPos, csaLengthTrim); + char keyStrES[] = "sFastImaging.lEchoSpacing"; + csaAscii->echoSpacing = readKey(keyStrES, keyPos, csaLengthTrim); char keyStrDS[] = "sDiffusion.dsScheme"; - *difBipolar = readKey(keyStrDS, keyPos, csaLengthTrim); - if (*difBipolar == 0) { + csaAscii->difBipolar = readKey(keyStrDS, keyPos, csaLengthTrim); + if (csaAscii->difBipolar == 0) { char keyStrROM[] = "ucReadOutMode"; - *difBipolar = readKey(keyStrROM, keyPos, csaLengthTrim); - if ((*difBipolar >= 1) && (*difBipolar <= 2)) { //E11C Siemens/CMRR dsScheme: 1=bipolar, 2=unipolar, B17 CMRR ucReadOutMode 0x1=monopolar, 0x2=bipolar - *difBipolar = 3 - *difBipolar; + csaAscii->difBipolar = readKey(keyStrROM, keyPos, csaLengthTrim); + if ((csaAscii->difBipolar >= 1) && (csaAscii->difBipolar <= 2)) { //E11C Siemens/CMRR dsScheme: 1=bipolar, 2=unipolar, B17 CMRR ucReadOutMode 0x1=monopolar, 0x2=bipolar + csaAscii->difBipolar = 3 - csaAscii->difBipolar; } //https://github.com/poldracklab/fmriprep/pull/1359#issuecomment-448379329 } - char keyStrES[] = "sFastImaging.lEchoSpacing"; - *echoSpacing = readKey(keyStrES, keyPos, csaLengthTrim); - char keyStrBase[] = "sKSpace.lBaseResolution"; - *baseResolution = readKey(keyStrBase, keyPos, csaLengthTrim); - char keyStrInterp[] = "sKSpace.uc2DInterpolation"; - *interp = readKey(keyStrInterp, keyPos, csaLengthTrim); - char keyStrPF[] = "sKSpace.ucPhasePartialFourier"; - *partialFourier = readKey(keyStrPF, keyPos, csaLengthTrim); + char keyStrAF[] = "sPat.lAccelFactPE"; + csaAscii->parallelReductionFactorInPlane = readKey(keyStrAF, keyPos, csaLengthTrim); + char keyStrRef[] = "sPat.lRefLinesPE"; + csaAscii->refLinesPE = readKey(keyStrRef, keyPos, csaLengthTrim); + + //char keyStrETD[] = "sFastImaging.lEchoTrainDuration"; //*echoTrainDuration = readKey(keyStrETD, keyPos, csaLengthTrim); - char keyStrRef[] = "sPat.lRefLinesPE"; - *refLinesPE = readKey(keyStrRef, keyPos, csaLengthTrim); - char keyStrAF[] = "sPat.lAccelFactPE"; - *parallelReductionFactorInPlane = readKey(keyStrAF, keyPos, csaLengthTrim); //char keyStrEF[] = "sFastImaging.lEPIFactor"; //ret = readKey(keyStrEF, keyPos, csaLengthTrim); char keyStrCoil[] = "sCoilElementID.tCoilID"; @@ -630,69 +666,69 @@ void siemensCsaAscii(const char * filename, TsWipMemBlock* sWipMemBlock, int csa readKeyStr(keyStrPn, keyPos, csaLengthTrim, protocolName); //read ALL alTI[*] values for (int k = 0; k < kMaxWipFree; k++) - sWipMemBlock->alTI[k] = NAN; + csaAscii->alTI[k] = NAN; char keyStrTiFree[] = "alTI["; - //check if ANY sWipMemBlock.alFree tags exist + //check if ANY csaAscii.alFree tags exist char *keyPosTi = (char *)memmem(keyPos, csaLengthTrim, keyStrTiFree, strlen(keyStrTiFree)); if (keyPosTi) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; sprintf(txt, "%s%d]", keyStrTiFree,k); - sWipMemBlock->alTI[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); + csaAscii->alTI[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); } } - //read ALL sWipMemBlock.alFree[*] values + //read ALL csaAscii.alFree[*] values for (int k = 0; k < kMaxWipFree; k++) - sWipMemBlock->alFree[k] = 0.0; + csaAscii->alFree[k] = 0.0; char keyStrAlFree[] = "sWipMemBlock.alFree["; - //check if ANY sWipMemBlock.alFree tags exist + //check if ANY csaAscii.alFree tags exist char *keyPosFree = (char *)memmem(keyPos, csaLengthTrim, keyStrAlFree, strlen(keyStrAlFree)); if (keyPosFree) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; sprintf(txt, "%s%d]", keyStrAlFree,k); - sWipMemBlock->alFree[k] = readKeyFloat(txt, keyPos, csaLengthTrim); + csaAscii->alFree[k] = readKeyFloat(txt, keyPos, csaLengthTrim); } } - //read ALL sWipMemBlock.adFree[*] values + //read ALL csaAscii.adFree[*] values for (int k = 0; k < kMaxWipFree; k++) - sWipMemBlock->adFree[k] = NAN; + csaAscii->adFree[k] = NAN; char keyStrAdFree[50]; strcpy(keyStrAdFree, "sWipMemBlock.adFree["); //char keyStrAdFree[] = "sWipMemBlock.adFree["; - //check if ANY sWipMemBlock.adFree tags exist + //check if ANY csaAscii.adFree tags exist keyPosFree = (char *)memmem(keyPos, csaLengthTrim, keyStrAdFree, strlen(keyStrAdFree)); if (!keyPosFree) { //"Wip" -> "WiP", modern -> old Siemens - strcpy(keyStrAdFree, "sWiPMemBlock.adFree["); + strcpy(keyStrAdFree, "sWipMemBlock.adFree["); keyPosFree = (char *)memmem(keyPos, csaLengthTrim, keyStrAdFree, strlen(keyStrAdFree)); } if (keyPosFree) { for (int k = 0; k < kMaxWipFree; k++) { char txt[1024] = {""}; sprintf(txt, "%s%d]", keyStrAdFree,k); - sWipMemBlock->adFree[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); + csaAscii->adFree[k] = readKeyFloatNan(txt, keyPos, csaLengthTrim); } } //read labelling plane char keyStrDThickness[] = "sRSatArray.asElm[1].dThickness"; - sWipMemBlock->dThickness = readKeyFloat(keyStrDThickness, keyPos, csaLengthTrim); - if (sWipMemBlock->dThickness > 0.0) { + csaAscii->dThickness = readKeyFloat(keyStrDThickness, keyPos, csaLengthTrim); + if (csaAscii->dThickness > 0.0) { char keyStrUlShape[] = "sRSatArray.asElm[1].ulShape"; - sWipMemBlock->ulShape = readKeyFloat(keyStrUlShape, keyPos, csaLengthTrim); + csaAscii->ulShape = readKeyFloat(keyStrUlShape, keyPos, csaLengthTrim); char keyStrSPositionDTra[] = "sRSatArray.asElm[1].sPosition.dTra"; - sWipMemBlock->sPositionDTra = readKeyFloat(keyStrSPositionDTra, keyPos, csaLengthTrim); + csaAscii->sPositionDTra = readKeyFloat(keyStrSPositionDTra, keyPos, csaLengthTrim); char keyStrSNormalDTra[] = "sRSatArray.asElm[1].sNormal.dTra"; - sWipMemBlock->sNormalDTra = readKeyFloat(keyStrSNormalDTra, keyPos, csaLengthTrim); + csaAscii->sNormalDTra = readKeyFloat(keyStrSNormalDTra, keyPos, csaLengthTrim); } //read delay time char keyStrDelay[] = "lDelayTimeInTR"; - *delayTimeInTR = readKeyFloat(keyStrDelay, keyPos, csaLengthTrim); + csaAscii->delayTimeInTR = readKeyFloat(keyStrDelay, keyPos, csaLengthTrim); char keyStrOver[] = "sKSpace.dPhaseOversamplingForDialog"; - *phaseOversampling = readKeyFloat(keyStrOver, keyPos, csaLengthTrim); + csaAscii->phaseOversampling = readKeyFloat(keyStrOver, keyPos, csaLengthTrim); char keyStrPhase[] = "sKSpace.dPhaseResolution"; - *phaseResolution = readKeyFloat(keyStrPhase, keyPos, csaLengthTrim); + csaAscii->phaseResolution = readKeyFloat(keyStrPhase, keyPos, csaLengthTrim); char keyStrAmp[] = "sTXSPEC.asNucleusInfo[0].flReferenceAmplitude"; - *txRefAmp = readKeyFloat(keyStrAmp, keyPos, csaLengthTrim); + csaAscii->txRefAmp = readKeyFloat(keyStrAmp, keyPos, csaLengthTrim); //lower order shims: newer sequences char keyStrSh0[] = "sGRADSPEC.asGPAData[0].lOffsetX"; shimSetting[0] = readKeyFloat(keyStrSh0, keyPos, csaLengthTrim); @@ -723,15 +759,16 @@ void siemensCsaAscii(const char * filename, TsWipMemBlock* sWipMemBlock, int csa free (buffer); return; } // siemensCsaAscii() + #endif //myReadAsciiCsa() #ifndef myDisableZLib //Uncomment next line to decode GE Protocol Data Block, for caveats see https://github.com/rordenlab/dcm2niix/issues/163 - // #define myReadGeProtocolBlock + #define myReadGeProtocolBlock #endif #ifdef myReadGeProtocolBlock int geProtocolBlock(const char * filename, int geOffset, int geLength, int isVerbose, int* sliceOrder, int* viewOrder) { - *sliceOrder = 0; + *sliceOrder = -1; *viewOrder = 0; int ret = EXIT_FAILURE; if ((geOffset < 0) || (geLength < 20)) return ret; @@ -799,7 +836,7 @@ int geProtocolBlock(const char * filename, int geOffset, int geLength, int isV if ((pUnCmp[0] == '<') && (pUnCmp[1] == '?')) printWarning("New XML-based GE Protocol Block is not yet supported: please report issue on dcm2niix Github page\n"); char keyStrSO[] = "SLICEORDER"; - *sliceOrder = readKey(keyStrSO, (char *) pUnCmp, unCmpSz); + *sliceOrder = readKeyN1(keyStrSO, (char *) pUnCmp, unCmpSz); char keyStrVO[] = "VIEWORDER"; //"MATRIXX"; *viewOrder = readKey(keyStrVO, (char *) pUnCmp, unCmpSz); if (isVerbose > 1) { @@ -838,6 +875,11 @@ void json_FloatNotNan(FILE *fp, const char *sLabel, float sVal) { fprintf(fp, sLabel, sVal ); } //json_Float +void print_FloatNotNan(const char *sLabel, int iVal, float sVal) { + if (isnan(sVal)) return; + printMessage(sLabel, iVal, sVal); +} //json_Float + void json_Float(FILE *fp, const char *sLabel, float sVal) { if (sVal <= 0.0) return; fprintf(fp, sLabel, sVal ); @@ -847,13 +889,13 @@ 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, difBipolar, parallelReductionFactorInPlane, refLinesPE; - //float pf = 1.0f; //partial fourier - float phaseOversampling, delayTimeInTR, phaseResolution, txRefAmp, shimSetting[8]; +#ifdef myReadAsciiCsa + float shimSetting[8]; char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], pulseSequenceDetails[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; - TsWipMemBlock sWipMemBlock; - siemensCsaAscii(filename, &sWipMemBlock, d->CSA.SeriesHeader_offset, d->CSA.SeriesHeader_length, &delayTimeInTR, &phaseOversampling, &phaseResolution, &txRefAmp, shimSetting, &baseResolution, &interpInt, &partialFourier, &echoSpacing, &difBipolar, ¶llelReductionFactorInPlane, &refLinesPE, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); + TCsaAscii csaAscii; + siemensCsaAscii(filename, &csaAscii, d->CSA.SeriesHeader_offset, d->CSA.SeriesHeader_length, shimSetting, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); strcpy(d->protocolName, protocolName); +#endif } void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename) { @@ -863,7 +905,7 @@ void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, // we will use %g for floats since exponents are allowed // we will not set the locale, so decimal separator is always a period, as required // https://www.ietf.org/rfc/rfc4627.txt - if ((!opts.isCreateBIDS) && (opts.isOnlyBIDS)) printMessage("Input-only mode: no BIDS/NIfTI output generated.\n"); + if ((!opts.isCreateBIDS) && (opts.isOnlyBIDS)) printMessage("Input-only mode: no BIDS/NIfTI output generated for '%s'\n", pathoutname); if (!opts.isCreateBIDS) return; char txtname[2048] = {""}; strcpy (txtname,pathoutname); @@ -890,24 +932,26 @@ void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, //attempt to determine BIDS sequence type /*(0018,0024) SequenceName ep_b: dwi -epfid2d: perf (unlike bold, asl has "MR_ASL" in CSA header) - pCASL sequences store Post Label Delay in sWipMemBlock.adFree[2] = 1200000.0 +epfid2d: perf epfid2d: bold +epfid3d1_15: swi epse2d: dwi (when b-vals specified) epse2d: fmap (spin echo, e.g. TOPUP, nb could also be extra B=0 for DWI sequence) fl2d: localizer -fl3d1r_t: angio (not sure how to detect angio or swi!) +fl3d1r_t: angio fl3d1r_tm: angio -fl3d1r: angio (20180628134317) +fl3d1r: angio fl3d1r: swi +fl3d1r: ToF fm2d: fmap (gradient echo, e.g. FUGUE) spc3d: T2 -spcir: flair -spcir:also double-inversion "SequenceVariant": "SK_SP_MP_OSP","ScanOptions": "IR_PFP_FS", +spcir: flair (dark fluid) spcR: PD tfl3d: T1 +tfl_me3d5_16ns: T1 (ME-MPRAGE) tir2d: flair tse2d: PD +tse2d: T2 tse3d: T2*/ /* if (d.manufacturer == kMANUFACTURER_SIEMENS) { @@ -1088,110 +1132,175 @@ tse3d: T2*/ 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)) { - int viewOrderGE = -1; - int sliceOrderGE = -1; - printWarning("Using GE Protocol Data Block for BIDS data (beware: new feature)\n"); - int ok = geProtocolBlock(filename, d.protocolBlockStartGE, d.protocolBlockLengthGE, opts.isVerbose, &sliceOrderGE, &viewOrderGE); - if (ok != EXIT_SUCCESS) - printWarning("Unable to decode GE protocol block\n"); - printMessage(" ViewOrder %d SliceOrder %d\n", viewOrderGE, sliceOrderGE); - } //read protocolBlockGE - #endif + + float delayTimeInTR = -0.01; #ifdef myReadAsciiCsa if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.SeriesHeader_offset > 0) && (d.CSA.SeriesHeader_length > 0)) { - int baseResolution, interpInt, partialFourier, echoSpacing, difBipolar, parallelReductionFactorInPlane, refLinesPE; float pf = 1.0f; //partial fourier - float delayTimeInTR, phaseResolution, txRefAmp, shimSetting[8]; + float shimSetting[8]; + char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], pulseSequenceDetails[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; - TsWipMemBlock sWipMemBlock; - siemensCsaAscii(filename, &sWipMemBlock, d.CSA.SeriesHeader_offset, d.CSA.SeriesHeader_length, &delayTimeInTR, &phaseOversampling, &phaseResolution, &txRefAmp, shimSetting, &baseResolution, &interpInt, &partialFourier, &echoSpacing, &difBipolar, ¶llelReductionFactorInPlane, &refLinesPE, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); - //ASL specific tags - Danny J.J. Wang http://www.loft-lab.org + TCsaAscii csaAscii; + siemensCsaAscii(filename, &csaAscii, d.CSA.SeriesHeader_offset, d.CSA.SeriesHeader_length, shimSetting, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); + if ((d.phaseEncodingLines < 1) && (csaAscii.phaseEncodingLines > 0)) + d.phaseEncodingLines = csaAscii.phaseEncodingLines; + //if (d.phaseEncodingLines != csaAscii.phaseEncodingLines) //e.g. phaseOversampling + // printWarning("PhaseEncodingLines reported in DICOM (%d) header does not match value CSA-ASCII (%d) %s\n", d.phaseEncodingLines, csaAscii.phaseEncodingLines, pathoutname); + delayTimeInTR = csaAscii.delayTimeInTR; + phaseOversampling = csaAscii.phaseOversampling; + if (csaAscii.existUcImageNumb > 0) { + if (d.CSA.protocolSliceNumber1 < 2) { + printWarning("Assuming mosaics saved in reverse order due to 'sSliceArray.ucImageNumb'\n"); + //never seen such an image in the wild.... sliceDir may need to be reversed + } + d.CSA.protocolSliceNumber1 = 2; + } + //ultra-verbose output for deciphering adFree/alFree/alTI values: + /* + if (opts.isVerbose > 1) { + for (int i = 0; i < kMaxWipFree; i++) + print_FloatNotNan("adFree[%d]=\t%g\n",i, csaAscii.adFree[i]); + for (int i = 0; i < kMaxWipFree; i++) + print_FloatNotNan("alFree[%d]=\t%g\n",i, csaAscii.alFree[i]); + for (int i = 0; i < kMaxWipFree; i++) + print_FloatNotNan("alTI[%d]=\t%g\n",i, csaAscii.alTI[i]); + } //verbose + */ + //ASL specific tags - 2D pCASL Danny J.J. Wang http://www.loft-lab.org if (strstr(pulseSequenceDetails,"ep2d_pcasl")) { - json_FloatNotNan(fp, "\t\"LabelOffset\": %g,\n", sWipMemBlock.adFree[1]); //mm - json_FloatNotNan(fp, "\t\"PostLabelDelay\": %g,\n", sWipMemBlock.adFree[2] * (1.0/1000000.0)); //usec -> sec - json_FloatNotNan(fp, "\t\"NumRFBlocks\": %g,\n", sWipMemBlock.adFree[3]); - json_FloatNotNan(fp, "\t\"RFGap\": %g,\n", sWipMemBlock.adFree[4] * (1.0/1000000.0) * (1.0/1000000.0)); //usec -> sec - json_FloatNotNan(fp, "\t\"MeanGzx10\": %g,\n", sWipMemBlock.adFree[10]); // mT/m - json_FloatNotNan(fp, "\t\"PhiAdjust\": %g,\n", sWipMemBlock.adFree[11]); // percent + json_FloatNotNan(fp, "\t\"LabelOffset\": %g,\n", csaAscii.adFree[1]); //mm + json_FloatNotNan(fp, "\t\"PostLabelDelay\": %g,\n", csaAscii.adFree[2] * (1.0/1000000.0)); //usec -> sec + json_FloatNotNan(fp, "\t\"NumRFBlocks\": %g,\n", csaAscii.adFree[3]); + json_FloatNotNan(fp, "\t\"RFGap\": %g,\n", csaAscii.adFree[4] * (1.0/1000000.0)); //usec -> sec + json_FloatNotNan(fp, "\t\"MeanGzx10\": %g,\n", csaAscii.adFree[10]); // mT/m + json_FloatNotNan(fp, "\t\"PhiAdjust\": %g,\n", csaAscii.adFree[11]); // percent + } + //ASL specific tags - 3D pCASL Danny J.J. Wang http://www.loft-lab.org + if (strstr(pulseSequenceDetails,"tgse_pcasl")) { + json_FloatNotNan(fp, "\t\"RFGap\": %g,\n", csaAscii.adFree[4] * (1.0/1000000.0)); //usec -> sec + json_FloatNotNan(fp, "\t\"MeanGzx10\": %g,\n", csaAscii.adFree[10]); // mT/m + json_FloatNotNan(fp, "\t\"T1\": %g,\n", csaAscii.adFree[12] * (1.0/1000000.0)); //usec -> sec } //ASL specific tags - 2D PASL Siemens Product if (strstr(pulseSequenceDetails,"ep2d_pasl")) { - json_FloatNotNan(fp, "\t\"InversionTime1\": %g,\n", sWipMemBlock.alTI[1] * (1.0/1000.0)); //ms->sec - json_FloatNotNan(fp, "\t\"InversionTime2\": %g,\n", sWipMemBlock.alTI[0] * (1.0/1000.0)); //usec -> sec - json_FloatNotNan(fp, "\t\"SaturationStopTime\": %g,\n", sWipMemBlock.alTI[2] * (1.0/1000.0)); + json_FloatNotNan(fp, "\t\"InversionTime\": %g,\n", csaAscii.alTI[0] * (1.0/1000000.0)); //us -> sec + json_FloatNotNan(fp, "\t\"SaturationStopTime\": %g,\n", csaAscii.alTI[2] * (1.0/1000000.0)); //us -> sec } //ASL specific tags - 3D PASL Siemens Product http://adni.loni.usc.edu/wp-content/uploads/2010/05/ADNI3_Basic_Siemens_Skyra_E11.pdf if (strstr(pulseSequenceDetails,"tgse_pasl")) { - json_FloatNotNan(fp, "\t\"BolusDuration\": %g,\n", sWipMemBlock.alTI[0] * (1.0/1000.0)); //ms->sec - json_FloatNotNan(fp, "\t\"InversionTime1\": %g,\n", sWipMemBlock.alTI[2] * (1.0/1000.0)); //usec -> sec - json_FloatNotNan(fp, "\t\"SaturationStopTime\": %g,\n", sWipMemBlock.alTI[2] * (1.0/1000.0)); + json_FloatNotNan(fp, "\t\"BolusDuration\": %g,\n", csaAscii.alTI[0] * (1.0/1000000.0)); //usec->sec + json_FloatNotNan(fp, "\t\"InversionTime\": %g,\n", csaAscii.alTI[2] * (1.0/1000000.0)); //usec -> sec + //json_FloatNotNan(fp, "\t\"SaturationStopTime\": %g,\n", csaAscii.alTI[2] * (1.0/1000.0)); + } + //PASL http://www.pubmed.com/11746944 http://www.pubmed.com/21606572 + if (strstr(pulseSequenceDetails,"ep2d_fairest")) { + json_FloatNotNan(fp, "\t\"PostInversionDelay\": %g,\n", csaAscii.adFree[2] * (1.0/1000.0)); //usec->sec + json_FloatNotNan(fp, "\t\"PostLabelDelay\": %g,\n", csaAscii.adFree[4] * (1.0/1000.0)); //usec -> sec } //ASL specific tags - Oxford (Thomas OKell) bool isOxfordASL = false; if (strstr(pulseSequenceDetails,"to_ep2d_VEPCASL")) { //Oxford 2D pCASL isOxfordASL = true; - json_Float(fp, "\t\"TagRFFlipAngle\": %g,\n", sWipMemBlock.alFree[4]); - json_Float(fp, "\t\"TagRFDuration\": %g,\n", sWipMemBlock.alFree[5]/1000000.0); //usec -> sec - json_Float(fp, "\t\"TagRFSeparation\": %g,\n", sWipMemBlock.alFree[6]/1000000.0); //usec -> sec - json_FloatNotNan(fp, "\t\"MeanTagGradient\": %g,\n", sWipMemBlock.adFree[0]); //mTm - json_FloatNotNan(fp, "\t\"TagGradientAmplitude\": %g,\n", sWipMemBlock.adFree[1]); //mTm - json_Float(fp, "\t\"TagDuration\": %g,\n", sWipMemBlock.alFree[9]/ 1000.0); //ms -> sec - json_Float(fp, "\t\"MaximumT1Opt\": %g,\n", sWipMemBlock.alFree[10]/ 1000.0); //ms -> sec + json_FloatNotNan(fp, "\t\"InversionTime\": %g,\n", csaAscii.alTI[2] * (1.0/1000000.0)); //ms->sec + json_FloatNotNan(fp, "\t\"BolusDuration\": %g,\n", csaAscii.alTI[0] * (1.0/1000000.0)); //usec -> sec + //alTI[0] = 700000 + //alTI[2] = 1800000 + + json_Float(fp, "\t\"TagRFFlipAngle\": %g,\n", csaAscii.alFree[4]); + json_Float(fp, "\t\"TagRFDuration\": %g,\n", csaAscii.alFree[5]/1000000.0); //usec -> sec + json_Float(fp, "\t\"TagRFSeparation\": %g,\n", csaAscii.alFree[6]/1000000.0); //usec -> sec + json_FloatNotNan(fp, "\t\"MeanTagGradient\": %g,\n", csaAscii.adFree[0]); //mTm + json_FloatNotNan(fp, "\t\"TagGradientAmplitude\": %g,\n", csaAscii.adFree[1]); //mTm + json_Float(fp, "\t\"TagDuration\": %g,\n", csaAscii.alFree[9]/ 1000.0); //ms -> sec + json_Float(fp, "\t\"MaximumT1Opt\": %g,\n", csaAscii.alFree[10]/ 1000.0); //ms -> sec + //report post label delay + + int nPLD = 0; bool isValid = true; //detect gaps in PLD array: If user sets PLD1=250, PLD2=0 PLD3=375 only PLD1 was acquired + for (int k = 11; k < 31; k++) { + if ((isnan(csaAscii.alFree[k])) || (csaAscii.alFree[k] <= 0.0)) isValid = false; + if (isValid) nPLD ++; + } //for k + if (nPLD > 0) { // record PostLabelDelays, these are listed as "PLD0","PLD1",etc in PDF + fprintf(fp, "\t\"InitialPostLabelDelay\": [\n"); //https://docs.google.com/document/d/15tnn5F10KpgHypaQJNNGiNKsni9035GtDqJzWqkkP6c/edit# + for (int i = 0; i < nPLD; i++) { + if (i != 0) + fprintf(fp, ",\n"); + fprintf(fp, "\t\t%g", csaAscii.alFree[i+11]/ 1000.0); //ms -> sec + } + fprintf(fp, "\t],\n"); + } + /*isValid = true; //detect gaps in PLD array: If user sets PLD1=250, PLD2=0 PLD3=375 only PLD1 was acquired for (int k = 11; k < 31; k++) { if (isValid) { char newstr[256]; sprintf(newstr, "\t\"PLD%d\": %%g,\n", k-11); - json_Float(fp, newstr, sWipMemBlock.alFree[k]/ 1000.0); //ms -> sec - if (sWipMemBlock.alFree[k] <= 0.0) isValid = false; + json_Float(fp, newstr, csaAscii.alFree[k]/ 1000.0); //ms -> sec + if (csaAscii.alFree[k] <= 0.0) isValid = false; }//isValid - } //for k + } //for k */ for (int k = 3; k < 11; k++) { //vessel locations char newstr[256]; - sprintf(newstr, "\t\"sWipMemBlockAdFree%d\": %%g,\n", k); - json_FloatNotNan(fp, newstr, sWipMemBlock.adFree[k]); + sprintf(newstr, "\t\"sWipMemBlock.AdFree%d\": %%g,\n", k); + json_FloatNotNan(fp, newstr, csaAscii.adFree[k]); } } if (strstr(pulseSequenceDetails,"jw_tgse_VEPCASL")) { //Oxford 3D pCASL isOxfordASL = true; - json_Float(fp, "\t\"TagRFFlipAngle\": %g,\n", sWipMemBlock.alFree[6]); - json_Float(fp, "\t\"TagRFDuration\": %g,\n", sWipMemBlock.alFree[7]/1000000.0); //usec -> sec - json_Float(fp, "\t\"TagRFSeparation\": %g,\n", sWipMemBlock.alFree[8]/1000000.0); //usec -> sec - json_Float(fp, "\t\"MaximumT1Opt\": %g,\n", sWipMemBlock.alFree[9]/1000.0); //ms -> sec - json_Float(fp, "\t\"Tag0\": %g,\n", sWipMemBlock.alFree[10]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"Tag1\": %g,\n", sWipMemBlock.alFree[11]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"Tag2\": %g,\n", sWipMemBlock.alFree[12]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"Tag3\": %g,\n", sWipMemBlock.alFree[13]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD0\": %g,\n", sWipMemBlock.alFree[30]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD1\": %g,\n", sWipMemBlock.alFree[31]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD2\": %g,\n", sWipMemBlock.alFree[32]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD3\": %g,\n", sWipMemBlock.alFree[33]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD4\": %g,\n", sWipMemBlock.alFree[34]/1000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"PLD5\": %g,\n", sWipMemBlock.alFree[35]/1000.0); //DelayTimeInTR usec -> sec + json_Float(fp, "\t\"TagRFFlipAngle\": %g,\n", csaAscii.alFree[6]); + json_Float(fp, "\t\"TagRFDuration\": %g,\n", csaAscii.alFree[7]/1000000.0); //usec -> sec + json_Float(fp, "\t\"TagRFSeparation\": %g,\n", csaAscii.alFree[8]/1000000.0); //usec -> sec + json_Float(fp, "\t\"MaximumT1Opt\": %g,\n", csaAscii.alFree[9]/1000.0); //ms -> sec + json_Float(fp, "\t\"Tag0\": %g,\n", csaAscii.alFree[10]/1000.0); //DelayTimeInTR usec -> sec + json_Float(fp, "\t\"Tag1\": %g,\n", csaAscii.alFree[11]/1000.0); //DelayTimeInTR usec -> sec + json_Float(fp, "\t\"Tag2\": %g,\n", csaAscii.alFree[12]/1000.0); //DelayTimeInTR usec -> sec + json_Float(fp, "\t\"Tag3\": %g,\n", csaAscii.alFree[13]/1000.0); //DelayTimeInTR usec -> sec + + int nPLD = 0; + bool isValid = true; //detect gaps in PLD array: If user sets PLD1=250, PLD2=0 PLD3=375 only PLD1 was acquired + for (int k = 30; k < 38; k++) { + if ((isnan(csaAscii.alFree[k])) || (csaAscii.alFree[k] <= 0.0)) isValid = false; + if (isValid) nPLD ++; + } //for k + if (nPLD > 0) { // record PostLabelDelays, these are listed as "PLD0","PLD1",etc in PDF + fprintf(fp, "\t\"InitialPostLabelDelay\": [\n"); //https://docs.google.com/document/d/15tnn5F10KpgHypaQJNNGiNKsni9035GtDqJzWqkkP6c/edit# + for (int i = 0; i < nPLD; i++) { + if (i != 0) + fprintf(fp, ",\n"); + fprintf(fp, "\t\t%g", csaAscii.alFree[i+30]/ 1000.0); //ms -> sec + } + fprintf(fp, "\t],\n"); + } + /* + json_Float(fp, "\t\"PLD0\": %g,\n", csaAscii.alFree[30]/1000.0); + json_Float(fp, "\t\"PLD1\": %g,\n", csaAscii.alFree[31]/1000.0); + json_Float(fp, "\t\"PLD2\": %g,\n", csaAscii.alFree[32]/1000.0); + json_Float(fp, "\t\"PLD3\": %g,\n", csaAscii.alFree[33]/1000.0); + json_Float(fp, "\t\"PLD4\": %g,\n", csaAscii.alFree[34]/1000.0); + json_Float(fp, "\t\"PLD5\": %g,\n", csaAscii.alFree[35]/1000.0); + */ } if (isOxfordASL) { //properties common to 2D and 3D ASL //labelling plane - fprintf(fp, "\t\"TagPlaneDThickness\": %g,\n", sWipMemBlock.dThickness); - fprintf(fp, "\t\"TagPlaneUlShape\": %g,\n", sWipMemBlock.ulShape); - fprintf(fp, "\t\"TagPlaneSPositionDTra\": %g,\n", sWipMemBlock.sPositionDTra); - fprintf(fp, "\t\"TagPlaneSNormalDTra\": %g,\n", sWipMemBlock.sNormalDTra); + fprintf(fp, "\t\"TagPlaneDThickness\": %g,\n", csaAscii.dThickness); + fprintf(fp, "\t\"TagPlaneUlShape\": %g,\n", csaAscii.ulShape); + fprintf(fp, "\t\"TagPlaneSPositionDTra\": %g,\n", csaAscii.sPositionDTra); + fprintf(fp, "\t\"TagPlaneSNormalDTra\": %g,\n", csaAscii.sNormalDTra); } //general properties - if (partialFourier > 0) { + if (csaAscii.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 - if (partialFourier == 2) pf = 0.625; // 5/8 - if (partialFourier == 4) pf = 0.75; - if (partialFourier == 8) pf = 0.875; + if (csaAscii.partialFourier == 1) pf = 0.5; // 4/8 + if (csaAscii.partialFourier == 2) pf = 0.625; // 5/8 + if (csaAscii.partialFourier == 4) pf = 0.75; + if (csaAscii.partialFourier == 8) pf = 0.875; fprintf(fp, "\t\"PartialFourier\": %g,\n", pf); } - if (interpInt > 0) { + if (csaAscii.interp > 0) { interp = true; fprintf(fp, "\t\"Interpolation2D\": %d,\n", interp); } - if (baseResolution > 0) fprintf(fp, "\t\"BaseResolution\": %d,\n", baseResolution ); + if (csaAscii.baseResolution > 0) fprintf(fp, "\t\"BaseResolution\": %d,\n", csaAscii.baseResolution ); if (shimSetting[0] != 0.0) { fprintf(fp, "\t\"ShimSetting\": [\n"); for (int i = 0; i < 8; i++) { @@ -1202,17 +1311,17 @@ tse3d: T2*/ fprintf(fp, "\t],\n"); } if (d.CSA.numDti > 0) { // - if (difBipolar == 1) fprintf(fp, "\t\"DiffusionScheme\": \"Bipolar\",\n" ); - if (difBipolar == 2) fprintf(fp, "\t\"DiffusionScheme\": \"Monopolar\",\n" ); + if (csaAscii.difBipolar == 1) fprintf(fp, "\t\"DiffusionScheme\": \"Bipolar\",\n" ); + if (csaAscii.difBipolar == 2) fprintf(fp, "\t\"DiffusionScheme\": \"Monopolar\",\n" ); } //DelayTimeInTR // https://groups.google.com/forum/#!topic/bids-discussion/nmg1BOVH1SU // https://groups.google.com/forum/#!topic/bids-discussion/seD7AtJfaFE json_Float(fp, "\t\"DelayTime\": %g,\n", delayTimeInTR/ 1000000.0); //DelayTimeInTR usec -> sec - json_Float(fp, "\t\"TxRefAmp\": %g,\n", txRefAmp); - json_Float(fp, "\t\"PhaseResolution\": %g,\n", phaseResolution); - json_Float(fp, "\t\"PhaseOversampling\": %g,\n", phaseOversampling); //usec -> sec - json_Float(fp, "\t\"VendorReportedEchoSpacing\": %g,\n", echoSpacing / 1000000.0); //usec -> sec + json_Float(fp, "\t\"TxRefAmp\": %g,\n", csaAscii.txRefAmp); + json_Float(fp, "\t\"PhaseResolution\": %g,\n", csaAscii.phaseResolution); + json_Float(fp, "\t\"PhaseOversampling\": %g,\n", phaseOversampling); + json_Float(fp, "\t\"VendorReportedEchoSpacing\": %g,\n", csaAscii.echoSpacing / 1000000.0); //usec -> sec //ETD and epiFactor not useful/reliable https://github.com/rordenlab/dcm2niix/issues/127 //if (echoTrainDuration > 0) fprintf(fp, "\t\"EchoTrainDuration\": %g,\n", echoTrainDuration / 1000000.0); //usec -> sec //if (epiFactor > 0) fprintf(fp, "\t\"EPIFactor\": %d,\n", epiFactor); @@ -1226,16 +1335,16 @@ tse3d: T2*/ 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); - if (refLinesPE > 0) - fprintf(fp, "\t\"RefLinesPE\": %d,\n", refLinesPE); + if (csaAscii.refLinesPE > 0) + fprintf(fp, "\t\"RefLinesPE\": %d,\n", csaAscii.refLinesPE); json_Str(fp, "\t\"ConsistencyInfo\": \"%s\",\n", consistencyInfo); - if (parallelReductionFactorInPlane > 0) {//AccelFactorPE -> phase encoding + if (csaAscii.parallelReductionFactorInPlane > 0) {//AccelFactorPE -> phase encoding if (d.accelFactPE < 1.0) { //value not found in DICOM header, but WAS found in CSA ascii - d.accelFactPE = parallelReductionFactorInPlane; //value found in ASCII but not in DICOM (0051,1011) + d.accelFactPE = csaAscii.parallelReductionFactorInPlane; //value found in ASCII but not in DICOM (0051,1011) //fprintf(fp, "\t\"ParallelReductionFactorInPlane\": %g,\n", d.accelFactPE); } - 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); + if (csaAscii.parallelReductionFactorInPlane != (int)(d.accelFactPE)) + printWarning("ParallelReductionFactorInPlane reported in DICOM [0051,1011] (%d) does not match CSA series value %d\n", (int)(d.accelFactPE), csaAscii.parallelReductionFactorInPlane); } } else { //e.g. Siemens Vida does not have CSA header, but has many attributes json_Str(fp, "\t\"ReceiveCoilActiveElements\": \"%s\",\n", d.coilElements); @@ -1267,6 +1376,7 @@ tse3d: T2*/ // We'll need this for generating a value for effectiveEchoSpacing that is consistent // with the *reconstructed* data. int reconMatrixPE = d.phaseEncodingLines; + if ((h->dim[2] > 0) && (h->dim[1] > 0)) { if (h->dim[1] == h->dim[2]) //phase encoding does not matter reconMatrixPE = h->dim[2]; @@ -1280,13 +1390,17 @@ tse3d: T2*/ if (bandwidthPerPixelPhaseEncode == 0.0) bandwidthPerPixelPhaseEncode = d.CSA.bandwidthPerPixelPhaseEncode; json_Float(fp, "\t\"BandwidthPerPixelPhaseEncode\": %g,\n", bandwidthPerPixelPhaseEncode ); - if ((!d.is3DAcq) && (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); + if (d.accelFactPE > 1.0) fprintf(fp, "\t\"ParallelReductionFactorInPlane\": %g,\n", d.accelFactPE); //https://github.com/rordenlab/dcm2niix/issues/314 //EffectiveEchoSpacing // Siemens bandwidthPerPixelPhaseEncode already accounts for the effects of parallel imaging, // interpolation, phaseOversampling, and phaseResolution, in the context of the size of the // *reconstructed* data in the PE dimension double effectiveEchoSpacing = 0.0; - if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode > 0.0)) + //next: dicm2nii's method for determining effectiveEchoSpacing if bandwidthPerPixelPhaseEncode is unknown, see issue 315 + //if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode <= 0.0) && (d.CSA.sliceMeasurementDuration >= 0)) + // effectiveEchoSpacing = d.CSA.sliceMeasurementDuration / (reconMatrixPE * 1000.0); + if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode > 0.0)) effectiveEchoSpacing = 1.0 / (bandwidthPerPixelPhaseEncode * reconMatrixPE); if (d.effectiveEchoSpacingGE > 0.0) effectiveEchoSpacing = d.effectiveEchoSpacingGE / 1000000.0; @@ -1301,7 +1415,6 @@ tse3d: T2*/ derivedEchoSpacing = bandwidthPerPixelPhaseEncode * trueESfactor * reconMatrixPE; if (derivedEchoSpacing != 0) derivedEchoSpacing = 1/derivedEchoSpacing; json_Float(fp, "\t\"DerivedVendorReportedEchoSpacing\": %g,\n", derivedEchoSpacing); - //TotalReadOutTime: Really should be called "EffectiveReadOutTime", by analogy with "EffectiveEchoSpacing". // But BIDS spec calls it "TotalReadOutTime". // So, we DO NOT USE EchoTrainLength, because not trying to compute the actual (physical) readout time. @@ -1356,36 +1469,12 @@ tse3d: T2*/ } //only save PhaseEncodingDirection if BOTH direction and POLARITY are known //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"); - 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.isXA10A) && (!d.is3DAcq) && (d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.sliceTiming[0] >= 0.0)) { + if ((!d.is3DAcq) && (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);" - for (int i = (h->dim[3]-1); i >= 0; i--) { - if (d.CSA.sliceTiming[i] < 0.0) break; - 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 < h->dim[3]; i++) { - if (i != 0) - fprintf(fp, ",\n"); - fprintf(fp, "\t\t%g", d.CSA.sliceTiming[i] / 1000.0 ); - } + 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 ); } fprintf(fp, "\t],\n"); } @@ -1535,8 +1624,10 @@ int * nii_saveDTI(char pathoutname[],int nConvert, struct TDCMsort dcmSort[],str free(vx); return NULL; } - for (int i = 0; i < numDti; i++) + if (opts.isVerbose) { + for (int i = 0; i < numDti; i++) printMessage("bxyz %g %g %g %g\n",vx[i].V[0],vx[i].V[1],vx[i].V[2],vx[i].V[3]); + } //Stutters XINAPSE7 seem to save B=0 as B=2000, but these are not derived? https://github.com/rordenlab/dcm2niix/issues/182 bool bZeroBvec = false; for (int i = 0; i < numDti; i++) {//check if all bvalues match first volume @@ -1677,8 +1768,8 @@ int * nii_saveDTI(char pathoutname[],int nConvert, struct TDCMsort dcmSort[],str if (!isSequential) printMessage("DTI volumes re-ordered by ascending b-value\n"); dcmList[indx0].CSA.numDti = numDti; //warning structure not changed outside scope! - geCorrectBvecs(&dcmList[indx0],sliceDir, vx); - siemensPhilipsCorrectBvecs(&dcmList[indx0],sliceDir, vx); + geCorrectBvecs(&dcmList[indx0],sliceDir, vx, opts.isVerbose); + siemensPhilipsCorrectBvecs(&dcmList[indx0],sliceDir, vx, opts.isVerbose); if (!opts.isFlipY ) { //!FLIP_Y&& (dcmList[indx0].CSA.mosaicSlices < 2) mosaics are always flipped in the Y direction for (int i = 0; i < (numDti); i++) { if (fabs(vx[i].V[2]) > FLT_EPSILON) @@ -1709,7 +1800,6 @@ int * nii_saveDTI(char pathoutname[],int nConvert, struct TDCMsort dcmSort[],str #else if (opts.isSaveNRRD) { if (numDti < kMaxDTI4D) { - //dcmList[indx0].CSA.numDti dcmList[indx0].CSA.numDti = numDti; for (int i = 0; i < numDti; i++) //for each direction for (int v = 0; v < 4; v++) //for each vector+B-value @@ -1864,7 +1954,7 @@ float intersliceDistance(struct TDICOMdata d1, struct TDICOMdata d2) { sqr(d1.patientPosition[3]-d2.patientPosition[3])); } //intersliceDistance() -#define myInstanceNumberOrderIsNotSpatial +//#define myInstanceNumberOrderIsNotSpatial //instance number is virtually always ordered based on spatial position. // interleaved/multi-band conversion will be disrupted if instance number refers to temporal order // these functions reorder images based on spatial position @@ -1875,7 +1965,7 @@ float intersliceDistance(struct TDICOMdata d1, struct TDICOMdata d2) { // as such images will probably disrupt most tools. // This option is only to salvage borked data. // This code has also not been tested on data stored in TXYZ rather than XYZT order -#ifdef myInstanceNumberOrderIsNotSpatial +//#ifdef myInstanceNumberOrderIsNotSpatial float intersliceDistanceSigned(struct TDICOMdata d1, struct TDICOMdata d2) { //reports distance between two slices, signed as 2nd slice can be in front or behind 1st @@ -1945,7 +2035,7 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s free(dcmSortIn); return false; } // ensureSequentialSlicePositions() -#endif //myInstanceNumberOrderIsNotSpatial +//#endif //myInstanceNumberOrderIsNotSpatial void swapDim3Dim4(int d3, int d4, struct TDCMsort dcmSort[]) { //swap space and time: input A0,A1...An,B0,B1...Bn output A0,B0,A1,B1,... @@ -2288,8 +2378,6 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt if (isDcmExt) strcat (outname,".dcm"); 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 // https://github.com/rordenlab/dcm2niix/issues/237 @@ -2322,11 +2410,16 @@ int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopt strcat (baseoutname,pth); char appendChar[2] = {"a"}; appendChar[0] = kPathSeparator; - if ((pth[strlen(pth)-1] != kPathSeparator) && (outname[0] != kPathSeparator)) + if ((strlen(pth) > 0) && (pth[strlen(pth)-1] != kPathSeparator) && (outname[0] != kPathSeparator)) strcat (baseoutname,appendChar); //Allow user to specify new folders, e.g. "-f dir/%p" or "-f %s/%p/%m" // These folders are created if they do not exist char *sep = strchr(outname, kPathSeparator); +#if defined(USING_R) && (defined(_WIN64) || defined(_WIN32)) + // R also uses forward slash on Windows, so allow it here + if (!sep) + sep = strchr(outname, kForeignPathSeparator); +#endif if (sep) { char newdir[2048] = {""}; strcat (newdir,baseoutname); @@ -2529,12 +2622,13 @@ void writeNiiGz (char * baseName, struct nifti_1_header hdr, unsigned char* src int nii_saveNII (char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d) { hdr.vox_offset = 352; + // Extract the basename from the full file path - // R always uses '/' as the path separator, so this should work on all platforms char *start = niiFilename + strlen(niiFilename); - while (*start != '/') + while (start >= niiFilename && *start != '/' && *start != kPathSeparator) start--; std::string name(++start); + nifti_image *image = nifti_convert_nhdr2nim(hdr, niiFilename); if (image == NULL) return EXIT_FAILURE; @@ -2547,9 +2641,26 @@ int nii_saveNII (char *niiFilename, struct nifti_1_header hdr, unsigned char *im return EXIT_SUCCESS; } -void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, struct TDCMopts &opts) +void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, struct TDCMopts &opts, const char *filename) { ImageList *images = (ImageList *) opts.imageList; + switch (data.modality) { + case kMODALITY_CR: + images->addAttribute("modality", "CR"); + break; + case kMODALITY_CT: + images->addAttribute("modality", "CT"); + break; + case kMODALITY_MR: + images->addAttribute("modality", "MR"); + break; + case kMODALITY_PT: + images->addAttribute("modality", "PT"); + break; + case kMODALITY_US: + images->addAttribute("modality", "US"); + break; + } switch (data.manufacturer) { case kMANUFACTURER_SIEMENS: images->addAttribute("manufacturer", "Siemens"); @@ -2568,6 +2679,14 @@ void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, images->addAttribute("scannerModelName", data.manufacturersModelName); if (strlen(data.imageType) > 0) images->addAttribute("imageType", data.imageType); + if (data.seriesNum > 0) + images->addAttribute("seriesNumber", int(data.seriesNum)); + if (strlen(data.seriesDescription) > 0) + images->addAttribute("seriesDescription", data.seriesDescription); + if (strlen(data.sequenceName) > 0) + images->addAttribute("sequenceName", data.sequenceName); + if (strlen(data.protocolName) > 0) + images->addAttribute("protocolName", data.protocolName); if (strlen(data.studyDate) >= 8 && strcmp(data.studyDate,"00000000") != 0) images->addDateAttribute("studyDate", data.studyDate); if (strlen(data.studyTime) > 0 && strncmp(data.studyTime,"000000",6) != 0) @@ -2580,18 +2699,94 @@ void nii_saveAttributes (struct TDICOMdata &data, struct nifti_1_header &header, images->addAttribute("echoTime", data.TE); if (data.TR > 0.0) images->addAttribute("repetitionTime", data.TR); - if ((data.CSA.bandwidthPerPixelPhaseEncode > 0.0) && (header.dim[2] > 0) && (header.dim[1] > 0)) { - if (data.phaseEncodingRC =='C') - images->addAttribute("dwellTime", 1.0/data.CSA.bandwidthPerPixelPhaseEncode/header.dim[2]); - else if (data.phaseEncodingRC == 'R') - images->addAttribute("dwellTime", 1.0/data.CSA.bandwidthPerPixelPhaseEncode/header.dim[1]); - } - if (data.phaseEncodingRC == 'C') - images->addAttribute("phaseEncodingDirection", "j"); - else if (data.phaseEncodingRC == 'R') - images->addAttribute("phaseEncodingDirection", "i"); - if (data.CSA.phaseEncodingDirectionPositive != -1) - images->addAttribute("phaseEncodingSign", data.CSA.phaseEncodingDirectionPositive == 0 ? -1 : 1); + if (data.TI > 0.0) + images->addAttribute("inversionTime", data.TI); + if (!data.isXRay) { + if (data.zThick > 0.0) + images->addAttribute("sliceThickness", data.zThick); + if (data.zSpacing > 0.0) + images->addAttribute("sliceSpacing", data.zSpacing); + } + if (data.CSA.multiBandFactor > 1) + images->addAttribute("multibandFactor", data.CSA.multiBandFactor); + if (data.phaseEncodingSteps > 0) + images->addAttribute("phaseEncodingSteps", data.phaseEncodingSteps); + if (data.phaseEncodingLines > 0) + images->addAttribute("phaseEncodingLines", data.phaseEncodingLines); + + // Calculations relating to the reconstruction in the phase encode direction, + // which are needed to derive effective echo spacing and readout time below. + // See the nii_SaveBIDS() function for details + int reconMatrixPE = data.phaseEncodingLines; + if ((header.dim[2] > 0) && (header.dim[1] > 0)) { + if (header.dim[2] == header.dim[2]) //phase encoding does not matter + reconMatrixPE = header.dim[2]; + else if (data.phaseEncodingRC =='R') + reconMatrixPE = header.dim[2]; + else if (data.phaseEncodingRC =='C') + reconMatrixPE = header.dim[1]; + } + + double bandwidthPerPixelPhaseEncode = data.bandwidthPerPixelPhaseEncode; + if (bandwidthPerPixelPhaseEncode == 0.0) + bandwidthPerPixelPhaseEncode = data.CSA.bandwidthPerPixelPhaseEncode; + double effectiveEchoSpacing = 0.0; + if ((reconMatrixPE > 0) && (bandwidthPerPixelPhaseEncode > 0.0)) + effectiveEchoSpacing = 1.0 / (bandwidthPerPixelPhaseEncode * reconMatrixPE); + if (data.effectiveEchoSpacingGE > 0.0) + effectiveEchoSpacing = data.effectiveEchoSpacingGE / 1000000.0; + + if (effectiveEchoSpacing > 0.0) + images->addAttribute("effectiveEchoSpacing", effectiveEchoSpacing); + if ((reconMatrixPE > 0) && (effectiveEchoSpacing > 0.0)) + images->addAttribute("effectiveReadoutTime", effectiveEchoSpacing * (reconMatrixPE - 1.0)); + if (data.pixelBandwidth > 0.0) + images->addAttribute("pixelBandwidth", data.pixelBandwidth); + if ((data.manufacturer == kMANUFACTURER_SIEMENS) && (data.dwellTime > 0)) + images->addAttribute("dwellTime", data.dwellTime * 1e-9); + + // Phase encoding polarity + // We only save these attributes if both direction and polarity are known + if (((data.phaseEncodingRC == 'R') || (data.phaseEncodingRC == 'C')) && (!data.is3DAcq) && ((data.CSA.phaseEncodingDirectionPositive == 1) || (data.CSA.phaseEncodingDirectionPositive == 0))) { + if (data.phaseEncodingRC == 'C') { + images->addAttribute("phaseEncodingDirection", "j"); + // Notice the XOR (^): the sense of phaseEncodingDirectionPositive + // is reversed if we are flipping the y-axis + images->addAttribute("phaseEncodingSign", ((data.CSA.phaseEncodingDirectionPositive == 0) ^ opts.isFlipY) ? -1 : 1); + } + else if (data.phaseEncodingRC == 'R') { + images->addAttribute("phaseEncodingDirection", "i"); + images->addAttribute("phaseEncodingSign", data.CSA.phaseEncodingDirectionPositive == 0 ? -1 : 1); + } + } + // Slice timing + if (data.CSA.sliceTiming[0] >= 0.0 && (data.manufacturer == kMANUFACTURER_UIH || data.manufacturer == kMANUFACTURER_GE || (data.manufacturer == kMANUFACTURER_SIEMENS && !data.isXA10A))) { + std::vector sliceTimes; + #pragma message ("Please test R specific code: at this stage all slice times should be in msec due to changes in checkSliceTiming() 20190704") + for (int i=header.dim[3]-1; i>=0; i--) { + if (data.CSA.sliceTiming[i] < 0.0) + break; + sliceTimes.push_back(data.CSA.sliceTiming[i]); //slice time in msec + } + images->addAttribute("sliceTiming", sliceTimes); + } + + if (strlen(data.patientID) > 0) + images->addAttribute("patientIdentifier", data.patientID); + if (strlen(data.patientName) > 0) + images->addAttribute("patientName", data.patientName); + if (strlen(data.patientBirthDate) >= 8 && strcmp(data.patientBirthDate,"00000000") != 0) + images->addDateAttribute("patientBirthDate", data.patientBirthDate); + if (strlen(data.patientAge) > 0 && strcmp(data.patientAge,"000Y") != 0) + images->addAttribute("patientAge", data.patientAge); + if (data.patientSex == 'F') + images->addAttribute("patientSex", "F"); + else if (data.patientSex == 'M') + images->addAttribute("patientSex", "M"); + if (data.patientWeight > 0.0) + images->addAttribute("patientWeight", data.patientWeight); + if (strlen(data.imageComments) > 0) + images->addAttribute("comments", data.imageComments); } #else @@ -2650,7 +2845,7 @@ int nii_saveNRRD(char * niiFilename, struct nifti_1_header hdr, unsigned char* i if (nDim < 1) return EXIT_FAILURE; bool isGz = opts.isGz; size_t imgsz = nii_ImgBytes(hdr); - if ((isGz) && (imgsz >= 2147483647)) { //use internal compressor + if ((isGz) && (imgsz >= 2147483647)) { printWarning("Saving huge image uncompressed (many GZip tools have 2 Gb limit).\n"); isGz = false; } @@ -2840,9 +3035,21 @@ int nii_saveNRRD(char * niiFilename, struct nifti_1_header hdr, unsigned char* i fprintf(fp,"DWMRI_b-value:=%g\n", b_max); //gradient tag, e.g. DWMRI_gradient_0000:=0.0 0.0 0.0 for (int i = 0; i < numDTI; i++) { - float factor = 0; + float factor = 0.0; if (b_max > 0) factor = sqrt(dti4D->S[i].V[0]/b_max); - fprintf(fp,"DWMRI_gradient_%04d:=%.17g %.17g %.17g\n", i, factor*dti4D->S[i].V[1], factor*dti4D->S[i].V[2], factor*dti4D->S[i].V[3]); + if ( (dti4D->S[i].V[0] > 50.0) && (isSameFloatGE(0.0, dti4D->S[i].V[1])) && (isSameFloatGE(0.0, dti4D->S[i].V[2])) && (isSameFloatGE(0.0, dti4D->S[i].V[3])) ) { + //On May 2, 2019, at 10:47 AM, Gordon L. Kindlmann <> wrote: + //(assuming b_max 2000, we write "isotropic" for the b=2000 isotropic image, and specify the b-value if it is an isotropic image but not b-bax + // DWMRI_gradient_0003:=isotropic b=1000 + // DWMRI_gradient_0004:=isotropic + if (isSameFloatGE(b_max, dti4D->S[i].V[0])) + fprintf(fp,"DWMRI_gradient_%04d:=isotropic\n", i); + else + fprintf(fp,"DWMRI_gradient_%04d:=isotropic b=%g\n", i, dti4D->S[i].V[0]); + } else + fprintf(fp,"DWMRI_gradient_%04d:=%.17g %.17g %.17g\n", i, factor*dti4D->S[i].V[1], factor*dti4D->S[i].V[2], factor*dti4D->S[i].V[3]); + //printf("%g = %g %g %g>>>>\n",dti4D->S[i].V[0], dti4D->S[i].V[1],dti4D->S[i].V[2],dti4D->S[i].V[3]); + } } fprintf(fp,"\n"); //blank line: end of NRRD header @@ -2904,7 +3111,7 @@ int nii_saveNII(char * niiFilename, struct nifti_1_header hdr, unsigned char* im strcat (fname,".nii"); #if defined(_WIN64) || defined(_WIN32) if ((opts.isGz) && (opts.isPipedGz)) - printWarning(" The 'optimal' piped gz is only available for Unix\n"); + printWarning("The 'optimal' piped gz is only available for Unix\n"); #else //if windows else Unix if ((opts.isGz) && (opts.isPipedGz) && (strlen(opts.pigzname) > 0) ) { //piped gz @@ -2921,7 +3128,8 @@ int nii_saveNII(char * niiFilename, struct nifti_1_header hdr, unsigned char* im strcat(command, fname); strcat(command, ".gz\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"' //strcat(command, "x.gz\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"' - printMessage("Compress: %s\n",command); + if (opts.isVerbose) + printMessage("Compress: %s\n",command); FILE *pigzPipe; if (( pigzPipe = popen(command, "w")) == NULL) { printError("Unable to open pigz pipe\n"); @@ -3893,8 +4101,9 @@ void checkDateTimeOrder(struct TDICOMdata * d, struct TDICOMdata * d1) { printWarning("Images sorted by instance number [0020,0013](%d..%d), but AcquisitionTime [0008,0032] suggests a different order (%g..%g) \n", d->imageNum,d1->imageNum, d->acquisitionTime,d1->acquisitionTime); } -void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { +void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1, int verbose) { //detect images with slice timing errors. https://github.com/rordenlab/dcm2niix/issues/126 +//modified 20190704: this function now ensures all slice times are in msec 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)) @@ -3902,7 +4111,7 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { 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 + if (isSliceTimeHHMMSS) {//handle midnight crossing for (int i = 0; i < nSlices; i++) d->CSA.sliceTiming[i] = dicomTimeToSec(d->CSA.sliceTiming[i]); float minT = d->CSA.sliceTiming[0]; @@ -3911,6 +4120,7 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { if (d->CSA.sliceTiming[i] < minT) minT = d->CSA.sliceTiming[i]; if (d->CSA.sliceTiming[i] < maxT) maxT = d->CSA.sliceTiming[i]; } + //printf("%d %g ---> %g..%g\n", nSlices, d->TR, minT, maxT); float kMidnightSec = 86400; float kNoonSec = 43200; if ((maxT - minT) > kNoonSec) { //volume started before midnight but ended next day! @@ -3921,7 +4131,6 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { 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; @@ -3933,10 +4142,15 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { 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 + if (isSliceTimeHHMMSS) //convert HHMMSS to msec 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 + d->CSA.sliceTiming[i] = dicomTimeToSec(d->CSA.sliceTiming[i]) * 1000.0; + float TRms = d->TR; //d->TR in msec! + if ((minT != maxT) && (maxT <= TRms)) { + if (verbose != 0) + printMessage("Slice timing range appears reasonable (range %g..%g, TR=%g ms)\n", minT, maxT, TRms); + 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 if ((strlen(d->seriesDescription) > 0) && (strstr(d->seriesDescription, "SBRef") != NULL)) return; //fine: single-band calibration data, the slice timing WILL exceed the TR @@ -3950,11 +4164,11 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { } if ((minT1 < 0.0) && (d->rtia_timerGE >= 0.0)) return; //use rtia timer 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); + printWarning("Siemens MoCo? Bogus slice timing (range %g..%g, TR=%g seconds)\n", minT1, maxT1, TRms); return; } - if ((minT1 == maxT1) || (maxT1 >= d->TR)) { //both first and second image corrupted - printWarning("Slice timing appears corrupted (range %g..%g, TR=%gms)\n", minT1, maxT1, d->TR); + if ((minT1 == maxT1) || (maxT1 >= TRms)) { //both first and second image corrupted + printWarning("Slice timing appears corrupted (range %g..%g, TR=%g ms)\n", minT1, maxT1, TRms); return; } //1st image corrupted, but 2nd looks ok - substitute values from 2nd image @@ -3963,15 +4177,285 @@ void checkSliceTiming(struct TDICOMdata * d, struct TDICOMdata * d1) { if (d1->CSA.sliceTiming[i] < 0.0) break; } d->CSA.multiBandFactor = d1->CSA.multiBandFactor; - printMessage("CSA slice timing based on 2nd volume, 1st volume corrupted (CMRR bug, range %g..%g, TR=%gms)\n", minT, maxT, d->TR); -}//checkSliceTiming + printMessage("CSA slice timing based on 2nd volume, 1st volume corrupted (CMRR bug, range %g..%g, TR=%g ms)\n", minT, maxT, TRms); +}//checkSliceTiming() + +void sliceTimingXA(struct TDCMsort *dcmSort,struct TDICOMdata *dcmList, struct nifti_1_header * hdr, int verbose, const char * filename, int nConvert) { + //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 + uint64_t indx0 = dcmSort[0].indx; //first volume + if (!dcmList[indx0].isXA10A) return; + if ( (nConvert == (hdr->dim[3]*hdr->dim[4])) && (hdr->dim[3] < (kMaxEPI3D-1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { + //XA11 2D classic + for (int v = 0; v < hdr->dim[3]; v++) + dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0]; + } else if ( (nConvert == (hdr->dim[4])) && (hdr->dim[3] < (kMaxEPI3D-1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { + //XA10 mosaics - these are missing a lot of information + float mn = dcmList[dcmSort[1].indx].CSA.sliceTiming[0]; + //get slice timing from second volume + for (int v = 0; v < hdr->dim[3]; v++) { + dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[1].indx].CSA.sliceTiming[v]; + if (dcmList[indx0].CSA.sliceTiming[v] < mn) mn = dcmList[indx0].CSA.sliceTiming[v]; + } + if (mn < 0.0) mn = 0.0; + int mb = 0; + for (int v = 0; v < hdr->dim[3]; v++) { + dcmList[indx0].CSA.sliceTiming[v] -= mn; + if (isSameFloatGE(dcmList[indx0].CSA.sliceTiming[v], 0.0)) mb ++; + } + if ((dcmList[indx0].CSA.multiBandFactor < 2) && (mb > 1)) + dcmList[indx0].CSA.multiBandFactor = mb; + //for (int v = 0; v < hdr->dim[3]; v++) + // printf("XA10sliceTiming\t%d\t%g\n", v, dcmList[indx0].CSA.sliceTiming[v]); + } +} //sliceTimingXA() + +void rescueSliceTimingGE(struct TDICOMdata * d, int verbose, int nSL, const char * filename) { + //we can often read GE slice timing from TriggerTime (0018,1060) or RTIA Timer (0021,105E) + // if both of these methods fail, we can often guess based on slice order stored in the Private Protocol Data Block (0025,101B) + // this is referred to as "rescue" as we only know the TR, not the TA. So assumes continuous scans with no gap + if (d->is3DAcq) return; //no need for slice times + if (d->CSA.sliceTiming[0] >= 0.0) return; //slice times calculated + if (nSL < 2) return; + if (d->manufacturer != kMANUFACTURER_GE) return; + if ((d->protocolBlockStartGE < 1) || (d->protocolBlockLengthGE < 19)) return; + #ifdef myReadGeProtocolBlock + //GE final desperate attempt to determine slice order + // GE does not provide a good estimate for TA: here we use TR, which will be wrong for sparse designs + // Also, unclear what happens if slice order is flipped + // Therefore, we warning the user that we are guessing + int viewOrderGE = -1; + int sliceOrderGE = -1; + //printWarning("Using GE Protocol Data Block for BIDS data (beware: new feature)\n"); + int ok = geProtocolBlock(filename, d->protocolBlockStartGE, d->protocolBlockLengthGE, verbose, &sliceOrderGE, &viewOrderGE); + if (ok != EXIT_SUCCESS) { + printWarning("Unable to decode GE protocol block\n"); + return; + } + if ((sliceOrderGE < 0) || (sliceOrderGE > 1)) return; + // 0=sequential/1=interleaved + printWarning("Guessing slice times using ProtocolBlock SliceOrder=%d (seq=0, int=1)\n", sliceOrderGE); + int nOdd = nSL / 2; + float secPerSlice = d->TR/nSL; //should be TA not TR! We do not know TR + if (sliceOrderGE == 0) { + for (int i = 0; i < nSL; i++) + d->CSA.sliceTiming[i] = i * secPerSlice; + } else { + for (int i = 0; i < nSL; i++) { + if (i % 2 == 0) { //ODD slices since we index from 0! + d->CSA.sliceTiming[i] = (i/2) * secPerSlice; + //printf("%g\n", d->CSA.sliceTiming[i]); + } else { + d->CSA.sliceTiming[i] = (nOdd+((i+1)/2)) * secPerSlice; + //printf("%g\n", d->CSA.sliceTiming[i]); + } + } //for each slice + } //if interleaved + #endif +} -void reportMat44o(char *str, mat44 A) { +void reverseSliceTiming(struct TDICOMdata * d, int verbose, int nSL) { + if ((d->CSA.protocolSliceNumber1 == 0) || ((d->CSA.protocolSliceNumber1 == 1))) return; //slices not flipped + if (d->is3DAcq) return; //no need for slice times + if (d->CSA.sliceTiming[0] < 0.0) return; //no slice times + if (nSL > kMaxEPI3D) return; + if (nSL < 2) return; + if (verbose) + printMessage("Slices were spatially flipped, so slice times are flipped\n"); + d->CSA.protocolSliceNumber1 = 0; + float sliceTiming[kMaxEPI3D]; + for (int i = 0; i < nSL; i++) + sliceTiming[i] = d->CSA.sliceTiming[i]; + for (int i = 0; i < nSL; i++) + d->CSA.sliceTiming[i] = sliceTiming[(nSL-1)-i]; +} + +void rescueSliceTimingSiemens(struct TDICOMdata * d, int verbose, int nSL, const char * filename) { + if (d->is3DAcq) return; //no need for slice times + if (d->CSA.sliceTiming[0] >= 0.0) return; //slice times calculated + if (nSL < 2) return; + if ((d->manufacturer != kMANUFACTURER_SIEMENS) || (d->CSA.SeriesHeader_offset < 1) || (d->CSA.SeriesHeader_length < 1)) return; + float shimSetting[8]; + char protocolName[kDICOMStrLarge], fmriExternalInfo[kDICOMStrLarge], coilID[kDICOMStrLarge], consistencyInfo[kDICOMStrLarge], coilElements[kDICOMStrLarge], pulseSequenceDetails[kDICOMStrLarge], wipMemBlock[kDICOMStrLarge]; + TCsaAscii csaAscii; + siemensCsaAscii(filename, &csaAscii, d->CSA.SeriesHeader_offset, d->CSA.SeriesHeader_length, shimSetting, coilID, consistencyInfo, coilElements, pulseSequenceDetails, fmriExternalInfo, protocolName, wipMemBlock); + int ucMode = csaAscii.ucMode; + if ((ucMode < 1) || (ucMode == 3) || (ucMode > 4)) return; + float trSec = d->TR / 1000.0; + float delaySec = csaAscii.delayTimeInTR/ 1000000.0; + float taSec = trSec - delaySec; + float sliceTiming[kMaxEPI3D]; + for (int i = 0; i < nSL; i++) + sliceTiming[i] = i * taSec/nSL * 1000.0; //expected in ms + if (ucMode == 1) //asc + for (int i = 0; i < nSL; i++) + d->CSA.sliceTiming[i] = sliceTiming[i]; + if (ucMode == 2) //desc + for (int i = 0; i < nSL; i++) + d->CSA.sliceTiming[i] = sliceTiming[(nSL-1) - i]; + if (ucMode == 4) { //int + int oddInc = 0; //for slices 1,3,5 + int evenInc = (nSL+1) / 2; //for 4 slices 0,1,2,3 we will order [2,0,3,1] for 5 slices [0,3,1,4,2] + if (nSL % 2 == 0) { //Siemens interleaved for acquisitions with odd number of slices https://www.mccauslandcenter.sc.edu/crnl/tools/stc + oddInc = evenInc; + evenInc = 0; + } + for (int i = 0; i < nSL; i++) { + if (i % 2 == 0) {//odd slice 1,3,etc [indexed from 0]! + d->CSA.sliceTiming[i] = sliceTiming[oddInc]; + //printf("%d %d\n", i, oddInc); + oddInc += 1; + } else { //even slice + d->CSA.sliceTiming[i] = sliceTiming[evenInc]; + //printf("%d %d %d\n", i, evenInc, nSL); + evenInc += 1; + } + } + } //if ucMode == 3 int + //dicm2nii provides sSliceArray.ucImageNumb - similar to protocolSliceNumber1 + //if asc_header(s, 'sSliceArray.ucImageNumb'), t = t(nSL:-1:1); end % rev-num +} + +void sliceTimingUIH(struct TDCMsort *dcmSort,struct TDICOMdata *dcmList, struct nifti_1_header * hdr, int verbose, const char * filename, int nConvert) { + uint64_t indx0 = dcmSort[0].indx; //first volume + if (!(dcmList[indx0].manufacturer == kMANUFACTURER_UIH)) return; + if (nConvert != (hdr->dim[3]*hdr->dim[4])) return; + if (hdr->dim[3] > (kMaxEPI3D-1)) return; + if (hdr->dim[4] < 2) return; + for (int v = 0; v < hdr->dim[3]; v++) + dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].acquisitionTime; //nb format is HHMMSS we need to handle midnight-crossing and convert to ms, see checkSliceTiming() +} + +void sliceTimingGE(struct TDCMsort *dcmSort,struct TDICOMdata *dcmList, struct nifti_1_header * hdr, int verbose, const char * filename, int nConvert) { + //GE check slice timing >>> + uint64_t indx0 = dcmSort[0].indx; //first volume + //printf("XXX %d\n", indx0); + if (!(dcmList[indx0].manufacturer == kMANUFACTURER_GE)) return; + bool GEsliceTiming_x0018x1060 = false; + if ((hdr->dim[3] < (kMaxEPI3D-1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { + //GE: 1st method for "epi" PSD + //0018x1060 is defined in msec: http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,1060) + //as of 20190704 dcm2niix expects sliceTiming to be encoded in msec for all vendors + GEsliceTiming_x0018x1060 = true; + for (int v = 0; v < hdr->dim[3]; v++) { + if (dcmList[dcmSort[v].indx].CSA.sliceTiming[0] < 0) + GEsliceTiming_x0018x1060 = false; + dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0]; //ms 20190704 + //dcmList[indx0].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0] / 1000.0; //ms -> sec prior to 20190704 + } + //printMessage(">>>>Reading GE slice timing from 0018,1060\n"); + //0018,1060 provides time at end of acquisition, not start... + if (GEsliceTiming_x0018x1060) { + float minT = dcmList[indx0].CSA.sliceTiming[0]; + float maxT = minT; + for (int v = 0; v < hdr->dim[3]; v++) + if (dcmList[indx0].CSA.sliceTiming[v] < minT) + minT = dcmList[indx0].CSA.sliceTiming[v]; + for (int v = 0; v < hdr->dim[3]; v++) + if (dcmList[indx0].CSA.sliceTiming[v] > maxT) + maxT = dcmList[indx0].CSA.sliceTiming[v]; + for (int v = 0; v < hdr->dim[3]; v++) + dcmList[indx0].CSA.sliceTiming[v] = dcmList[indx0].CSA.sliceTiming[v] - minT; + if (isSameFloatGE(minT, maxT)) { + //ABCD simulated GE DICOMs do not populate 0018,1060 correctly + GEsliceTiming_x0018x1060 = false; + dcmList[indx0].CSA.sliceTiming[0] = -1.0; //no valid slice times + } + } //adjust: first slice is time = 0.0 + } //GE slice timing from 0018,1060 + if ((!GEsliceTiming_x0018x1060) && (hdr->dim[3] < (kMaxEPI3D-1)) && (hdr->dim[3] > 1) && (hdr->dim[4] > 1)) { + //printMessage(">>>>Reading GE slice timing from epiRT (0018,1060 did not work)\n"); + //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 = hdr->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 < hdr->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) { + double scale2Sec = 1.0; + if (dcmList[indx0].TR > 0.0) { //issue 286: determine units for rtia_timerGE + //See https://github.com/rordenlab/dcm2niix/tree/master/GE + // Nikadon's DV24 data stores RTIA Timer as seconds, issue 286 14_LX uses 1/10,000 sec + // The slice timing should always be less than the TR (which is in ms) + // Below we assume 1/10,000 of a sec if slice time is >90% and less than <100% of a TR + // Will not work for sparse designs, but slice timing inappropriate for those datasets + float maxSliceTimeFrac = (maxTime-minTime) / dcmList[indx0].TR; //should be slightly less than 1.0 + if ((maxSliceTimeFrac > 9.0) && (maxSliceTimeFrac < 10)) + scale2Sec = 1.0 / 10000.0; + //printMessage(">> %g %g %g\n", maxSliceTimeFrac, scale2Sec, dcmList[indx0].TR); + } + double scale2ms = scale2Sec * 1000.0; //20190704: convert slice timing values to ms for all vendors + for (int v = 0; v < hdr->dim[3]; v++) + dcmList[indx0].CSA.sliceTiming[v] = (dcmList[dcmSort[v+j].indx].rtia_timerGE - minTime) * scale2ms; + dcmList[indx0].CSA.sliceTiming[hdr->dim[3]] = -1; + //detect multi-band + int nZero = 0; + for (int v = 0; v < hdr->dim[3]; v++) + if (isSameFloatGE(dcmList[indx0].CSA.sliceTiming[hdr->dim[3]], 0.0)) + nZero ++; + if ((nZero > 1) && (nZero < hdr->dim[3]) && ((hdr->dim[3] % nZero) == 0)) + dcmList[indx0].CSA.multiBandFactor = nZero; + //report times + if (verbose > 0) { + printMessage("GE slice timing (sec)\n"); + printMessage("\tTime\tX\tY\tZ\tInstance\n"); + for (int v = 0; v < hdr->dim[3]; v++) { + if (v == (hdr->dim[3]-1)) + printMessage("...\n"); + if ((v < 4) || (v == (hdr->dim[3]-1))) + printMessage("\t%g\t%g\t%g\t%g\t%d\n", dcmList[indx0].CSA.sliceTiming[v] / 1000.0, 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 +} //sliceTimingGE() + +int sliceTimingCore(struct TDCMsort *dcmSort,struct TDICOMdata *dcmList, struct nifti_1_header * hdr, int verbose, const char * filename, int nConvert) { + int sliceDir = 0; + //uint64_t indx0 = dcmSort[0].indx; + //uint64_t indx1 = dcmSort[1].indx; + struct TDICOMdata * d0 = &dcmList[dcmSort[0].indx]; + uint64_t indx1 = dcmSort[1].indx; + if (nConvert < 2) indx1 = dcmSort[0].indx; + struct TDICOMdata * d1 = &dcmList[indx1]; + sliceTimingGE(dcmSort, dcmList, hdr, verbose, filename, nConvert); + sliceTimingUIH(dcmSort, dcmList, hdr, verbose, filename, nConvert); + sliceTimingXA(dcmSort, dcmList, hdr, verbose, filename, nConvert); + checkSliceTiming(d0, d1, verbose); + rescueSliceTimingSiemens(d0, verbose, hdr->dim[3], filename); //desperate attempts if conventional methods fail + rescueSliceTimingGE(d0, verbose, hdr->dim[3], filename); //desperate attempts if conventional methods fail + if (hdr->dim[3] > 1)sliceDir = headerDcm2Nii2(dcmList[dcmSort[0].indx], dcmList[indx1] , hdr, true); + //UNCOMMENT NEXT TWO LINES TO RE-ORDER MOSAIC WHERE CSA's protocolSliceNumber does not start with 1 + if (dcmList[dcmSort[0].indx].CSA.protocolSliceNumber1 > 1) { + printWarning("Weird CSA 'ProtocolSliceNumber' (System/Miscellaneous/ImageNumbering reversed): VALIDATE SLICETIMING AND BVECS\n"); + //https://www.healthcare.siemens.com/siemens_hwem-hwem_ssxa_websites-context-root/wcm/idc/groups/public/@global/@imaging/@mri/documents/download/mdaz/nzmy/~edisp/mri_60_graessner-01646277.pdf + //see https://github.com/neurolabusc/dcm2niix/issues/40 + sliceDir = -1; //not sure how to handle negative determinants? + } + if (sliceDir < 0) { + if ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_UIH) || (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_GE)) + dcmList[dcmSort[0].indx].CSA.protocolSliceNumber1 = -1; + } + reverseSliceTiming(d0, verbose, hdr->dim[3]); + return sliceDir; +} //sliceTiming() + +/*void reportMat44o(char *str, mat44 A) { printMessage("%s = [%g %g %g %g; %g %g %g %g; %g %g %g %g; 0 0 0 1]\n",str, A.m[0][0],A.m[0][1],A.m[0][2],A.m[0][3], A.m[1][0],A.m[1][1],A.m[1][2],A.m[1][3], A.m[2][0],A.m[2][1],A.m[2][2],A.m[2][3]); -} +}*/ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dcmList[], struct TSearchList *nameList, struct TDCMopts opts, struct TDTI4D *dti4D, int segVol) { bool iVaries = intensityScaleVaries(nConvert,dcmSort,dcmList); @@ -4039,7 +4523,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc dcmList[indx0].triggerDelayTime = triggerDx; //next: determine gantry tilt if (dcmList[indx0].gantryTilt != 0.0f) - printMessage(" Warning: note these images have gantry tilt of %g degrees (manufacturer ID = %d)\n", dcmList[indx0].gantryTilt, dcmList[indx0].manufacturer); + printWarning("Note these images have gantry tilt of %g degrees (manufacturer ID = %d)\n", dcmList[indx0].gantryTilt, dcmList[indx0].manufacturer); if (hdr0.dim[3] < 2) { //stack volumes with multiple acquisitions int nAcq = 1; @@ -4112,19 +4596,21 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc printWarning("Seconds between volumes varies\n"); // saveAs3D = true; // printWarning("Creating independent volumes as time between volumes varies\n"); - printMessage(" OnsetTime = ["); - for (int i = 0; i < nConvert; i++) - if (isSamePosition(dcmList[indx0],dcmList[dcmSort[i].indx])) { - float trDiff = acquisitionTimeDifference(&dcmList[indx0], &dcmList[dcmSort[i].indx]); - printMessage(" %g", trDiff); - } - printMessage(" ]\n"); + if (opts.isVerbose) { + printMessage(" OnsetTime = ["); + for (int i = 0; i < nConvert; i++) + if (isSamePosition(dcmList[indx0],dcmList[dcmSort[i].indx])) { + float trDiff = acquisitionTimeDifference(&dcmList[indx0], &dcmList[dcmSort[i].indx]); + printMessage(" %g", trDiff); + } + printMessage(" ]\n"); + } } //if trVaries } //if PET //next: detect variable inter-slice distance float dx = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[1].indx]); - #ifdef MY_INSTANCE_NUMBER_ORDER_IS_NOT_SPATIAL - if (!isSameFloat(dx, 0.0)) //only for XYZT, not TXYZ: perhaps run for swapDim3Dim4? Extremely rare anomaly + #ifdef myInstanceNumberOrderIsNotSpatial + if (!isSameFloat(dx, 0.0)) //only for XYZT, not TXYZ: perhaps run for swapDim3Dim4? Extremely rare anomaly if (!ensureSequentialSlicePositions(hdr0.dim[3],hdr0.dim[4], dcmSort, dcmList)) dx = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[1].indx]); #endif @@ -4137,33 +4623,68 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc sliceMMarray = (float *) malloc(sizeof(float)*nConvert); sliceMMarray[0] = 0.0f; printWarning("Interslice distance varies in this volume (incompatible with NIfTI format).\n"); - printMessage("Dimensions %d %d %d %d nAcq %d nConvert %d\n", hdr0.dim[1], hdr0.dim[2], hdr0.dim[3], hdr0.dim[4], nAcq, nConvert); - printMessage(" Distance from first slice:\n"); - printMessage("dx=[0"); - for (int i = 1; i < nConvert; i++) { - float dx0 = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[i].indx]); - printMessage(" %g", dx0); - sliceMMarray[i] = dx0; + if (opts.isVerbose) { + printMessage("Dimensions %d %d %d %d nAcq %d nConvert %d\n", hdr0.dim[1], hdr0.dim[2], hdr0.dim[3], hdr0.dim[4], nAcq, nConvert); + printMessage(" Distance from first slice:\n"); + printMessage("dx=[0"); + for (int i = 1; i < nConvert; i++) { + float dx0 = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[i].indx]); + printMessage(" %g", dx0); + sliceMMarray[i] = dx0; + } + printMessage("]\n"); } - printMessage("]\n"); + #ifndef myInstanceNumberOrderIsNotSpatial + //kludge to handle single volume without instance numbers (0020,0013), e.g. https://www.morphosource.org/Detail/MediaDetail/Show/media_id/8430 + bool isInconsistenSliceDir = false; + int slicePositionRepeats = 1; //how many times is first position repeated + if (nConvert > 2) { + float dxPrev = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[1].indx]); + if (isSameFloatGE(dxPrev, 0.0)) slicePositionRepeats++; + for (int i = 2; i < nConvert; i++) { + float dx = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[i].indx]); + if (dx < dxPrev) + isInconsistenSliceDir = true; + if (isSameFloatGE(dxPrev, 0.0)) slicePositionRepeats++; + dxPrev = dx; + } + } + if ((isInconsistenSliceDir) && (slicePositionRepeats == 1)) { + //printWarning("Slice order as defined by instance number not spatially sequential.\n"); + //printWarning("Attempting to reorder slices based on spatial position.\n"); + ensureSequentialSlicePositions(hdr0.dim[3],hdr0.dim[4], dcmSort, dcmList); + dx = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[1].indx]); + hdr0.pixdim[3] = dx; + isInconsistenSliceDir = false; + } + if (isInconsistenSliceDir) { + printMessage("Unable to equalize slice distances: slice order not consistently ascending.\n"); + printMessage("First spatial position repeated %d times\n", slicePositionRepeats); + printError(" Recompiling with '-DmyInstanceNumberOrderIsNotSpatial' might help.\n"); + return EXIT_FAILURE; + } + #endif int imageNumRange = 1 + abs( dcmList[dcmSort[nConvert-1].indx].imageNum - dcmList[dcmSort[0].indx].imageNum); if ((imageNumRange > 1) && (imageNumRange != nConvert)) { printWarning("Missing images? Expected %d images, but instance number (0020,0013) ranges from %d to %d\n", nConvert, dcmList[dcmSort[0].indx].imageNum, dcmList[dcmSort[nConvert-1].indx].imageNum); - printMessage("instance=["); - for (int i = 0; i < nConvert; i++) { - printMessage(" %d", dcmList[dcmSort[i].indx].imageNum); + if (opts.isVerbose) { + printMessage("instance=["); + for (int i = 0; i < nConvert; i++) { + printMessage(" %d", dcmList[dcmSort[i].indx].imageNum); + } + printMessage("]\n"); } - printMessage("]\n"); } //imageNum not sequential } //dx varies } //not 4D if ((hdr0.dim[4] > 0) && (dxVaries) && (dx == 0.0) && ((dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_UNKNOWN) || (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_GE) || (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_PHILIPS)) ) { //Niels Janssen has provided GE sequential multi-phase acquisitions that also require swizzling swapDim3Dim4(hdr0.dim[3],hdr0.dim[4],dcmSort); dx = intersliceDistance(dcmList[dcmSort[0].indx],dcmList[dcmSort[1].indx]); - printMessage("swizzling 3rd and 4th dimensions (XYTZ -> XYZT), assuming interslice distance is %f\n",dx); + if (opts.isVerbose) + printMessage("Swizzling 3rd and 4th dimensions (XYTZ -> XYZT), assuming interslice distance is %f\n",dx); } if ((dx == 0.0 ) && (!dxVaries)) { //all images are the same slice - 16 Dec 2014 - printMessage(" Warning: all images appear to be a single slice - please check slice/vector orientation\n"); + printWarning("All images appear to be a single slice - please check slice/vector orientation\n"); hdr0.dim[3] = 1; hdr0.dim[4] = nConvert; hdr0.dim[0] = 4; @@ -4195,141 +4716,46 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc //printMessage(" %d %d %d %d %lu\n", hdr0.dim[1], hdr0.dim[2], hdr0.dim[3], hdr0.dim[4], (unsigned long)[imgM length]); struct nifti_1_header hdrI; //double time = -1.0; - for (int i = 1; i < nConvert; i++) { //stack additional images - indx = dcmSort[i].indx; - //double time2 = dcmList[dcmSort[i].indx].acquisitionTime; - //if (time != time2) - // printWarning("%g\n", time2); - //time = time2; - //if (headerDcm2Nii(dcmList[indx], &hdrI) == EXIT_FAILURE) return EXIT_FAILURE; - img = nii_loadImgXL(nameList->str[indx], &hdrI, dcmList[indx],iVaries, opts.compressFlag, opts.isVerbose, dti4D); - if (img == NULL) return EXIT_FAILURE; - if ((hdr0.dim[1] != hdrI.dim[1]) || (hdr0.dim[2] != hdrI.dim[2]) || (hdr0.bitpix != hdrI.bitpix)) { - printError("Image dimensions differ %s %s",nameList->str[dcmSort[0].indx], nameList->str[indx]); - free(imgM); - free(img); - return EXIT_FAILURE; - } - memcpy(&imgM[(uint64_t)i*imgsz], &img[0], imgsz); - free(img); - } + if (!opts.isOnlyBIDS) { + for (int i = 1; i < nConvert; i++) { //stack additional images + indx = dcmSort[i].indx; + //double time2 = dcmList[dcmSort[i].indx].acquisitionTime; + //if (time != time2) + // printWarning("%g\n", time2); + //time = time2; + //if (headerDcm2Nii(dcmList[indx], &hdrI) == EXIT_FAILURE) return EXIT_FAILURE; + img = nii_loadImgXL(nameList->str[indx], &hdrI, dcmList[indx],iVaries, opts.compressFlag, opts.isVerbose, dti4D); + if (img == NULL) return EXIT_FAILURE; + if ((hdr0.dim[1] != hdrI.dim[1]) || (hdr0.dim[2] != hdrI.dim[2]) || (hdr0.bitpix != hdrI.bitpix)) { + printError("Image dimensions differ %s %s",nameList->str[dcmSort[0].indx], nameList->str[indx]); + free(imgM); + free(img); + return EXIT_FAILURE; + } + memcpy(&imgM[(uint64_t)i*imgsz], &img[0], imgsz); + free(img); + } + } //skip if we are only creating BIDS 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]); } - //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)) && (nConvert == (hdr0.dim[3]*hdr0.dim[4])) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { - //XA11 2D classic - for (int v = 0; v < hdr0.dim[3]; v++) - dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = dcmList[dcmSort[v].indx].CSA.sliceTiming[0]; - } else if (((dcmList[dcmSort[0].indx].isXA10A)) && (nConvert == (hdr0.dim[4])) && (hdr0.dim[3] < (kMaxEPI3D-1)) && (hdr0.dim[3] > 1) && (hdr0.dim[4] > 1)) { - //XA10 mosaics - these are missing a lot of information - float mn = dcmList[dcmSort[1].indx].CSA.sliceTiming[0]; - //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]; - float maxT = minT; - 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++) - if (dcmList[dcmSort[0].indx].CSA.sliceTiming[v] > maxT) - maxT = 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; - if (isSameFloatGE(minT, maxT)) { - //ABCD simulated GE DICOMs do not populate 0018,1060 correctly - GEsliceTiming_x0018x1060 = false; - dcmList[dcmSort[0].indx].CSA.sliceTiming[0] = -1.0; //no valid slice times - } - } //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) { - double scale2Sec = 1.0; - if (dcmList[dcmSort[0].indx].TR > 0.0) { //issue 286: determine units for rtia_timerGE - //See https://github.com/rordenlab/dcm2niix/tree/master/GE - // Nikadon's DV24 data stores RTIA Timer as seconds, issue 286 14_LX uses 1/10,000 sec - // The slice timing should always be less than the TR (which is in ms) - // Below we assume 1/10,000 of a sec if slice time is >90% and less than <100% of a TR - // Will not work for sparse designs, but slice timing inappropriate for those datasets - float maxSliceTimeFrac = (maxTime-minTime) / dcmList[dcmSort[0].indx].TR; //should be slightly less than 1.0 - if ((maxSliceTimeFrac > 9.0) && (maxSliceTimeFrac < 10)) - scale2Sec = 1.0 / 10000.0; - //printMessage(">> %g %g %g\n", maxSliceTimeFrac, scale2Sec, dcmList[dcmSort[0].indx].TR); - } - for (int v = 0; v < hdr0.dim[3]; v++) - dcmList[dcmSort[0].indx].CSA.sliceTiming[v] = (dcmList[dcmSort[v+j].indx].rtia_timerGE - minTime) * scale2Sec; - 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 times - 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 + int sliceDir = sliceTimingCore(dcmSort, dcmList, &hdr0, opts.isVerbose, nameList->str[dcmSort[0].indx], nConvert); + //move before headerDcm2Nii2 checkSliceTiming(&dcmList[indx0], &dcmList[indx1]); + char pathoutname[2048] = {""}; + if (nii_createFilename(dcmList[dcmSort[0].indx], pathoutname, opts) == EXIT_FAILURE) { + free(imgM); + return EXIT_FAILURE; + } + if (strlen(pathoutname) <1) { + free(imgM); + return EXIT_FAILURE; + } + nii_SaveBIDS(pathoutname, dcmList[dcmSort[0].indx], opts, &hdr0, nameList->str[dcmSort[0].indx]); + if (opts.isOnlyBIDS) { + //note we waste time loading every image, however this ensures hdr0 matches actual output + free(imgM); + return EXIT_SUCCESS; + } if ((segVol >= 0) && (hdr0.dim[4] > 1)) { int inVol = hdr0.dim[4]; int nVol = 0; @@ -4363,15 +4789,6 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc } if (strlen(dcmList[dcmSort[0].indx].protocolName) < 1) //beware: tProtocolName can vary within a series "t1+AF8-mpr+AF8-ns+AF8-sag+AF8-p2+AF8-iso" vs "T1_mprage_ns_sag_p2_iso 1.0mm_192" 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); - return EXIT_FAILURE; - } - if (strlen(pathoutname) <1) { - free(imgM); - return EXIT_FAILURE; - } // Prevent these DICOM files from being reused. for(int i = 0; i < nConvert; ++i) dcmList[dcmSort[i].indx].converted2NII = 1; @@ -4384,23 +4801,9 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc printMessage(" %s\n",nameList->str[dcmSort[0].indx]); return EXIT_SUCCESS; } - checkSliceTiming(&dcmList[indx0], &dcmList[indx1]); - int sliceDir = 0; - if (hdr0.dim[3] > 1)sliceDir = headerDcm2Nii2(dcmList[dcmSort[0].indx],dcmList[dcmSort[nConvert-1].indx] , &hdr0, true); - //UNCOMMENT NEXT TWO LINES TO RE-ORDER MOSAIC WHERE CSA's protocolSliceNumber does not start with 1 - if (dcmList[dcmSort[0].indx].CSA.protocolSliceNumber1 > 1) { - printWarning("Weird CSA 'ProtocolSliceNumber' (System/Miscellaneous/ImageNumbering reversed): VALIDATE SLICETIMING AND BVECS\n"); - //https://www.healthcare.siemens.com/siemens_hwem-hwem_ssxa_websites-context-root/wcm/idc/groups/public/@global/@imaging/@mri/documents/download/mdaz/nzmy/~edisp/mri_60_graessner-01646277.pdf - //see https://github.com/neurolabusc/dcm2niix/issues/40 - sliceDir = -1; //not sure how to handle negative determinants? - } - if (sliceDir < 0) { + 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) { @@ -4418,14 +4821,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc return EXIT_SUCCESS; } } - //move before headerDcm2Nii2 checkSliceTiming(&dcmList[indx0], &dcmList[indx1]); - nii_SaveBIDS(pathoutname, dcmList[dcmSort[0].indx], opts, &hdr0, nameList->str[dcmSort[0].indx]); - if (opts.isOnlyBIDS) { - //note we waste time loading every image, however this ensures hdr0 matches actual output - free(imgM); - return EXIT_SUCCESS; - } - nii_saveText(pathoutname, dcmList[dcmSort[0].indx], opts, &hdr0, nameList->str[indx]); + nii_saveText(pathoutname, dcmList[dcmSort[0].indx], opts, &hdr0, nameList->str[indx]); int numADC = 0; int * volOrderIndex = nii_saveDTI(pathoutname,nConvert, dcmSort, dcmList, opts, sliceDir, dti4D, &numADC); PhilipsPrecise(&dcmList[dcmSort[0].indx], opts.isPhilipsFloatNotDisplayScaling, &hdr0, opts.isVerbose); @@ -4438,7 +4834,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc } else if ((!opts.isMaximize16BitRange) && (hdr0.datatype == DT_UINT16) && (!dcmList[dcmSort[0].indx].isSigned)) 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]); - #ifdef USING_R + #ifndef USING_R fflush(stdout); //show immediately if run from MRIcroGL GUI #endif //~ if (!dcmList[dcmSort[0].indx].isSlicesSpatiallySequentialPhilips) @@ -4542,9 +4938,11 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[],struct TDICOMdata dc returnCode = nii_saveCrop(pathoutname, hdr0, imgM, opts, dcmList[dcmSort[0].indx]); //n.b. must be run AFTER nii_setOrtho()! #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, dcmList[dcmSort[0].indx]) == EXIT_SUCCESS) - nii_saveAttributes(dcmList[dcmSort[0].indx], hdr0, opts); + // Hence this extra test + if (returnCode != EXIT_SUCCESS) + returnCode = nii_saveNII(pathoutname, hdr0, imgM, opts, dcmList[dcmSort[0].indx]); + if (returnCode == EXIT_SUCCESS) + nii_saveAttributes(dcmList[dcmSort[0].indx], hdr0, opts, nameList->str[dcmSort[0].indx]); #endif free(imgM); return returnCode;//EXIT_SUCCESS; @@ -4672,6 +5070,9 @@ int compareTDCMsort(void const *item1, void const *item2) { retval = -1; else if (dcm1->img > dcm2->img) retval = 1; + //printf("%d %d\n", dcm1->img, dcm2->img); + //for(int i=0; i < MAX_NUMBER_OF_DIMENSIONS; i++) + // printf("%d %d\n", dcm1->dimensionIndexValues[i], dcm2->dimensionIndexValues[i]); if(retval != 0) return retval; //sorted images // Check the dimensionIndexValues (useful for enhanced DICOM 4D series). // ->img is basically behaving as a (seriesNum, imageNum) sort key @@ -4772,7 +5173,7 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt if (strstr(d1.sequenceName, "_ep_b") && strstr(d2.sequenceName, "_ep_b") && (strstr(d1.softwareVersions, "VB13") || strstr(d1.softwareVersions, "VB12")) ) { //Siemens B12/B13 users with a "DWI" but not "DTI" license would ofter create multi-series acquisitions if (!warnings->forceStackSeries) - printMessage("diffusion images stacked despite varying series number (early Siemens DTI).\n"); + printMessage("Diffusion images stacked despite varying series number (early Siemens DTI).\n"); warnings->forceStackSeries = true; isForceStackSeries = true; } @@ -4798,41 +5199,44 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt 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"); + printMessage("Slices not stacked: dimensions or bit-depth varies\n"); warnings->bitDepthVaries = true; return false; } #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); + 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)) + if ((opts->isForceStackSameSeries == 1) || ((opts->isForceStackSameSeries == 2) && (d1.isXRay) )) { + // "isForceStackSameSeries == 2" will automatically stack CT scans but not MR + //if ((d1.TE != d2.TE) || (d1.echoNum != d2.echoNum)) + if ((!(isSameFloat(d1.TE, d2.TE))) || (d1.echoNum != d2.echoNum)) *isMultiEcho = true; return true; //we will stack these images, even if they differ in the following attributes } if ((d1.isHasImaginary != d2.isHasImaginary) || (d1.isHasPhase != d2.isHasPhase) || ((d1.isHasReal != d2.isHasReal))) { if (!warnings->phaseVaries) - printMessage("slices not stacked: some are phase/real/imaginary maps, others are not. Use 'merge 2D slices' option to force stacking\n"); + printMessage("Slices not stacked: some are phase/real/imaginary maps, others are not. Use 'f 2D slices' option to force stacking\n"); warnings->phaseVaries = true; return false; } - if ((d1.TE != d2.TE) || (d1.echoNum != d2.echoNum)) { + //if ((d1.TE != d2.TE) || (d1.echoNum != d2.echoNum)) { + if ((!(isSameFloat(d1.TE, d2.TE)) ) || (d1.echoNum != d2.echoNum)) { if ((!warnings->echoVaries) && (d1.isXRay)) //for CT/XRay we check DICOM tag 0018,1152 (XRayExposure) - printMessage("slices not stacked: X-Ray Exposure varies (exposure %g, %g; number %d, %d). Use 'merge 2D slices' option to force stacking\n", d1.TE, d2.TE,d1.echoNum, d2.echoNum ); + printMessage("Slices not stacked: X-Ray Exposure varies (exposure %g, %g; number %d, %d). Use 'merge 2D slices' option to force stacking\n", d1.TE, d2.TE,d1.echoNum, d2.echoNum ); if ((!warnings->echoVaries) && (!d1.isXRay)) //for MRI - printMessage("slices not stacked: echo varies (TE %g, %g; echo %d, %d). Use 'merge 2D slices' option to force stacking\n", d1.TE, d2.TE,d1.echoNum, d2.echoNum ); + printMessage("Slices not stacked: echo varies (TE %g, %g; echo %d, %d). Use 'merge 2D slices' option to force stacking\n", d1.TE, d2.TE,d1.echoNum, d2.echoNum ); warnings->echoVaries = true; *isMultiEcho = true; return false; } if (d1.coilCrc != d2.coilCrc) { if (!warnings->coilVaries) - printMessage("slices not stacked: coil varies\n"); + printMessage("Slices not stacked: coil varies\n"); warnings->coilVaries = true; *isCoilVaries = true; return false; @@ -4843,14 +5247,14 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt warnings->nameEmpty = true; } else if ((strcmp(d1.protocolName, d2.protocolName) != 0)) { if (!warnings->nameVaries) - printMessage("slices not stacked: protocol name varies '%s' != '%s'\n", d1.protocolName, d2.protocolName); + printMessage("Slices not stacked: protocol name varies '%s' != '%s'\n", d1.protocolName, d2.protocolName); warnings->nameVaries = true; return false; } 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) && (!d1.isNonParallelSlices)) - printMessage("slices not stacked: orientation varies (vNav or localizer?) [%g %g %g %g %g %g] != [%g %g %g %g %g %g]\n", + 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; @@ -4858,13 +5262,13 @@ bool isSameSet (struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts* opt 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"); + if ((!warnings->acqNumVaries) && (opts->isVerbose)) //virtually always people want to stack these + printMessage("Slices stacked despite varying acquisition numbers (if this is not desired recompile with 'mySegmentByAcq')\n"); warnings->acqNumVaries = true; } if ((!isForceStackSeries) && (d1.seriesUidCrc != d2.seriesUidCrc)) { if (!warnings->seriesUidVaries) - printMessage("slices not stacked: series instance UID varies (duplicates all other properties)\n"); + printMessage("Slices not stacked: series instance UID varies (duplicates all other properties)\n"); warnings->seriesUidVaries = true; return false; } @@ -4909,8 +5313,12 @@ int textDICOM(struct TDCMopts* opts, char *fname) { //check input file FILE *fp = fopen(fname, "r"); if (fp == NULL) +#ifdef USING_R + return EXIT_FAILURE; +#else exit(EXIT_FAILURE); - int nConvert = 0; +#endif + int nConvert = 0; char dcmname[2048]; while (fgets(dcmname, sizeof(dcmname), fp)) { int sz = strlen(dcmname); @@ -4931,7 +5339,7 @@ int textDICOM(struct TDCMopts* opts, char *fname) { return EXIT_FAILURE; } printMessage("Found %d DICOM file(s)\n", nConvert); - #ifdef USING_R + #ifndef USING_R fflush(stdout); //show immediately if run from MRIcroGL GUI #endif TDCMsort * dcmSort = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); @@ -4963,65 +5371,6 @@ int textDICOM(struct TDCMopts* opts, char *fname) { return ret; }//textDICOM() -/* -//code below fails on Windows https://github.com/rordenlab/dcm2niix/issues/288 -int textDICOM(struct TDCMopts* opts, char *fname) { - //check input file - FILE *fp = fopen(fname, "r"); - if (fp == NULL) - exit(EXIT_FAILURE); - char *dcmname = NULL; - int nConvert = 0; - size_t len = 0; - size_t sz; - while ((sz =getline(&dcmname, &len, fp)) != -1) { - if (sz > 0 && dcmname[sz-1] == '\n') dcmname[sz-1] = 0; //Unix LF - if (sz > 1 && dcmname[sz-2] == '\r') dcmname[sz-2] = 0; //Windows CR/LF - //if (isDICOMfile(dcmname) == 0) { //<- this will reject DICOM metadata not wrapped with a header - if ((!is_fileexists(dcmname)) || (!is_fileNotDir(dcmname)) ) { //<-this will accept meta data - fclose(fp); - printError("Problem with file '%s'\n", dcmname); - return EXIT_FAILURE; - } - //printf("%s\n", dcmname); - nConvert ++; - } - fclose(fp); - if (nConvert < 1) { - printError("No DICOM files found '%s'\n", dcmname); - return EXIT_FAILURE; - } - printMessage("Found %d DICOM file(s)\n", nConvert); - #ifdef USING_R - fflush(stdout); //show immediately if run from MRIcroGL GUI - #endif - TDCMsort * dcmSort = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); - struct TDICOMdata *dcmList = (struct TDICOMdata *)malloc(nConvert * sizeof(struct TDICOMdata)); - struct TDTI4D dti4D; - struct TSearchList nameList; - nameList.maxItems = nConvert; // larger requires more memory, smaller more passes - nameList.str = (char **) malloc((nameList.maxItems+1) * sizeof(char *)); //reserve one pointer (32 or 64 bits) per potential file - nameList.numItems = 0; - nConvert = 0; - fp = fopen(fname, "r"); - while ((sz =getline(&dcmname, &len, fp)) != -1) { - if (sz > 0 && dcmname[sz-1] == '\n') dcmname[sz-1] = 0; - if (sz > 1 && dcmname[sz-2] == '\r') dcmname[sz-2] = 0; - nameList.str[nameList.numItems] = (char *)malloc(strlen(dcmname)+1); - strcpy(nameList.str[nameList.numItems],dcmname); - nameList.numItems++; - dcmList[nConvert] = readDICOMv(nameList.str[nConvert], opts->isVerbose, opts->compressFlag, &dti4D); //ignore compile warning - memory only freed on first of 2 passes - fillTDCMsort(dcmSort[nConvert], nConvert, dcmList[nConvert]); - nConvert ++; - } - fclose(fp); - qsort(dcmSort, nConvert, sizeof(struct TDCMsort), compareTDCMsort); //sort based on series and image numbers.... - int ret = saveDcm2Nii(nConvert, dcmSort, dcmList, &nameList, *opts, &dti4D); - free(dcmSort); - free(dcmList); - freeNameList(nameList); - return ret; -}//textDICOM()*/ #else //ifdef myTextFileInputLists int textDICOM(struct TDCMopts* opts, char *fname) { printError("Unable to parse txt files: re-compile with 'myTextFileInputLists' (see issue 288)"); @@ -5065,12 +5414,14 @@ void searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, i } nameList->numItems++; //printMessage("dcm %lu %s \n",nameList->numItems, filename); +#ifndef USING_R } else { if (fileBytes(filename) > 2048) convert_foreign (filename, *opts); #ifdef MY_DEBUG printMessage("Not a dicom:\t%s\n", filename); #endif +#endif } tinydir_next(&dir); } @@ -5151,7 +5502,7 @@ int copyFile (char * src_path, char * dst_path) { } if (is_fileexists(dst_path)) { if (true) { - printWarning("Naming conflict: skipping existing %s\n", dst_path); + printWarning("Naming conflict (duplicates?): '%s' '%s'\n", src_path, dst_path); return EXIT_SUCCESS; } else { printError("File naming conflict. Existing file %s\n", dst_path); @@ -5175,15 +5526,112 @@ int copyFile (char * src_path, char * dst_path) { return EXIT_SUCCESS; } +#ifdef USING_R + +// This implementation differs enough from the mainline one to be separated +int searchDirRenameDICOM(char *path, int maxDepth, int depth, struct TDCMopts* opts ) { + // The tinydir_open_sorted function reads the whole directory at once, + // which is necessary in this context since we may be creating new + // files in the same directory, which we don't want to further examine + tinydir_dir dir; + int count = 0; + if (tinydir_open_sorted(&dir, path) != 0) + return -1; + + for (size_t i=0; i(sourcePath.c_str()); + if ((file.is_dir) && (depth < maxDepth) && (file.name[0] != '.')) { + const int subdirectoryCount = searchDirRenameDICOM(sourcePathPtr, maxDepth, depth+1, opts); + if (subdirectoryCount < 0) { + tinydir_close(&dir); + return -1; + } + count += subdirectoryCount; + } else if (file.is_reg && strlen(file.name) > 0 && file.name[0] != '.' && strcicmp(file.name,"DICOMDIR") != 0 && isDICOMfile(sourcePathPtr)) { + TDICOMdata dcm = readDICOM(sourcePathPtr); + if (dcm.imageNum > 0) { + if ((opts->isIgnoreDerivedAnd2D) && ((dcm.isLocalizer) || (strcmp(dcm.sequenceName, "_tfl2d1")== 0) || (strcmp(dcm.sequenceName, "_fl3d1_ns")== 0) || (strcmp(dcm.sequenceName, "_fl2d1")== 0)) ) { + printMessage("Ignoring localizer %s\n", sourcePathPtr); + opts->ignoredPaths.push_back(sourcePath); + } else if ((opts->isIgnoreDerivedAnd2D && dcm.isDerived) ) { + printMessage("Ignoring derived %s\n", sourcePathPtr); + opts->ignoredPaths.push_back(sourcePath); + } else { + // Create an initial file name + char outname[PATH_MAX] = {""}; + if (dcm.echoNum > 1) + dcm.isMultiEcho = true; + nii_createFilename(dcm, outname, *opts); + + // If the file name part of the target path has no extension, add ".dcm" + std::string targetPath(outname); + std::string targetStem, targetExtension; + const size_t periodLoc = targetPath.find_last_of('.'); + if (periodLoc == targetPath.length() - 1) { + targetStem = targetPath.substr(0, targetPath.length() - 1); + targetExtension = ".dcm"; + } else if (periodLoc == std::string::npos || periodLoc < targetPath.find_last_of("\\/")) { + targetStem = targetPath; + targetExtension = ".dcm"; + } else { + targetStem = targetPath.substr(0, periodLoc); + targetExtension = targetPath.substr(periodLoc); + } + + // Deduplicate the target path to avoid overwriting existing files + targetPath = targetStem + targetExtension; + GetRNGstate(); + while (is_fileexists(targetPath.c_str())) { + std::ostringstream suffix; + unsigned suffixValue = static_cast(round(R::unif_rand() * (R_pow_di(2.0,24) - 1.0))); + suffix << std::hex << std::setfill('0') << std::setw(6) << suffixValue; + targetPath = targetStem + "_" + suffix.str() + targetExtension; + } + PutRNGstate(); + + // Copy the file, unless the source and target paths are the same + if (targetPath.compare(sourcePath) == 0) { + if (opts->isVerbose > 1) + printMessage("Skipping %s, which would be copied onto itself\n", sourcePathPtr); + } else if (copyFile(sourcePathPtr, const_cast(targetPath.c_str())) == EXIT_SUCCESS) { + opts->sourcePaths.push_back(sourcePath); + opts->targetPaths.push_back(targetPath); + count++; + if (opts->isVerbose > 0) + printMessage("Copying %s -> %s\n", sourcePathPtr, targetPath.c_str()); + } else { + printWarning("Unable to copy to path %s\n", targetPath.c_str()); + } + } + } + } + } + return count; +} + +#else + int searchDirRenameDICOM(char *path, int maxDepth, int depth, struct TDCMopts* opts ) { int retAll = 0; - //bool isDcmExt = isExt(opts->filename, ".dcm"); // "%r.dcm" with multi-echo should generate "1.dcm", "1e2.dcm" tinydir_dir dir; - tinydir_open(&dir, path); - while (dir.has_next) { - tinydir_file file; - file.is_dir = 0; //avoids compiler warning: this is set by tinydir_readfile - tinydir_readfile(&dir, &file); + if (tinydir_open_sorted(&dir, path) != 0) { + if (opts->isVerbose > 0) + printMessage("Unable to open %s\n", path); + return -1; + } + if (dir.n_files < 1) { + if (opts->isVerbose > 0) + printMessage("No files in %s\n", path); + return 0; + } + if (opts->isVerbose > 0) + printMessage("Found %lu items in %s\n", dir.n_files, path); + for (size_t i=0; icrc < dcm2->crc) + return -1; + else if (dcm1->crc > dcm2->crc) + return 1; + return 0; //tie +} +#endif + +#ifdef myTimer +int reportProgress(int progressPct, float frac) { + int newProgressPct = round(100.0 * frac); + const int kMinPct = 5; //e.g. if 10 then report 0.1, 0.2, 0.3... + newProgressPct = (newProgressPct / kMinPct) * kMinPct; //if MinPct is 5 and we are 87 percent done report 85% + if (newProgressPct == progressPct) return progressPct; + if (newProgressPct != progressPct) //only report for change + printProgress((float)newProgressPct/100.0); + return newProgressPct; +} +#endif + + int nii_loadDirCore(char *indir, struct TDCMopts* opts) { struct TSearchList nameList; #if defined(_WIN64) || defined(_WIN32) || defined(USING_R) nameList.maxItems = 24000; // larger requires more memory, smaller more passes #else //UNIX, not R nameList.maxItems = 96000; // larger requires more memory, smaller more passes + #endif + //progress variables + const float kStage1Frac = 0.05; //e.g. finding files requires ~05pct + const float kStage2Frac = 0.45; //e.g. reading headers and converting 4D files requires ~45pct + const float kStage3Frac = 0.50; //e.g. converting 2D/3D files to 3D/4D files requires ~50pct + int progressPct = 0; //proportion correct, 0..100 + if (opts->isProgress) + progressPct = reportProgress(-1, 0.0); //report 0% + #ifdef myTimer + clock_t start = clock(); #endif //1: find filenames of dicom files: up to two passes if we found more files than we allocated memory for (int i = 0; i < 2; i++ ) { @@ -5257,7 +5762,13 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { } size_t nDcm = nameList.numItems; printMessage( "Found %lu DICOM file(s)\n", nameList.numItems); //includes images and other non-image DICOMs - // struct TDICOMdata dcmList [nameList.numItems]; //<- this exhausts the stack for large arrays + #ifdef myTimer + if (opts->isProgress > 1) printMessage ("Stage 1 (Count number of DICOMs) required %f seconds.\n",((float)(clock()-start))/CLOCKS_PER_SEC); + start = clock(); + #endif + if (opts->isProgress) + progressPct = reportProgress(progressPct, kStage1Frac); //proportion correct, 0..100 + // struct TDICOMdata dcmList [nameList.numItems]; //<- this exhausts the stack for large arrays struct TDICOMdata *dcmList = (struct TDICOMdata *)malloc(nameList.numItems * sizeof(struct TDICOMdata)); struct TDTI4D dti4D; int nConvertTotal = 0; @@ -5291,7 +5802,13 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { compressionWarning = true; //generate once per conversion rather than once per image printMessage("Image Decompression is new: please validate conversions\n"); } + if (opts->isProgress) + progressPct = reportProgress(progressPct, kStage1Frac+ (kStage2Frac *(float)i/(float)nDcm)); //proportion correct, 0..100 } + #ifdef myTimer + if (opts->isProgress > 1) printMessage ("Stage 2 (Read DICOM headers, Convert 4D) required %f seconds.\n",((float)(clock()-start))/CLOCKS_PER_SEC); + start = clock(); + #endif if (opts->isRenameNotConvert) { return EXIT_SUCCESS; } @@ -5300,6 +5817,9 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { TWarnings warnings = setWarnings(); // Create the first series from the first DICOM file TDicomSeries firstSeries; + char firstSeriesName[2048] = ""; + nii_createFilename(dcmList[0], firstSeriesName, *opts); + firstSeries.name = firstSeriesName; firstSeries.representativeData = dcmList[0]; firstSeries.files.push_back(nameList.str[0]); opts->series.push_back(firstSeries); @@ -5318,6 +5838,9 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { // If not, create a new series object if (!matched) { TDicomSeries nextSeries; + char nextSeriesName[2048] = ""; + nii_createFilename(dcmList[i], nextSeriesName, *opts); + nextSeries.name = nextSeriesName; nextSeries.representativeData = dcmList[i]; nextSeries.files.push_back(nameList.str[i]); opts->series.push_back(nextSeries); @@ -5327,6 +5850,7 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { nConvertTotal = nDcm; } else { #endif + #ifdef myBubbleSort //3: stack DICOMs with the same Series struct TWarnings warnings = setWarnings(); for (int i = 0; i < (int)nDcm; i++ ) { @@ -5362,7 +5886,6 @@ int nii_loadDirCore(char *indir, 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.... @@ -5380,9 +5903,76 @@ int nii_loadDirCore(char *indir, struct TDCMopts* opts) { free(dcmSort); }//convert all images of this series } + #else //avoid bubble sort - dont check all images for match, only those with identical series instance UID + //3: stack DICOMs with the same Series + struct TWarnings warnings = setWarnings(); + //sort by series instance UID ... avoids bubble-sort penalty + TCRCsort * crcSort = (TCRCsort *)malloc(nDcm * sizeof(TCRCsort)); + for (int i = 0; i < (int)nDcm; i++ ) + fillTCRCsort(crcSort[i], i, dcmList[i].seriesUidCrc); + qsort(crcSort, nDcm, sizeof(struct TCRCsort), compareTCRCsort); //sort based on series and image numbers.... + int * convertIdxs = (int *)malloc(sizeof(int) * (nDcm)); + for (int i = 0; i < (int)nDcm; i++ ) { + int ii = crcSort[i].indx; + if (dcmList[ii].converted2NII) continue; + if (!dcmList[ii].isValid) continue; + int nConvert = 0; + bool isMultiEcho = false; + bool isNonParallelSlices = false; + bool isCoilVaries = false; + for (int j = i; j < (int)nDcm; j++) { + int ji = crcSort[j].indx; + if (dcmList[ii].seriesUidCrc != dcmList[ji].seriesUidCrc) break; //seriesUID no longer matches no need to examine any subsequent images + isMultiEcho = false; + isNonParallelSlices = false; + isCoilVaries = false; + if (isSameSet(dcmList[ii], dcmList[ji], opts, &warnings, &isMultiEcho, &isNonParallelSlices, &isCoilVaries)) { + dcmList[ji].converted2NII = 1; //do not reprocess repeats + convertIdxs[nConvert] = ji; + nConvert++; + } else { + if (isNonParallelSlices) { + dcmList[ii].isNonParallelSlices = true; + dcmList[ji].isNonParallelSlices = true; + } + if (isMultiEcho) { + dcmList[ii].isMultiEcho = true; + dcmList[ji].isMultiEcho = true; + } + if (isCoilVaries) { + dcmList[ii].isCoilVaries = true; + dcmList[ji].isCoilVaries = true; + } + } //unable to stack images: mark files that may need file name dis-ambiguation + } //for all images with same seriesUID as first one + TDCMsort * dcmSort = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); + for (int j = 0; j < nConvert; j++) + fillTDCMsort(dcmSort[j], convertIdxs[j], dcmList[convertIdxs[j]]); + qsort(dcmSort, nConvert, sizeof(struct TDCMsort), compareTDCMsort); //sort based on series and image numbers.... + if (opts->isVerbose) + nConvert = removeDuplicatesVerbose(nConvert, dcmSort, &nameList); + else + nConvert = removeDuplicates(nConvert, dcmSort); + int ret = saveDcm2Nii(nConvert, dcmSort, dcmList, &nameList, *opts, &dti4D); + if (ret == EXIT_SUCCESS) + nConvertTotal += nConvert; + else + convertError = true; + free(dcmSort); + if (opts->isProgress) + progressPct = reportProgress(progressPct, kStage1Frac+kStage2Frac+ (kStage3Frac *(float)nConvertTotal/(float)nDcm)); //proportion correct, 0..100 + } + free(convertIdxs); + free(crcSort); + #endif #ifdef USING_R } #endif + #ifdef myTimer + if (opts->isProgress > 1) + printMessage ("Stage 3 (Convert 2D and 3D images) required %f seconds.\n",((float)(clock()-start))/CLOCKS_PER_SEC); + #endif + if (opts->isProgress) progressPct = reportProgress(progressPct, 1); //proportion correct, 0..100 free(dcmList); freeNameList(nameList); if (convertError) @@ -5433,6 +6023,10 @@ int nii_loadDir(struct TDCMopts* opts) { if (isFile) //if user passes ~/dicom/mr1.dcm we will look at all files in ~/dicom dropFilenameFromPath(opts->indir); dropTrailingFileSep(opts->indir); +#ifdef USING_R + // Full file paths are only used by R/divest when reorganising DICOM files + if (opts->isRenameNotConvert) { +#endif if (strlen(opts->outdir) < 1) { strcpy(opts->outdir,opts->indir); } else @@ -5446,9 +6040,14 @@ int nii_loadDir(struct TDCMopts* opts) { return EXIT_FAILURE; #endif } +#ifdef USING_R + } +#endif getFileNameX(opts->indirParent, opts->indir, 512); +#ifndef USING_R if (isFile && ( (isExt(indir, ".v"))) ) return convert_foreign (indir, *opts); +#endif if (isFile && ( (isExt(indir, ".par")) || (isExt(indir, ".rec"))) ) { char pname[512], rname[512]; strcpy(pname,indir); @@ -5469,7 +6068,11 @@ int nii_loadDir(struct TDCMopts* opts) { if (opts->isRenameNotConvert) { int nConvert = searchDirRenameDICOM(opts->indir, opts->dirSearchDepth, 0, opts); if (nConvert < 0) return EXIT_FAILURE; +#ifdef USING_R + printMessage("Renamed %d DICOMs\n", nConvert); +#else printMessage("Converted %d DICOMs\n", nConvert); +#endif return EXIT_SUCCESS; } if ((isFile) && (opts->isOnlySingleFile)) @@ -5556,21 +6159,23 @@ int findpathof(char *pth, const char *exe) { #ifndef USING_R void readFindPigz (struct TDCMopts *opts, const char * argv[]) { - #if defined(_WIN64) || defined(_WIN32) + #if defined(_WIN64) || defined(_WIN32) strcpy(opts->pigzname,"pigz.exe"); if (!is_exe(opts->pigzname)) { #if defined(__APPLE__) #ifdef myDisableZLib printMessage("Compression requires %s in the same folder as the executable http://macappstore.org/pigz/\n",opts->pigzname); #else //myUseZLib - printMessage("Compression will be faster with %s in the same folder as the executable http://macappstore.org/pigz/\n",opts->pigzname); + if (opts->isVerbose > 0) + printMessage("Compression will be faster with %s in the same folder as the executable http://macappstore.org/pigz/\n",opts->pigzname); #endif strcpy(opts->pigzname,""); #else #ifdef myDisableZLib printMessage("Compression requires %s in the same folder as the executable\n",opts->pigzname); #else //myUseZLib - printMessage("Compression will be faster with %s in the same folder as the executable\n",opts->pigzname); + if (opts->isVerbose > 0) + printMessage("Compression will be faster with %s in the same folder as the executable\n",opts->pigzname); #endif strcpy(opts->pigzname,""); #endif @@ -5624,13 +6229,15 @@ void readFindPigz (struct TDCMopts *opts, const char * argv[]) { #ifdef myDisableZLib printMessage("Compression requires 'pigz' to be installed http://macappstore.org/pigz/\n"); #else //myUseZLib - printMessage("Compression will be faster with 'pigz' installed http://macappstore.org/pigz/\n"); + if (opts->isVerbose > 0) + printMessage("Compression will be faster with 'pigz' installed http://macappstore.org/pigz/\n"); #endif #else //if APPLE else ... #ifdef myDisableZLib printMessage("Compression requires 'pigz' to be installed\n"); #else //myUseZLib - printMessage("Compression will be faster with 'pigz' installed\n"); + if (opts->isVerbose > 0) + printMessage("Compression will be faster with 'pigz' installed\n"); #endif #endif return; @@ -5643,7 +6250,9 @@ void readFindPigz (struct TDCMopts *opts, const char * argv[]) { 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,""); +#ifndef USING_R readFindPigz(opts, argv); +#endif #ifdef myEnableJasper opts->compressFlag = kCompressYes; //JASPER for JPEG2000 #else @@ -5660,7 +6269,7 @@ void setDefaultOpts (struct TDCMopts *opts, const char * argv[]) { //either "set opts->isOnlySingleFile = false; //convert all files in a directory, not just a single file opts->isOneDirAtATime = false; opts->isRenameNotConvert = false; - opts->isForceStackSameSeries = false; + opts->isForceStackSameSeries = 2; //automatic: stack CTs, do not stack MRI opts->isForceStackDCE = true; opts->isIgnoreDerivedAnd2D = false; opts->isPhilipsFloatNotDisplayScaling = true; @@ -5671,6 +6280,7 @@ void setDefaultOpts (struct TDCMopts *opts, const char * argv[]) { //either "set opts->isPipedGz = false; //e.g. pipe data directly to pigz instead of saving uncompressed to disk opts->isSave3D = false; opts->dirSearchDepth = 5; + opts->isProgress = 0; opts->nameConflictBehavior = kNAME_CONFLICT_ADD_SUFFIX; #ifdef myDisableZLib opts->gzLevel = 6; diff --git a/console/nii_dicom_batch.h b/console/nii_dicom_batch.h index 08aa290a..c9880e02 100644 --- a/console/nii_dicom_batch.h +++ b/console/nii_dicom_batch.h @@ -17,6 +17,7 @@ extern "C" { #ifdef USING_R struct TDicomSeries { + std::string name; TDICOMdata representativeData; std::vector files; }; @@ -29,8 +30,8 @@ extern "C" { #define MAX_NUM_SERIES 16 struct TDCMopts { - bool isSaveNRRD, isOneDirAtATime, isRenameNotConvert, isMaximize16BitRange, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isForceStackSameSeries, isRotate3DAcq, isCrop; - int nameConflictBehavior, isVerbose, compressFlag, dirSearchDepth, gzLevel; //support for compressed data 0=none, + bool isSaveNRRD, isOneDirAtATime, isRenameNotConvert, isMaximize16BitRange, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isRotate3DAcq, isCrop; + int isForceStackSameSeries, nameConflictBehavior, isVerbose, isProgress, 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; @@ -38,6 +39,11 @@ extern "C" { bool isScanOnly; void *imageList; std::vector series; + + // Used when sorting a directory + std::vector sourcePaths; + std::vector targetPaths; + std::vector ignoredPaths; #endif }; void saveIniFile (struct TDCMopts opts); diff --git a/console/print.h b/console/print.h index 4b9f43cc..b73d2f1c 100644 --- a/console/print.h +++ b/console/print.h @@ -11,9 +11,10 @@ #ifdef USING_R #define R_USE_C99_IN_CXX #include - #define printMessage(...) { Rprintf("[dcm2niix info] "); Rprintf(__VA_ARGS__); } - #define printWarning(...) { Rprintf("[dcm2niix WARNING] "); Rprintf(__VA_ARGS__); } - #define printError(...) { Rprintf("[dcm2niix ERROR] "); Rprintf(__VA_ARGS__); } + #define printMessage(...) do { Rprintf("[dcm2niix info] "); Rprintf(__VA_ARGS__); } while (0) + #define printWarning(...) do { Rprintf("[dcm2niix WARNING] "); Rprintf(__VA_ARGS__); } while (0) + #define printError(...) do { Rprintf("[dcm2niix ERROR] "); Rprintf(__VA_ARGS__); } while (0) + #define printError(frac) do { Rprintf("[dcm2niix PROGRESS] %g", frac); } while (0) #else #ifdef myUseCOut //for piping output to Qtextedit @@ -32,10 +33,12 @@ delete[] buf; } #define printError(...) do { printMessage("Error: "); printMessage(__VA_ARGS__);} while(0) + #define printProgress(frac) do { printMessage("Progress: %g\n", frac);} while(0) #else #include #define printMessage printf //#define printMessageError(...) fprintf (stderr, __VA_ARGS__) + #define printProgress(frac) do { printMessage("Progress: %g\n", frac);} while(0) #ifdef myErrorStdOut //for XCode MRIcro project, pipe errors to stdout not stderr #define printError(...) do { printMessage("Error: "); printMessage(__VA_ARGS__);} while(0) #else diff --git a/dcm_qa_nih b/dcm_qa_nih index 3830f57e..de8008ba 160000 --- a/dcm_qa_nih +++ b/dcm_qa_nih @@ -1 +1 @@ -Subproject commit 3830f57e0d252b139b693c0a2ad97dda81b341bd +Subproject commit de8008ba85f359d91b188106920908d39e54f422 diff --git a/dcm_qa_uih b/dcm_qa_uih index 215e5171..4917be51 160000 --- a/dcm_qa_uih +++ b/dcm_qa_uih @@ -1 +1 @@ -Subproject commit 215e5171a10046b3899885c0ff1e733e2c5f2423 +Subproject commit 4917be515cc76d21ca7148266b49b0086f1026f1