From e973e0ff5ba5ddde73bb2b95088a9fadc2f4997b Mon Sep 17 00:00:00 2001 From: Chris Rorden Date: Tue, 1 Jun 2021 08:21:26 -0400 Subject: [PATCH 01/20] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1f764e62..a69ea02e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,21 +8,33 @@ assignees: '' --- **Describe the bug** + A clear and concise description of what the bug is. -**To Reproduce** +**To reproduce** + Steps to reproduce the behavior: 1. Run the command 'dcm2niix ...' 2. See error ... **Expected behavior** + A clear and concise description of what you expected to happen. -**Output Log** +**Output log** + If applicable, output generated converting data. -** Version (please complete the following information):** +**Version** + +Please report the complete version string: + - dcm2niix version string, e.g. `dcm2niiX version v1.0.20201207 Clang12.0.0 ARM (64-bit MacOS)` The version string is always the first line generated when dcm2niix is run. + +**Troubleshooting** + +Please try the following steps to resolve your issue: + - Is this the [latest stable release](https://github.com/rordenlab/dcm2niix/releases)? If not, does the latest stable release resolve your issue? - If the latest stable version fails, and you are using Windows. Does the latest commit on the development branch resolve your issue? You can get a pre-compiled version from [AppVeyor](https://ci.appveyor.com/project/neurolabusc/dcm2niix) (click on the Artifacts button). - If the latest stable version fails, and you are using macOS or Linux. Does the latest commit on the development branch resolve your issue? You can build this using the recipe below: From 3d01b54b1047224ea1dac13d9d49c8ee0d6c2aef Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 9 Sep 2021 15:08:35 -0400 Subject: [PATCH 02/20] Set repetitionTimePreparation for known ASL sequences, reduce verbosity of slice re-ordering, improve ECAT success/failure warnings. --- Philips/README.md | 4 +- README.md | 4 ++ console/main_console.cpp | 1 + console/nii_dicom.cpp | 20 ++++-- console/nii_dicom.h | 3 +- console/nii_dicom_batch.cpp | 136 +++++++++++++++++++++++++++--------- console/nii_foreign.cpp | 6 +- 7 files changed, 130 insertions(+), 44 deletions(-) diff --git a/Philips/README.md b/Philips/README.md index c79c40d1..56889d55 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -77,7 +77,7 @@ The DICOM standard does not require that the [Instance Number (0020,0013)](http: ## Arterial Spin Labelling -Details and sample datasets for Philips Arterial Spin Labeling (ASL) are provided with the [dcm_qa_philips_asl](https://github.com/neurolabusc/dcm_qa_philips_asl) (classic DICOM) and [dcm_qa_philips_asl_enh](https://github.com/neurolabusc/dcm_qa_philips_asl) (enhanced DICOM) repositories. +Details and sample datasets for Philips Arterial Spin Labeling (ASL) are provided with the [dcm_qa_philips_asl](https://github.com/neurolabusc/dcm_qa_philips_asl) (classic DICOM) and [dcm_qa_philips_asl_enh](https://github.com/neurolabusc/dcm_qa_philips_asl) (enhanced DICOM) repositories. dcm2niix v1.0.20210819 and later will attempt to store volumes in [temporal order](https://github.com/rordenlab/dcm2niix/issues/533) regardless of whether the data is acquired as classic or enhanced DICOM. The [BIDS BEP005](https://bids.neuroimaging.io/get_involved) requires ASL sequences to report `PostLabelingDelay` with respect to the first slice of the volume. Curiously, Philips reports label delay independently for each slice (using DICOM tags 0020,9153; 0018,1060). In theory, this might allow a method to infer slice timing (though details like descending acquisitions may complicate these methods). ## Derived parametric maps stored with raw diffusion data @@ -140,6 +140,8 @@ MyCustomDirections 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. +Research users may want to explore the direct NIfTI export provided by Philips. This tool may have access to sequence information not provided in the DICOM export. However, this [manufacturer provided NIfTI export](https://github.com/rordenlab/dcm2niix/issues/529) is limited to certain image types and sequences and does not support features like FSL format diffusion bvec/bvals or BIDS sidecars. + [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 or the [sequence PDF](http://adni.loni.usc.edu/wp-content/uploads/2017/09/ADNI-3-Basic-Philips-R5.pdf). For precise timing, it is also worth knowing if equidistant "temporal slice spacing" is set and whether "prospect. motion corr." is on or off (if on, a short delay occurs between volumes). 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 7ca3a5f3..3e290923 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ dcm2niix is designed to convert neuroimaging data from the DICOM format to the NIfTI format. This web page hosts the developmental source code - a compiled version for Linux, MacOS, and Windows of the most recent stable release is included with [MRIcroGL](https://www.nitrc.org/projects/mricrogl/). A full manual for this software is available in the form of a [NITRC wiki](http://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage). +The DICOM format is the standard image format generated by modern medical imaging devices. However, DICOM is very [complicated](https://github.com/jonclayden/divest) and has been interpreted differently by different vendors. The NIfTI format is popular with scientists, it is very simple and explicit. However, this simplicity also imposes limitations (e.g. it demands equidistant slices). dcm2niix is also able to generate a [BIDS JSON format](https://bids-specification.readthedocs.io/en/stable/) `sidecar` which includes relevant information for brain scientists in a vendor agnostic and human readable form. +The [Neuroimaging DICOM and NIfTI Primer]https://github.com/DataCurationNetwork/data-primers/blob/master/Neuroimaging%20DICOM%20and%20NIfTI%20Data%20Curation%20Primer/neuroimaging-dicom-and-nifti-data-curation-primer.md) provides details. + ## License This software is open source. The bulk of the code is covered by the BSD license. Some units are either public domain (nifti*.*, miniz.c) or use the MIT license (ujpeg.cpp). See the license.txt file for more details. @@ -102,6 +105,7 @@ If you have any problems with the cmake build script described above or want to ## Alternatives + - [BIDS-converter](https://github.com/openneuropet/BIDS-converter) hosts Matlab and Python scripts for PET images, supporting DICOM and ECAT (ecat2nii) formats. - [dcm2nii](https://people.cas.sc.edu/rorden/mricron/dcm2nii.html) is the predecessor of dcm2niix. It is deprecated for modern images, but does handle image formats that predate DICOM (proprietary Elscint, GE and Siemens formats). - Python [dcmstack](https://github.com/moloney/dcmstack) DICOM to Nifti conversion with meta data preservation. - [dicm2nii](http://www.mathworks.com/matlabcentral/fileexchange/42997-dicom-to-nifti-converter) is written in Matlab. The Matlab language makes this very scriptable. diff --git a/console/main_console.cpp b/console/main_console.cpp index 68d44f7d..1f25b041 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -158,6 +158,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { #endif printf(" --big-endian : byte order (y/n/o, default o) [y=big-end, n=little-end, o=optimal/native]\n"); printf(" --progress : Slicer format progress information (y/n, default n)\n"); + printf(" --ignore_trigger_times : disregard values in 0018,1060 and 0020,9153\n"); printf(" --terse : omit filename post-fixes (can cause overwrites)\n"); printf(" --version : report version\n"); printf(" --xml : Slicer format features\n"); diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index d94aa71b..cadd048b 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4654,9 +4654,12 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); dcmDim[numDimensionIndexValues].RWVScale = d.RWVScale; dcmDim[numDimensionIndexValues].RWVIntercept = d.RWVIntercept; //printf("%d %d %g????\n", isTriggerSynced, isProspectiveSynced, d.triggerDelayTime); - if ((isASL) || (d.aslFlags != kASL_FLAG_NONE)) d.triggerDelayTime = 0.0; //see dcm_qa_philips_asl - if ((d.manufacturer == kMANUFACTURER_PHILIPS) && ((!isTriggerSynced) || (!isProspectiveSynced)) ) //issue408 - d.triggerDelayTime = 0.0; + //TODO533: isKludgeIssue533 alias Philips ASL as FrameDuration? + //if ((d.triggerDelayTime > 0.0) && (d.manufacturer == kMANUFACTURER_PHILIPS) && (d.aslFlags != kASL_FLAG_NONE)) + //printf(">>>%g\n", d.triggerDelayTime); + //if ((isASL) || (d.aslFlags != kASL_FLAG_NONE)) d.triggerDelayTime = 0.0; //see dcm_qa_philips_asl + //if ((d.manufacturer == kMANUFACTURER_PHILIPS) && ((!isTriggerSynced) || (!isProspectiveSynced)) ) //issue408 + // d.triggerDelayTime = 0.0; if (isSameFloat(MRImageDynamicScanBeginTime * 1000.0, d.triggerDelayTime) ) dcmDim[numDimensionIndexValues].triggerDelayTime = 0.0; //issue395 else @@ -7234,8 +7237,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); bool isScaleVaries = false; //setting j = 1 in next few lines is a hack, just in case TE/scale/intercept listed AFTER dimensionIndexValues int j = 0; - if (d.xyzDim[3] > 1) - j = 1; + //if (d.xyzDim[3] > 1) //not sure why this was included, disrupts Philips multiPLD ASL where each slice has different triggertime + // j = 1; for (int i = 0; i < d.xyzDim[4]; i++) { int slice = j + (i * d.xyzDim[3]); //dti4D->gradDynVol[i] = 0; //only PAR/REC @@ -7328,10 +7331,13 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); strcpy(d.seriesInstanceUID, d.studyInstanceUID); d.seriesUidCrc = mz_crc32X((unsigned char *)&d.protocolName, strlen(d.protocolName)); } - if ((d.manufacturer == kMANUFACTURER_PHILIPS) && ((!isTriggerSynced) || (!isProspectiveSynced)) ) //issue408 - d.triggerDelayTime = 0.0; //Philips ASL use "(0018,9037) CS [NONE]" but "(2001,1010) CS [TRIGGERED]", a situation not described in issue408 + //TODO533: alias Philips ASL PLD as frameDuration? isKludgeIssue533 + //if ((d.manufacturer == kMANUFACTURER_PHILIPS) && ((!isTriggerSynced) || (!isProspectiveSynced)) ) //issue408 + // d.triggerDelayTime = 0.0; //Philips ASL use "(0018,9037) CS [NONE]" but "(2001,1010) CS [TRIGGERED]", a situation not described in issue408 if (isSameFloat(MRImageDynamicScanBeginTime * 1000.0, d.triggerDelayTime)) //issue395 d.triggerDelayTime = 0.0; + if (d.phaseNumber > 0) //Philips TurboQUASAR set this uniquely for each slice + d.triggerDelayTime = 0.0; //printf("%d\t%g\t%g\t%g\n", d.imageNum, d.acquisitionTime, d.triggerDelayTime, MRImageDynamicScanBeginTime); if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (strlen(seriesTimeTxt) > 1) && (d.isXA10A) && (d.xyzDim[3] == 1) && (d.xyzDim[4] < 2)) { //printWarning(">>Ignoring series number of XA data saved as classic DICOM (issue 394)\n"); diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 105b4937..9fbb8e32 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210819" +#define kDCMdate "v1.0.20210909" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic @@ -122,6 +122,7 @@ static const int kMaxDTI4D = kMaxSlice2D; //issue460: maximum number of DTI dire #define kEXIT_SOME_OK_SOME_BAD 8 #define kEXIT_RENAME_ERROR 9 #define kEXIT_INCOMPLETE_VOLUMES_FOUND 10 //issue 515 +#define kEXIT_NOMINAL 11 //did not expect to convert files //0043,10A3 ---: PSEUDOCONTINUOUS //0043,10A4 ---: 3D pulsed continuous ASL technique diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index c69f6b0c..4ee465e9 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -348,8 +348,9 @@ void siemensPhilipsCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI vx[i].V[v] = 0.0f; //remove sign from values that are virtually zero } #ifndef USING_R - for (int i = 0; i < 3; i++) - printf("%g = %g %g %g\n", vx[i].V[0], vx[i].V[1], vx[i].V[2], vx[i].V[3]); + //simple diagnostics for data prior to realignment: useful as first direction is the same for al Philips sequences + //for (int i = 0; i < 3; i++) + // printf("%g = %g %g %g\n", vx[i].V[0], vx[i].V[1], vx[i].V[2], vx[i].V[3]); #endif return; } //https://github.com/rordenlab/dcm2niix/issues/225 @@ -1376,7 +1377,10 @@ tse3d: T2*/ fprintf(fp, ",\n"); if (dti4D->frameDuration[i] < 0) break; - fprintf(fp, "\t\t%g", dti4D->frameDuration[i] / 1000.0); // from 0018,1242 ms -> sec + if ((isSameFloatGE(dti4D->frameDuration[i], 0.0)) && (d.TR > 0.0)) + fprintf(fp, "\t\t%g", d.TR / 1000.0); // from 0018,1242 ms -> sec + else + fprintf(fp, "\t\t%g", dti4D->frameDuration[i] / 1000.0); // from 0018,1242 ms -> sec } fprintf(fp, "\t],\n"); } @@ -1399,7 +1403,8 @@ tse3d: T2*/ if ((d.TE > 0.0) && (!d.isXRay)) fprintf(fp, "\t\"EchoTime\": %g,\n", d.TE / 1000.0); //if ((d.TE2 > 0.0) && (!d.isXRay)) fprintf(fp, "\t\"EchoTime2\": %g,\n", d.TE2 / 1000.0 ); - json_Float(fp, "\t\"RepetitionTime\": %g,\n", d.TR / 1000.0); + if (dti4D->frameDuration[0] < 0.0) //e.g. PET scans can have variable TR + json_Float(fp, "\t\"RepetitionTime\": %g,\n", d.TR / 1000.0); json_Float(fp, "\t\"RepetitionTimeExcitation\": %g,\n", dti4D->repetitionTimeExcitation); json_Float(fp, "\t\"RepetitionTimeInversion\": %g,\n", dti4D->repetitionTimeInversion); json_Bool(fp, "\t\"MTState\": %s,\n", d.mtState); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP] @@ -1428,6 +1433,7 @@ tse3d: T2*/ fprintf(fp, "\t\"PhaseEncodingPolarityGE\": \"Flipped\",\n"); } float delayTimeInTR = -0.01; + float repetitionTimePreparation = 0.0; #ifdef myReadAsciiCsa if ((d.manufacturer == kMANUFACTURER_SIEMENS) && (d.CSA.SeriesHeader_offset > 0) && (d.CSA.SeriesHeader_length > 0)) { float pf = 1.0f; //partial fourier @@ -1471,6 +1477,7 @@ tse3d: T2*/ //ASL specific tags - 2D pCASL Danny J.J. Wang http://www.loft-lab.org if ((strstr(pulseSequenceDetails, "ep2d_pcasl")) || (strstr(pulseSequenceDetails, "ep2d_pcasl_UI_PHC"))) { isPCASL = true; + repetitionTimePreparation = d.TR; 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]); @@ -1481,6 +1488,7 @@ tse3d: T2*/ //ASL specific tags - 3D pCASL Danny J.J. Wang http://www.loft-lab.org if (strstr(pulseSequenceDetails, "tgse_pcasl")) { isPCASL = true; + repetitionTimePreparation = d.TR; 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 @@ -1509,6 +1517,7 @@ tse3d: T2*/ if (strstr(pulseSequenceDetails, "to_ep2d_VEPCASL")) { //Oxford 2D pCASL isOxfordASL = true; isPCASL = true; + repetitionTimePreparation = d.TR; 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 json_Float(fp, "\t\"TagRFFlipAngle\": %g,\n", csaAscii.alFree[4]); @@ -1697,6 +1706,31 @@ tse3d: T2*/ //printf("PhaseLines=%d EchoTrainLength=%d SENSE=%g\n", d.phaseEncodingLines, d.echoTrainLength, d.accelFactPE); //n.b. we can not distinguish pF from SENSE/GRAPPA for UIH } #endif + // https://neurostars.org/t/repetitiontime-parameters-what-are-they-and-where-to-find-them/20020/6 + json_Float(fp, "\t\"RepetitionTimePreparation\": %g,\n", repetitionTimePreparation); + //Philips ASL specific tags, issue533 + //Philips ASL issue 533 + /* + //see dcm_qa_philips_asl: this works for the mulit-PLD sequence, but not for other sequences. Also beware that value varies per slice, so incorrect values for descending + if ( (d.aslFlags != kASL_FLAG_NONE) && (dti4D->triggerDelayTime[0] >= 0.0)) { //see BEP009 PET https://docs.google.com/document/d/1mqMLnxVdLwZjDd4ZiWFqjEAmOmfcModA_R535v3eQs0 + bool isMultiPLD = false; + for (int i = 0; i < h->dim[4]; i++) + if (dti4D->triggerDelayTime[i] != dti4D->triggerDelayTime[0]) + isMultiPLD = true; + if (isMultiPLD) { + fprintf(fp, "\t\"InitialPostLabelDelay\": [\n"); + for (int i = 0; i < h->dim[4]; i++) { + if (i != 0) + fprintf(fp, ",\n"); + if (dti4D->triggerDelayTime[i] < 0) + break; + fprintf(fp, "\t\t%g", dti4D->triggerDelayTime[i] / 1000.0); + } + fprintf(fp, "\t],\n"); + } else //if all delays are the same, write scalar + json_Float(fp, "\t\"InitialPostLabelDelay\": %g,\n", dti4D->triggerDelayTime[0] / 1000.0); + } + */ //GE ASL specific tags if (d.aslFlags & kASL_FLAG_GE_CONTINUOUS) fprintf(fp, "\t\"ASLContrastTechnique\": \"CONTINUOUS\",\n"); @@ -1888,13 +1922,18 @@ tse3d: T2*/ fprintf(fp, "\t],\n"); } //DICOM orientation and phase encoding: useful for 3D undistortion. Original DICOM values: DICOM not NIfTI space, ignores if 3D image re-oriented - fprintf(fp, "\t\"ImageOrientationPatientDICOM\": [\n"); - for (int i = 1; i < 7; i++) { - if (i != 1) - fprintf(fp, ",\n"); - fprintf(fp, "\t\t%g", d.orient[i]); + float mxOrient = 0.0; + for (int i = 1; i < 7; i++) + mxOrient = max(mxOrient, fabs(d.orient[i])); + if (! isSameFloatGE(mxOrient, 0.0)) { //if set + fprintf(fp, "\t\"ImageOrientationPatientDICOM\": [\n"); + for (int i = 1; i < 7; i++) { + if (i != 1) + fprintf(fp, ",\n"); + fprintf(fp, "\t\t%g", d.orient[i]); + } + fprintf(fp, "\t],\n"); } - fprintf(fp, "\t],\n"); json_Str(fp, "\t\"ImageOrientationText\": \"%s\",\n", d.imageOrientationText); if (d.phaseEncodingRC == 'C') fprintf(fp, "\t\"InPlanePhaseEncodingDirectionDICOM\": \"COL\",\n"); @@ -1911,14 +1950,16 @@ tse3d: T2*/ #ifndef USING_R void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char *filename) { - struct TDTI4D *dti4D; + struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); dti4D->sliceOrder[0] = -1; dti4D->volumeOnsetTime[0] = -1; dti4D->decayFactor[0] = -1; + dti4D->triggerDelayTime[0] = -1.0; dti4D->intenScale[0] = 0.0; dti4D->repetitionTimeExcitation = 0.0; dti4D->repetitionTimeInversion = 0.0; nii_SaveBIDSX(pathoutname, d, opts, h, filename, dti4D); + free(dti4D); } // nii_SaveBIDSX() #endif @@ -2578,20 +2619,27 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s maxPhase = max(maxPhase, dcmList[dcmSort[i].indx].phaseNumber); } bool isUseInstanceNumberForVolume = false; - if ((maxPhase == 1) && (minVol == maxVolNotADC) && (minInstance < maxInstance)) { + if ((d4 > 1) && (maxPhase == 1) && (minVol == maxVolNotADC) && (minInstance < maxInstance)) { printWarning("Volume number does not vary (0019,10A2; 0020,0100; 2005,1063; 2005,1413), assuming meaningful instance number (0020,0013).\n"); isUseInstanceNumberForVolume = true; } - printMessage("ranges volume %d..%d instance %d..%d\n", minVol, maxVolNotADC, minInstance, maxInstance); //TODO + bool isVerbose = (verbose > 1); //issue533 + if (isVerbose) + printMessage("Ranges volume %d..%d instance %d..%d\n", minVol, maxVolNotADC, minInstance, maxInstance); //TODO bool isASL = (dcmList[dcmSort[0].indx].aslFlags != kASL_FLAG_NONE); //we will renumber volumes for Philips ASL (Contrast/Label, phase) and DWI (derived trace) int minVolOut = kMaxDTI4D + 1; int maxVolOut = -1; + bool isUsePhaseForVol = false; + if ((!isASL) && (minVol == maxVol) && (maxPhase > 1)) isUsePhaseForVol = true;//e.g. TurboQUASAR + if (isVerbose) + printMessage("InstanceNumber\tPosition\tVolume\tRepeat\tASLlabel\tPhase\tTriggerTime\n"); for (int i = 0; i < nConvert; i++) { int vol = dcmList[dcmSort[i].indx].rawDataRunNumber; int rawvol = vol; int instance = dcmList[dcmSort[i].indx].imageNum; int phase = max(1, dcmList[dcmSort[i].indx].phaseNumber); + if (isUsePhaseForVol) vol = phase; int isAslLabel = dcmList[dcmSort[i].indx].aslFlags == kASL_FLAG_PHILIPS_LABEL; dx = intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[i].indx]); if (isASL) { @@ -2610,8 +2658,8 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s } if (isUseInstanceNumberForVolume) vol = instance; - //if (verbose > 1) //only report slice data for logorrheic verbosity - printMessage("instanceNumber %4d position %g volume %d repeat %d ASLlabel %d phase %d\n", instance, dx, vol, rawvol, isAslLabel, phase); + if (isVerbose) //only report slice data for logorrheic verbosity + printMessage("%d\t%g\t%d\t%d\t%d\t%d\t%g\n", instance, dx, vol, rawvol, isAslLabel, phase, dcmList[dcmSort[i].indx].triggerDelayTime); if (vol > kMaxDTI4D) //issue529 Philips derived Trace/ADC embedded into DWI vol = maxVol + 1; minVolOut = min(minVolOut, vol); @@ -3065,7 +3113,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts if (dcm.isHasMagnitude) strcat(outname, "Mag"); //Philips enhanced with BOTH phase and Magnitude in single file } - if ((isAddNamePostFixes) && (dcm.triggerDelayTime >= 1) && (dcm.manufacturer != kMANUFACTURER_GE)) { //issue 336 GE uses this for slice timing + if ((isAddNamePostFixes) && (dcm.aslFlags == kASL_FLAG_NONE) && (dcm.triggerDelayTime >= 1) && (dcm.manufacturer != kMANUFACTURER_GE)) { //issue 336 GE uses this for slice timing sprintf(newstr, "_t%d", (int)roundf(dcm.triggerDelayTime)); strcat(outname, newstr); } @@ -5501,7 +5549,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if (nConvert > 1) { //next: detect trigger time see example https://www.slicer.org/wiki/Documentation/4.4/Modules/MultiVolumeExplorer double triggerDx = dcmList[dcmSort[nConvert - 1].indx].triggerDelayTime - dcmList[indx0].triggerDelayTime; - if (triggerDx > 0.0) //issue 384 + if ((triggerDx > 0.0) && (dcmList[indx0].aslFlags == kASL_FLAG_NONE)) //issue 384, issue533 dcmList[indx0].triggerDelayTime = triggerDx; //next: determine gantry tilt if (dcmList[indx0].gantryTilt != 0.0f) @@ -5584,8 +5632,10 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d //next: detect variable inter-volume time https://github.com/rordenlab/dcm2niix/issues/184 //if ((nConvert > 1) && ((dcmList[indx0].modality == kMODALITY_PT)|| (opts.isForceOnsetTimes))) { if ((nConvert > 1) && ((dcmList[indx0].modality == kMODALITY_PT) || ((opts.isForceOnsetTimes) && (dcmList[indx0].manufacturer != kMANUFACTURER_GE)))) { - if (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_PHILIPS) + if (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_PHILIPS) { ensureSequentialSlicePositions(hdr0.dim[3], hdr0.dim[4], dcmSort, dcmList, opts.isVerbose); //issue529 + indx0 = dcmSort[0].indx; + } //printf("Bogo529\n"); return EXIT_SUCCESS; //note: GE 0008,0032 unreliable, see mb=6 data from sw27.0 20201026 //issue 407 @@ -5633,6 +5683,17 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d dcmList[indx0].TR = tr * 1000.0; //as msec } } + if (dcmList[indx0].aslFlags != kASL_FLAG_NONE) { //issue533 + int nTR = 0; + for (int i = 0; i < nConvert; i++) { + if (isSamePosition(dcmList[indx0], dcmList[dcmSort[i].indx])) { + dti4D->triggerDelayTime[nTR] = dcmList[dcmSort[i].indx].triggerDelayTime; + nTR += 1; + if (nTR >= kMaxDTI4D) + break; + } + } //for each volume + } //if ASL if ((maxtr - mintr) > toleranceSec) trVaries = true; if (trVaries) { @@ -5647,7 +5708,6 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d float trDiff = acquisitionTimeDifference(&dcmList[indx0], &dcmList[dcmSort[i].indx]); dti4D->volumeOnsetTime[nTR] = trDiff; dti4D->decayFactor[nTR] = dcmList[dcmSort[i].indx].decayFactor; - //printf("%d %g\n", i, dcmList[dcmSort[i].indx].decayFactor); nTR += 1; if (nTR >= kMaxDTI4D) break; @@ -6173,7 +6233,7 @@ int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmLi dti4D->gradDynVol[0] = 1; for (int i = 1; i < dcmList[indx].xyzDim[4]; i++) { for (int j = 0; j < i; j++) - if (isSameFloatGE(dti4D->triggerDelayTime[i], dti4D->triggerDelayTime[j]) && (dti4D->intenIntercept[i] == dti4D->intenIntercept[j]) && (dti4D->intenScale[i] == dti4D->intenScale[j]) && (dti4D->isReal[i] == dti4D->isReal[j]) && (dti4D->isImaginary[i] == dti4D->isImaginary[j]) && (dti4D->isPhase[i] == dti4D->isPhase[j]) && (dti4D->TE[i] == dti4D->TE[j])) + if (( (dcmList[indx].aslFlags != kASL_FLAG_NONE) || isSameFloatGE(dti4D->triggerDelayTime[i], dti4D->triggerDelayTime[j])) && (dti4D->intenIntercept[i] == dti4D->intenIntercept[j]) && (dti4D->intenScale[i] == dti4D->intenScale[j]) && (dti4D->isReal[i] == dti4D->isReal[j]) && (dti4D->isImaginary[i] == dti4D->isImaginary[j]) && (dti4D->isPhase[i] == dti4D->isPhase[j]) && (dti4D->TE[i] == dti4D->TE[j])) dti4D->gradDynVol[i] = dti4D->gradDynVol[j]; if (dti4D->gradDynVol[i] == 0) { series++; @@ -6210,6 +6270,8 @@ int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmLi dcmList[indx].intenScalePhilips = intenScalePhilips; dcmList[indx].RWVIntercept = RWVIntercept; dcmList[indx].RWVScale = RWVScale; + //for (int i = 0; i < dcmList[indx].xyzDim[4]; i++) //for each volume + // printf("%g<<triggerDelayTime[i]); for (int i = 0; i < dcmList[indx].xyzDim[4]; i++) { //for each volume if (dti4D->gradDynVol[i] == s) { //dti4D->gradDynVol[i] = s; @@ -6223,7 +6285,7 @@ int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmLi dcmList[indx].isHasPhase = dti4D->isPhase[i]; dcmList[indx].isHasReal = dti4D->isReal[i]; dcmList[indx].isHasImaginary = dti4D->isImaginary[i]; - dcmList[indx].triggerDelayTime = dti4D->triggerDelayTime[i]; + dcmList[indx].triggerDelayTime = dti4D->triggerDelayTime[i]; dcmList[indx].isHasMagnitude = false; dcmList[indx].echoNum = echoNum[i]; break; @@ -6471,7 +6533,7 @@ bool isSameSet(struct TDICOMdata d1, struct TDICOMdata d2, struct TDCMopts *opts *isMultiEcho = true; return false; } - if ((d1.triggerDelayTime != d2.triggerDelayTime) && (d1.manufacturer == kMANUFACTURER_PHILIPS)) { //issue 384 + if ((d1.triggerDelayTime != d2.triggerDelayTime) && (d1.manufacturer == kMANUFACTURER_PHILIPS) && (d1.aslFlags == kASL_FLAG_NONE)) { //issue 384 if (!warnings->triggerVaries) printMessage("Slices not stacked: trigger time varies\n"); warnings->triggerVaries = true; @@ -6577,7 +6639,8 @@ size_t fileBytes(const char *fname) { return fileLen; } //fileBytes() -void searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, int depth, struct TDCMopts *opts) { +int searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, int depth, struct TDCMopts *opts) { + int ret = kEXIT_NOMINAL; tinydir_dir dir; tinydir_open(&dir, path); while (dir.has_next) { @@ -6589,9 +6652,10 @@ void searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, i strcat(filename, path); strcat(filename, kFileSep); strcat(filename, file.name); - if ((file.is_dir) && (depth < maxDepth) && (file.name[0] != '.')) - searchDirForDICOM(filename, nameList, maxDepth, depth + 1, opts); - else if (!file.is_reg) //ignore files "." and ".." + if ((file.is_dir) && (depth < maxDepth) && (file.name[0] != '.')) { + int tmp = searchDirForDICOM(filename, nameList, maxDepth, depth + 1, opts); + if (tmp != kEXIT_NOMINAL) ret = tmp; //e.g. found ecat + } else if (!file.is_reg) //ignore files "." and ".." ; else if ((strlen(file.name) < 1) || (file.name[0] == '.')) ; //printMessage("skipping hidden file %s\n", file.name); @@ -6606,8 +6670,10 @@ void searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, i //printMessage("dcm %lu %s \n",nameList->numItems, filename); #ifndef USING_R } else { - if (fileBytes(filename) > 2048) - convert_foreign(filename, *opts); + if (fileBytes(filename) > 2048) { + int tmp = convert_foreign(filename, *opts); + if (tmp == EXIT_SUCCESS) ret = tmp; //e.g. found ecat + } #ifdef MY_DEBUG printMessage("Not a dicom:\t%s\n", filename); #endif @@ -6616,6 +6682,7 @@ void searchDirForDICOM(char *path, struct TSearchList *nameList, int maxDepth, i tinydir_next(&dir); } tinydir_close(&dir); + return ret; } // searchDirForDICOM() int removeDuplicates(int nConvert, struct TDCMsort dcmSort[]) { @@ -6917,6 +6984,7 @@ int reportProgress(int progressPct, float frac) { int nii_loadDirCore(char *indir, struct TDCMopts *opts) { struct TSearchList nameList; + int nConvertTotal = 0; #if defined(_WIN64) || defined(_WIN32) || defined(USING_R) nameList.maxItems = 24000; // larger requires more memory, smaller more passes #else //UNIX, not R @@ -6969,7 +7037,9 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { for (int i = 0; i < 2; i++) { nameList.str = (char **)malloc((nameList.maxItems + 1) * sizeof(char *)); //reserve one pointer (32 or 64 bits) per potential file nameList.numItems = 0; - searchDirForDICOM(indir, &nameList, opts->dirSearchDepth, 0, opts); + int ret = searchDirForDICOM(indir, &nameList, opts->dirSearchDepth, 0, opts); + if (ret == EXIT_SUCCESS) //e.g. converted ECAT + nConvertTotal++; if (nameList.numItems <= nameList.maxItems) break; freeNameList(nameList); @@ -6977,12 +7047,13 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { //printMessage("Second pass required, found %ld images\n", nameList.numItems); } if (nameList.numItems < 1) { - if (opts->dirSearchDepth > 0) + if ((opts->dirSearchDepth > 0) && (nConvertTotal < 1)) printError("Unable to find any DICOM images in %s (or subfolders %d deep)\n", indir, opts->dirSearchDepth); else //keep silent for dirSearchDepth = 0 - presumably searching multiple folders { }; free(nameList.str); //ignore compile warning - memory only freed on first of 2 passes + if (nConvertTotal > 0) return EXIT_SUCCESS; //e.g. converted ECAT return kEXIT_NO_VALID_FILES_FOUND; } } @@ -7000,7 +7071,6 @@ int nii_loadDirCore(char *indir, struct TDCMopts *opts) { struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); struct TDCMprefs prefs; opts2Prefs(opts, &prefs); - int nConvertTotal = 0; bool compressionWarning = false; bool convertError = false; bool isDcmExt = isExt(opts->filename, ".dcm"); // "%r.dcm" with multi-echo should generate "1.dcm", "1e2.dcm" @@ -7583,8 +7653,8 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe #else opts->gzLevel = MZ_DEFAULT_LEVEL; //-1; #endif - opts->isMaximize16BitRange = kMaximize16BitRange_False; //e.g. if INT16 image has range 0..500 scale to be 0..50000 with hdr.scl_slope = hdr.scl_slope * 0.01 - //opts->isMaximize16BitRange = kMaximize16BitRange_Raw; //future version will use this option as default: preserve UINT16 even if it can be losslessly converted to INT16 + //opts->isMaximize16BitRange = kMaximize16BitRange_False; //e.g. if INT16 image has range 0..500 scale to be 0..50000 with hdr.scl_slope = hdr.scl_slope * 0.01 + opts->isMaximize16BitRange = kMaximize16BitRange_Raw; //future version will use this option as default: preserve UINT16 even if it can be losslessly converted to INT16 opts->isFlipY = true; //false: images in raw DICOM orientation, true: image rows flipped to cartesian coordinates opts->isRGBplanar = false; //false for NIfTI (RGBRGB...), true for Analyze (RRR..RGGG..GBBB..B) opts->isCreateBIDS = true; diff --git a/console/nii_foreign.cpp b/console/nii_foreign.cpp index af277b83..5547a58c 100644 --- a/console/nii_foreign.cpp +++ b/console/nii_foreign.cpp @@ -443,9 +443,11 @@ int convert_foreign(const char *fn, struct TDCMopts opts) { return EXIT_FAILURE; char niiFilename[1024]; int ret = nii_createFilename(dcm, niiFilename, opts); - printMessage("Saving ECAT as '%s'\n", niiFilename); - if (ret != EXIT_SUCCESS) + if (ret != EXIT_SUCCESS) { + printError("Failed to save ECAT as '%s'\n", niiFilename); return ret; + } + printMessage("Saving ECAT as '%s'\n", niiFilename); //struct TDTI4D dti4D; //nii_SaveBIDS(niiFilename, dcm, opts, &dti4D, &hdr, fn); nii_SaveBIDS(niiFilename, dcm, opts, &hdr, fn); From a761412f9823423c314cb0d9fb4a55cb477911fe Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Fri, 10 Sep 2021 20:22:28 -0400 Subject: [PATCH 03/20] issue 539 (https://github.com/rordenlab/dcm2niix/issues/539) --- console/nii_dicom_batch.cpp | 19 +++++++++++--- docs/source/conf.py | 2 +- docs/source/dcm2niix.rst | 52 ++++++++++++++++++++++++++++--------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 4ee465e9..7253ec86 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -6022,7 +6022,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d #ifndef USING_R fflush(stdout); //show immediately if run from MRIcroGL GUI #endif - //~ if (!dcmList[dcmSort[0].indx].isSlicesSpatiallySequentialPhilips) + //~ if (!dcmList[dcmSort[0].indx].isSlicesSpatiallySequentialPhilips) //~ nii_reorderSlices(imgM, &hdr0, dti4D); //hdr0.pixdim[3] = dxNoTilt; if (hdr0.dim[3] < 2) @@ -6035,8 +6035,21 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d bool isFlipY = false; bool isSetOrtho = false; if ((opts.isRotate3DAcq) && (dcmList[dcmSort[0].indx].is3DAcq) && (!dcmList[dcmSort[0].indx].isEPI) && (hdr0.dim[3] > 1) && (hdr0.dim[0] < 4)) { - imgM = nii_setOrtho(imgM, &hdr0); - isSetOrtho = true; + bool isSliceEquidistant = true; //issue539 + if (nConvert > 0) { + float dx = sliceMMarray[1] - sliceMMarray[0]; + float thr = fabs(dx) * 0.1; + for (int i = 2; i < nConvert; i++) + if (fabs(dx- (sliceMMarray[i]-sliceMMarray[i-1])) > (thr) ) { + printWarning("Unable to rotate 3D volume: slices not equidistant: %g != %g\n", dx, sliceMMarray[i]-sliceMMarray[i-1]); + isSliceEquidistant = false; + break; + } + } + if (isSliceEquidistant) { + imgM = nii_setOrtho(imgM, &hdr0); + isSetOrtho = true; + } } else if (opts.isFlipY) { //(FLIP_Y) //(dcmList[indx0].CSA.mosaicSlices < 2) && imgM = nii_flipY(imgM, &hdr0); isFlipY = true; diff --git a/docs/source/conf.py b/docs/source/conf.py index e344633d..fadb0270 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -49,7 +49,7 @@ # General information about the project. project = u'dcm2niix' -copyright = u'2016 The dcm2niix contributors' +copyright = u'2021 The dcm2niix contributors' author = u'The dcm2niix contributors' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/source/dcm2niix.rst b/docs/source/dcm2niix.rst index 52bd9ee6..a5d15e00 100644 --- a/docs/source/dcm2niix.rst +++ b/docs/source/dcm2niix.rst @@ -26,10 +26,11 @@ Options -1..-9 gz compression level (1=fastest..9=smallest, default 6) --b Save additional BIDS metadata to a side-car .json file. +-b Save additional BIDS metadata to a side-car .json file (default y). The "i"nput-only option reads DICOMs but saves neither BIDS nor NIfTI. --ba anonymize BIDS +-ba anonymize BIDS (default y). + If "n"o, side-car may report patient name, age and weight. -f Format string for the output filename(s). The following specifiers are supported: @@ -41,10 +42,11 @@ Options - %e, echo number - %f, folder name - %i, patient ID - - %j, seriesInstanceUID - - %k, studyInstanceUID + - %j, series instance UID + - %k, study instance UID - %m, manufacturer - %n, patient name + - %o, media object instance UID - %p, protocol - %r, instance number (of 1st DICOM file) - %s, series number @@ -56,15 +58,28 @@ Options The default format string is "%p_%e_%4s". --i Ignore derived, localizer and 2D images. +-g Generate defaults file (default n) + If "y", create default file on completion + If "n", default will not be written + If "o", only reset and write defaults + If "i", the values of the defualts file are ignored + : reset defaults], default n) --l Losslessly scale 16-bit integers to use maximal dynamic range. +-h Show help --m Merge slices from the same series regardless of study time, - echo, coil, orientation, etc... +-i Ignore derived, localizer and 2D images (default n) --n Only convert this series number. Provide a negative number for - listing of series numbers in input folder. +-l Losslessly scale 16-bit integers to use maximal dynamic range (default o). + If "y", then intensity rescaled to use full 16-bit range. + If "n", data not scaled uint16 will be saved as int16. + If "o", original data and datatype preserved. + +-m Merge slices from the same series regardless of study time, + echo, coil, orientation, etc. (default 2). + If "2", automatic based on image modality. + +-n Only convert this series CRC number. Provide a negative number for + listing of series CRC numbers in input folder. -o Output directory where the converted files should be saved. If unspecified, the files are saved within the specified source @@ -80,15 +95,28 @@ Options -u Update check: attempts to see if newer version is available. --v Enable verbose output. "n" for succinct, "y" for verbose, "h" for +-v <2/y/n> Enable verbose output. "n" for succinct, "y" for verbose, "2" for high verbosity --x Crop images. This will attempt to remove excess neck from 3D acquisitions. +-x Crop images. This will attempt to remove excess neck from 3D acquisitions. + If "i", images are neither cropped nor rotated to canonical space. -z Desired compression method. The "y"es option uses the external program pigz if available. The "i" option compresses the image using the slower built-in compression routines. +--big-endian Byte order (default o). Optimal is machine native + +--progress Slicer format progress information (y/n, default n) + +--ignore_trigger_times Disregard values in 0018,1060 and 0020,9153 + +--terse Omit filename post-fixes (can cause overwrites) + +--version Report version and terminate + +--xml Slicer format features + Licensing --------- From 22c47148914f1a3bd8088b022795fd570114d9c9 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Fri, 10 Sep 2021 20:28:35 -0400 Subject: [PATCH 04/20] Never equalize slices if slices have been rotated --- console/nii_dicom_batch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 7253ec86..359bdf7f 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -6186,7 +6186,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d } else printMessage("Tilt correction skipped\n"); } - if (sliceMMarray != NULL) { + if ((sliceMMarray != NULL) && (!isSetOrtho)) { if (dcmList[indx0].isResampled) { printMessage("Slice thickness correction skipped: 0008,2111 reports RESAMPLED\n"); } else From 5b0eb89457fc17d17acaa63211b6aa46a23a8b82 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 11 Sep 2021 14:30:23 +0200 Subject: [PATCH 05/20] Typos found by codespell --- BIDS/README.md | 3 +-- COMPILE.md | 1 - CONTRIBUTE.md | 2 +- Canon/README.md | 4 +-- ERRORS.md | 1 - FILENAMING.md | 4 +-- GE/README.md | 3 ++- PARREC/README.md | 6 ++--- Philips/README.md | 10 +++---- README.md | 2 +- Siemens/README.md | 3 +-- SuperBuild/SuperBuild.cmake | 1 - Troubleshooting/README.md | 2 +- VERSIONS.md | 2 +- batch_config.yml | 2 +- console/jpg_0XC3.cpp | 2 +- console/main_console_batch.cpp | 2 +- console/makefile | 4 +-- console/nifti1_io_core.cpp | 7 ----- console/nii_dicom.cpp | 6 ++--- console/nii_dicom.h | 6 ++--- console/nii_dicom_batch.cpp | 48 +++++++++++++++++----------------- console/nii_ortho.h | 4 +-- cpfiles.command | 2 +- docs/source/dcm2niibatch.rst | 2 -- docs/source/dcm2niix.rst | 4 +-- docs/source/index.rst | 1 - license.txt | 2 +- rmfiles.command | 1 - 29 files changed, 60 insertions(+), 77 deletions(-) diff --git a/BIDS/README.md b/BIDS/README.md index 4be310d2..bf596b66 100644 --- a/BIDS/README.md +++ b/BIDS/README.md @@ -328,6 +328,5 @@ Fields specific to United Imaging Healthcare systems (e.g. uMR 770). | Field | Unit | Comments | Defined By | |--------------------------------|------|--------------------------|------------| -| BandwidthPerPixelPhaseEncode | Hz | DICOM tag 0019,1028 | B | +| BandwidthPerPixelPhaseEncode | Hz | DICOM tag 0019,1028 | B | | ParallelReductionFactorInPlane | | DICOM tag 0065,100D | B | - diff --git a/COMPILE.md b/COMPILE.md index fec61b75..b8bcc99b 100644 --- a/COMPILE.md +++ b/COMPILE.md @@ -159,4 +159,3 @@ Once the installation is completed, you can revert these changes: git config --global --unset-all url.https://github.com/.insteadof git config --global --unset-all url.https://.insteadof ``` - diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 2a1b59ef..dcb0e593 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -26,4 +26,4 @@ dcm2niix is written in C. Different programmers prefer different styles of inden ``` clang-format -i -style="{BasedOnStyle: LLVM, IndentWidth: 4, IndentCaseLabels: false, TabWidth: 4, UseTab: Always, ColumnLimit: 0}" *.cpp *.h -``` \ No newline at end of file +``` diff --git a/Canon/README.md b/Canon/README.md index 7ee79845..6a458856 100644 --- a/Canon/README.md +++ b/Canon/README.md @@ -17,7 +17,7 @@ Since the acquisition by Canon, these public tags are no longer populated for im (0020,4000) LT [b=1500(0.445,0.000,0.895)] # 26, 1 ImageComments ``` -In contrast, when exporting images as enhanced (4D) DICOM, information is stored in public tags and does appear to compensate for phase encode polarity. These coordinates are with respect to the scanner bore, not image space. A Canon classic DICOM DWI image may report: +In contrast, when exporting images as enhanced (4D) DICOM, information is stored in public tags and does appear to compensate for phase encode polarity. These coordinates are with respect to the scanner bore, not image space. A Canon classic DICOM DWI image may report: ``` (0018,9087) FD 1500 # 8, 1 DiffusionBValue @@ -36,4 +36,4 @@ The [BIDS format](https://bids.neuroimaging.io) can record several sequence prop - [Toshiba Aquilion](https://www.aliza-dicom-viewer.com/download/datasets). - [Toshiba 3T Galan Diffusion Dataset](https://github.com/neurolabusc/dcm_qa_toshiba). - - [Canon 3T Galan Diffusion Dataset](https://github.com/neurolabusc/dcm_qa_canon). \ No newline at end of file + - [Canon 3T Galan Diffusion Dataset](https://github.com/neurolabusc/dcm_qa_canon). diff --git a/ERRORS.md b/ERRORS.md index 89d270b3..57afb609 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -26,4 +26,3 @@ Below is a list of possible return values from running dcm2niix. | 7 | Unable to write to output folder (check file permissions) | | 8 | Converted some but not all of the input DICOMs | | 9 | Unable to rename files (result of `dcm2niix -r y ~/in`) | - diff --git a/FILENAMING.md b/FILENAMING.md index 7175588a..890796a0 100644 --- a/FILENAMING.md +++ b/FILENAMING.md @@ -38,7 +38,7 @@ In general dcm2niix creates images with 3D dimensions, or 4 dimensions when the - _cNx.._cNz where C* refers to the coil name (typically only seen for uncombined data, where a separate image is generated for each antenna) - _e1..eN echo number for multi-echo sequences - - _Eq is commonly seen in [CT scans](https://github.com/neurolabusc/dcm_qa_ct). For example, CT scans of the brain often have many slices closely packed near the brain stem and only a few slices spread far apart near the top of the head. Variable between-slice spacing is rarer in MRI, and if you see this from a MRI sequence you should ensure that [all of the acquired slices have been provided to dcm2niix](https://neurostars.org/t/field-mapping-siemens-scanners-dcm2niix-output-2-bids/2075/7). NIfTI asumes all 2D slices that form a 3D stack are equidistant. Therefore, dcm2niix reslices the input data to generate an equidistant volume. + - _Eq is commonly seen in [CT scans](https://github.com/neurolabusc/dcm_qa_ct). For example, CT scans of the brain often have many slices closely packed near the brain stem and only a few slices spread far apart near the top of the head. Variable between-slice spacing is rarer in MRI, and if you see this from a MRI sequence you should ensure that [all of the acquired slices have been provided to dcm2niix](https://neurostars.org/t/field-mapping-siemens-scanners-dcm2niix-output-2-bids/2075/7). NIfTI assumes all 2D slices that form a 3D stack are equidistant. Therefore, dcm2niix reslices the input data to generate an equidistant volume. - _ph phase map - _iN appended image number for non-parallel slices - _imaginary imaginary component of complex image @@ -87,4 +87,4 @@ dcm2niix will attempt to write your image using the naming scheme you specify wi [Control characters](https://en.wikipedia.org/wiki/ASCII#Control_characters) like backspace and tab are also forbidden. -Be warned that dcm2niix will copy all allowed characters verbatim, which can cause problems for some other tools. Consider this [sample dataset](https://github.com/neurolabusc/dcm_qa_nih/tree/master/In/20180918GE/mr_0004) where the DICOM Protocol Name (0018,1030) is 'Axial_EPI-FMRI_(Interleaved_I_to_S)'. The parentheses ("round brackets") may cause other tools issues. Consider converting this series with the command 'dcm2niix -f %s_%p ~/DICOM' to create the file '4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii'.If you now run the command 'fslhd 4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii' you will get the error '-bash: syntax error near unexpected token `(''. Therefore, it is often a good idea to use double quotes to specify the names of files. In this example 'fslhd "4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii"' will work correctly. \ No newline at end of file +Be warned that dcm2niix will copy all allowed characters verbatim, which can cause problems for some other tools. Consider this [sample dataset](https://github.com/neurolabusc/dcm_qa_nih/tree/master/In/20180918GE/mr_0004) where the DICOM Protocol Name (0018,1030) is 'Axial_EPI-FMRI_(Interleaved_I_to_S)'. The parentheses ("round brackets") may cause other tools issues. Consider converting this series with the command 'dcm2niix -f %s_%p ~/DICOM' to create the file '4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii'.If you now run the command 'fslhd 4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii' you will get the error '-bash: syntax error near unexpected token `(''. Therefore, it is often a good idea to use double quotes to specify the names of files. In this example 'fslhd "4_Axial_EPI-FMRI_(Interleaved_I_to_S).nii"' will work correctly. diff --git a/GE/README.md b/GE/README.md index 35c5bdef..0ac92e96 100644 --- a/GE/README.md +++ b/GE/README.md @@ -110,4 +110,5 @@ Anatomical localizers (e.g. scout images) are quick-and-dirty scans used to posi - [Slice Timing and Phase Encoding examples](https://github.com/jannikadon/cc-dcm2bids-wrapper/tree/main/dicom-qa-examples) - [Slice timing validation](https://github.com/neurolabusc/dcm_qa_stc) for different varieties of GE EPI sequences. - [Examples of phase encoding polarity, slice timing and diffusion gradients](https://github.com/neurolabusc/dcm_qa_ge). - - The dcm2niix [wiki](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage) includes examples of diffusion data, slice timing, and other variations. \ No newline at end of file + - The dcm2niix [wiki](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage) includes examples of diffusion data, slice timing, and other variations. + diff --git a/PARREC/README.md b/PARREC/README.md index fba3e5ee..a75c91ef 100644 --- a/PARREC/README.md +++ b/PARREC/README.md @@ -1,9 +1,9 @@ ## About -dcm2niix attempts to convert Philips PAR/REC format images to NIfTI. While this format remains popular with users, it is slowly being superceded by Philips enhanced DICOM format, an XML/REC format as well as the direct NIfTI export. Note that dcm2niix does not support the XML/REC format. +dcm2niix attempts to convert Philips PAR/REC format images to NIfTI. While this format remains popular with users, it is slowly being superseded by Philips enhanced DICOM format, an XML/REC format as well as the direct NIfTI export. Note that dcm2niix does not support the XML/REC format. -According to [Matthew Clemence](https://www.nitrc.org/forum/forum.php?thread_id=9319&forum_id=4703) DICOM (classic and enhanced) and XML/REC are supported in the base product, NIFTI forms part of a Neuroscience commercial option from release 5 onwards. PAR/REC requires a research agreement to obtain. For the two formats XML/REC and PAR/REC, the "REC" part is identical but instead of a plain text file of the "par" format, the same information is now available as an XML file. This descision has been taken to allow the information to be more easily extended as the PAR file was getting increasingly limited. +According to [Matthew Clemence](https://www.nitrc.org/forum/forum.php?thread_id=9319&forum_id=4703) DICOM (classic and enhanced) and XML/REC are supported in the base product, NIFTI forms part of a Neuroscience commercial option from release 5 onwards. PAR/REC requires a research agreement to obtain. For the two formats XML/REC and PAR/REC, the "REC" part is identical but instead of a plain text file of the "par" format, the same information is now available as an XML file. This decision has been taken to allow the information to be more easily extended as the PAR file was getting increasingly limited. ## Detecting, Reporting and Fixing the V4 Image offcentre Bug @@ -43,5 +43,3 @@ Note that for Philips (unlike DICOM) the For PAR/REC the acquisition (%u) and se ## dcm2niix Limitations Be aware that dcm2niix assumes that the data is stored in complete 3D volumes. It will not convert datasets where the scan is interrupted mid-volume (e.g. where the number of 2D slices is not divisible by the number of slices in a volume). This can occur if the user aborts a sequence part way through acquisition. If dcm2niix detects this situation it will suggest you use [dicm2nii](https://www.mathworks.com/matlabcentral/fileexchange/42997-dicom-to-nifti-converter--nifti-tool-and-viewer) which can handle these files. - - diff --git a/Philips/README.md b/Philips/README.md index 56889d55..2b6e6bc8 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -18,9 +18,9 @@ Therefore, dcm2niix will ignore the IPP enclosed in 2005,140F unless no alternat ## Image Scaling -How data is represented in DICOM for MR has several challenges and the technology and standard has evolved over the years to accommodate new uses. Unlike CT, where the signal is naturally displayed in Hounsfield units, MR has no natural signal units and the magnitude is influenced by the electronics and the software processing required to bring this to the final image. Secondly most of the original DICOM implementations used small bit number integers to store the underlying images for economy of storage. As a result it is necessary to apply scaling from the internal DICOM storage to a form suitable for radiographic display or quantitative measurement. There remain several challenges with this process, ensuring that the mapping to the integer values makes best use of the available bit depth for images with large dynamic range, or large changes between images, without clipping the data while also preserving the appearance of the noise field which is demanded by the needs of radiographic visual review. Note that for most MRI modalities these concerns do not impact analyses: the intensity is assumed arbitrary, the statistics treat signal offset and scaling as nuisance regressors when fitting models, and cacluations are computed with high precision floating point numbers. However, there are some situations such as arterial spin labeling where image scaling is important. In these situations, scaling is a crucial aspect to be aware of for quantitative methods and which representation is used depends upon your needs. +How data is represented in DICOM for MR has several challenges and the technology and standard has evolved over the years to accommodate new uses. Unlike CT, where the signal is naturally displayed in Hounsfield units, MR has no natural signal units and the magnitude is influenced by the electronics and the software processing required to bring this to the final image. Secondly most of the original DICOM implementations used small bit number integers to store the underlying images for economy of storage. As a result it is necessary to apply scaling from the internal DICOM storage to a form suitable for radiographic display or quantitative measurement. There remain several challenges with this process, ensuring that the mapping to the integer values makes best use of the available bit depth for images with large dynamic range, or large changes between images, without clipping the data while also preserving the appearance of the noise field which is demanded by the needs of radiographic visual review. Note that for most MRI modalities these concerns do not impact analyses: the intensity is assumed arbitrary, the statistics treat signal offset and scaling as nuisance regressors when fitting models, and calculations are computed with high precision floating point numbers. However, there are some situations such as arterial spin labeling where image scaling is important. In these situations, scaling is a crucial aspect to be aware of for quantitative methods and which representation is used depends upon your needs. -At its simplest image scaling requires a rescale slope and intercept defined by the DICOM standard tags [0028,1053](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)) and [0028,1052](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)). Whether these values are the same for all images, or image specific depends upon the implementation and potentially the location of these tags withing the DICOM tag structure. For manufacturers other than Philips, these are the only intensity scaling values provided, so there is no concern regarding which scaling values should be used. +At its simplest image scaling requires a rescale slope and intercept defined by the DICOM standard tags [0028,1053](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)) and [0028,1052](http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0028,1053)). Whether these values are the same for all images, or image specific depends upon the implementation and potentially the location of these tags within the DICOM tag structure. For manufacturers other than Philips, these are the only intensity scaling values provided, so there is no concern regarding which scaling values should be used. However, the DICOM standard introduced the concept of [`real world units`](http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_A.46.html). This allows the storage of one or more mappings to allow selective viewing of the data mapped into different value ranges (which may also be non-linear mappings). @@ -33,7 +33,7 @@ Philips thinks in terms of three different representations (using the terminolog | Floating Point | FP | An internal value at a point earlier in the reconstruction chain before the conversion to DICOM/integer for image presentation. | | Real World Value | WV | DICOM defined real world units| -In general SV should not be used for quantitative measurements as it is an integer format. In practice, if the Rescale values are the same for all images (the typical case, but not guaranteed) SV can be used to compare signal intensities between images from the same scan. Note that the NIfTI format only provides a single `scl_slope` and `scl_inter` for the entire file, whereas in DICOM rescale values can in theory differ across 2D slices. Therefore, in situations where the rescale values do differ across slices, dcm2niix will apply the requested rescale to each slice and save the scaled data as the 32-bit float NIfTI dataset. This preserves the varibility reported by the rescale tags, at the cost of disk space. +In general SV should not be used for quantitative measurements as it is an integer format. In practice, if the Rescale values are the same for all images (the typical case, but not guaranteed) SV can be used to compare signal intensities between images from the same scan. Note that the NIfTI format only provides a single `scl_slope` and `scl_inter` for the entire file, whereas in DICOM rescale values can in theory differ across 2D slices. Therefore, in situations where the rescale values do differ across slices, dcm2niix will apply the requested rescale to each slice and save the scaled data as the 32-bit float NIfTI dataset. This preserves the variability reported by the rescale tags, at the cost of disk space. DV can be used for quantitative comparison of signal intensities between images in the same scan as long as the relevant rescale values are taken into account. These rescale values may come from the tags standard tags 0028,1053 and 0028,1052 or from a relevant RealWorld block if present. If the DV is derived from a RealWorld block with defined units (tag (0008,0104) such as Hz or ms rather than “no units”) or a RescaleType (0028,1054) with a non-US type (not defined by the standard), then the DV is already quantitative and cross scan comparison may be done. @@ -146,7 +146,7 @@ Research users may want to explore the direct NIfTI export provided by Philips. 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). -Another value desirable for TOPUP is the "TotalReadoutTime". Again, one can not confidently calculate this from Philips DICOMs (though on can [appoximate it if you make a few assumptions](https://github.com/nipreps/sdcflows/issues/5)). If you do decide to calculate this using values from the MRI console, be aware that the [FSL definition](https://github.com/rordenlab/dcm2niix/issues/130) is not intuitive for scans with interpolation, partial Fourier, parallel imaging, etc. However, it should be pointed out that the "TotalReadoutTime" only influences TOPUP's calibrated validation images that are typically ignored. The data used in subsequent steps will not be influenced by this value. +Another value desirable for TOPUP is the "TotalReadoutTime". Again, one can not confidently calculate this from Philips DICOMs (though on can [approximate it if you make a few assumptions](https://github.com/nipreps/sdcflows/issues/5)). If you do decide to calculate this using values from the MRI console, be aware that the [FSL definition](https://github.com/rordenlab/dcm2niix/issues/130) is not intuitive for scans with interpolation, partial Fourier, parallel imaging, etc. However, it should be pointed out that the "TotalReadoutTime" only influences TOPUP's calibrated validation images that are typically ignored. The data used in subsequent steps will not be influenced by this value. ## Partial Volumes @@ -178,4 +178,4 @@ Prior versions of dcm2niix used different methods to sort images. However, these - [Diffusion Examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging) - [Additional Diffusion Examples](https://github.com/neurolabusc/dcm_qa_philips) - Classic and enhanced [ASL Examples](https://github.com/neurolabusc/dcm_qa_philips_asl) - - [Enhanced DICOMs](https://github.com/neurolabusc/dcm_qa_enh) \ No newline at end of file + - [Enhanced DICOMs](https://github.com/neurolabusc/dcm_qa_enh) diff --git a/README.md b/README.md index 3e290923..f34ce206 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ dcm2niix is developed by the community for the community and everybody can becom ## Running -Command line usage is described in the [NITRC wiki](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#General_Usage). The minimal command line call would be `dcm2niix /path/to/dicom/folder`. However, you may want to invoke additional options, for example the call `dcm2niix -z y -f %p_%t_%s -o /path/ouput /path/to/dicom/folder` will save data as gzip compressed, with the filename based on the protocol name (%p) acquisition time (%t) and DICOM series number (%s), with all files saved to the folder "output". For more help see help: `dcm2niix -h`. +Command line usage is described in the [NITRC wiki](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#General_Usage). The minimal command line call would be `dcm2niix /path/to/dicom/folder`. However, you may want to invoke additional options, for example the call `dcm2niix -z y -f %p_%t_%s -o /path/output /path/to/dicom/folder` will save data as gzip compressed, with the filename based on the protocol name (%p) acquisition time (%t) and DICOM series number (%s), with all files saved to the folder "output". For more help see help: `dcm2niix -h`. [See the BATCH.md file for instructions on using the batch processing version](./BATCH.md). diff --git a/Siemens/README.md b/Siemens/README.md index 51975e7c..ec98c853 100644 --- a/Siemens/README.md +++ b/Siemens/README.md @@ -10,7 +10,7 @@ The DICOM images exported by the X-series is radically different than the V-seri X-series users are strongly encouraged to export data using the "Enhanced" format and to not use any of the "Anonymize" features on the console. The consequences of these options is discussed in detail in [issue 236](https://github.com/rordenlab/dcm2niix/issues/236). Siemens notes `We highly recommend that the Enhanced DICOM format be used. This is because this format retains far more information in the header`. Failure to export data in this format has led to catastrophic data loss for numerous users (for publicly reported details see issues [203](https://github.com/rordenlab/dcm2niix/issues/203), [236](https://github.com/rordenlab/dcm2niix/issues/236), [240](https://github.com/rordenlab/dcm2niix/issues/240), [274](https://github.com/rordenlab/dcm2niix/issues/274), [303](https://github.com/rordenlab/dcm2niix/issues/303), [370](https://github.com/rordenlab/dcm2niix/issues/370), [394](https://github.com/rordenlab/dcm2niix/issues/394)). This reflects limitations of the DICOM data, not dcm2niix. -While X-series consoles allow users to export data as enhanced, mosaic or classic 2D formats, choosing an option other than enhanced dramatically degrades the meta data. Note that the Siemens considers mosaic images `secondary capture` data intended for quality assurance only. The mosaic scans lack several "Type 1" DICOM properties, necessarily limiting conversion. The non-mosaic 2D enhanced DICOMs are compact and efficient, but appear to have limited details relative to the enhanced output. This is unfortunate, as for the V-series the mosaic format has major benefits, so users may be in the habit of prefering mosaic export. Finally, each of the formats (enhanced, mosaic, classic) can be exported as anonymized. The Siemens console anonymization of current XA10A (Fall 2018) strips many useful tags. Siemens suggests `the use an offline/in-house anonymization software instead`. Another limitation of the current X-series format is that it retains no versioning details beyond the minor version for software and hardware stepping (e.g. versions are merely XA10 or XA11 with no details for service packs). If you use a X-series, you are strongly encouraged to manually log every hardware or software upgrade to allow future analyses to identify and regress out any effects of these modifications. Since the X-series format does not have a CSA header, dcm2niix will attempt to use the new private DICOM tags to populate the BIDS file. These tags are described in [issue 240](https://github.com/rordenlab/dcm2niix/issues/240). +While X-series consoles allow users to export data as enhanced, mosaic or classic 2D formats, choosing an option other than enhanced dramatically degrades the meta data. Note that the Siemens considers mosaic images `secondary capture` data intended for quality assurance only. The mosaic scans lack several "Type 1" DICOM properties, necessarily limiting conversion. The non-mosaic 2D enhanced DICOMs are compact and efficient, but appear to have limited details relative to the enhanced output. This is unfortunate, as for the V-series the mosaic format has major benefits, so users may be in the habit of preferring mosaic export. Finally, each of the formats (enhanced, mosaic, classic) can be exported as anonymized. The Siemens console anonymization of current XA10A (Fall 2018) strips many useful tags. Siemens suggests `the use an offline/in-house anonymization software instead`. Another limitation of the current X-series format is that it retains no versioning details beyond the minor version for software and hardware stepping (e.g. versions are merely XA10 or XA11 with no details for service packs). If you use a X-series, you are strongly encouraged to manually log every hardware or software upgrade to allow future analyses to identify and regress out any effects of these modifications. Since the X-series format does not have a CSA header, dcm2niix will attempt to use the new private DICOM tags to populate the BIDS file. These tags are described in [issue 240](https://github.com/rordenlab/dcm2niix/issues/240). When creating enhanced DICOMs diffusion information is provided in public tags. Based on a limited sample, it seems that classic DICOMs do not store diffusion data for XA10, and use private tags for [XA11](https://www.nitrc.org/forum/forum.php?thread_id=10013&forum_id=4703). @@ -114,4 +114,3 @@ jw_tgse_VEPCASL //pCASL 3D Oxford - [DTI examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Diffusion_Tensor_Imaging). - [Archival (old) examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Archival_MRI). - [Unusual examples](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Unusual_MRI). - diff --git a/SuperBuild/SuperBuild.cmake b/SuperBuild/SuperBuild.cmake index caf2d932..2a0a956f 100644 --- a/SuperBuild/SuperBuild.cmake +++ b/SuperBuild/SuperBuild.cmake @@ -156,4 +156,3 @@ option(BUILD_DOCS "Build documentation (manpages)" OFF) if(BUILD_DOCS) add_subdirectory(docs) endif() - diff --git a/Troubleshooting/README.md b/Troubleshooting/README.md index 8ebffb10..a05c6015 100644 --- a/Troubleshooting/README.md +++ b/Troubleshooting/README.md @@ -1,6 +1,6 @@ ## About -The DICOM standard has become the dominant imaging format in medicine. However, it is necessarily complex. The relative simplicity of NIfTI makes it popular with many scientific tools.Further, to protect participant privacy scientists often wish to anonymize the datasets, removing [protected health information](https://www.hipaajournal.com/considered-phi-hipaa/). It is much easier to ensure a simple format is completely anonymized relative to a complex format.The role of dcm2niix is to convert these images into the simpler NIfTI standard. A challenge is that the DICOM standard is implemented differently by different vendors, and is evolving. Indeed, the complexity of the standard means that many DICOM images do not perfectly conform to the standard. To thoroughly read these images, one needs to develop an understanding of how each vender has interpreted the DICOM standard. Due to these factors, dcm2niix may not always create the results you expect. +The DICOM standard has become the dominant imaging format in medicine. However, it is necessarily complex. The relative simplicity of NIfTI makes it popular with many scientific tools.Further, to protect participant privacy scientists often wish to anonymize the datasets, removing [protected health information](https://www.hipaajournal.com/considered-phi-hipaa/). It is much easier to ensure a simple format is completely anonymized relative to a complex format.The role of dcm2niix is to convert these images into the simpler NIfTI standard. A challenge is that the DICOM standard is implemented differently by different vendors, and is evolving. Indeed, the complexity of the standard means that many DICOM images do not perfectly conform to the standard. To thoroughly read these images, one needs to develop an understanding of how each vendor has interpreted the DICOM standard. Due to these factors, dcm2niix may not always create the results you expect. This page explains situations where dcm2niix may fail or generate impoverished results. If this page is unable to resolve your problem, you may want to consider creating a [new issue report on the Github web page](https://github.com/rordenlab/dcm2niix/issues). diff --git a/VERSIONS.md b/VERSIONS.md index a1a371e7..0f61dc09 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -139,4 +139,4 @@ - Support for CT scans with gantry tilt and varying distance between slices. 11-Oct-2014 - - Initial public release. \ No newline at end of file + - Initial public release. diff --git a/batch_config.yml b/batch_config.yml index c6b892a9..d22a987c 100644 --- a/batch_config.yml +++ b/batch_config.yml @@ -13,4 +13,4 @@ Files: - in_dir: /path/to/second/folder out_dir: /path/to/output/folder - filename: fa3 \ No newline at end of file + filename: fa3 diff --git a/console/jpg_0XC3.cpp b/console/jpg_0XC3.cpp index 7b5d689f..54e5dc4b 100644 --- a/console/jpg_0XC3.cpp +++ b/console/jpg_0XC3.cpp @@ -72,7 +72,7 @@ int decodePixelDifference(unsigned char *lRawRA, long *lRawPos, int *lCurrentBit do { lInputBits++; lInput = (lInput << 1) + readBit(lRawRA, lRawPos, lCurrentBitPos); - if (l.DHTliRA[lInputBits] != 0) { //if any entires with this length + if (l.DHTliRA[lInputBits] != 0) { //if any entries with this length for (int lI = l.DHTstartRA[lInputBits]; lI <= (l.DHTstartRA[lInputBits] + l.DHTliRA[lInputBits] - 1); lI++) { if (lInput == l.HufCode[lI]) lHufValSSSS = l.HufVal[lI]; diff --git a/console/main_console_batch.cpp b/console/main_console_batch.cpp index b0cd383b..e968962e 100644 --- a/console/main_console_batch.cpp +++ b/console/main_console_batch.cpp @@ -1,7 +1,7 @@ // main.m dcm2niix // by Chris Rorden on 3/22/14, see license.txt // Copyright (c) 2014 Chris Rorden. All rights reserved. -// yaml batch suport by Benjamin Irving, 2016 - maintains copyright +// yaml batch support by Benjamin Irving, 2016 - maintains copyright #include //requires VS 2015 or later #ifdef _MSC_VER diff --git a/console/makefile b/console/makefile index d88192fd..49084991 100644 --- a/console/makefile +++ b/console/makefile @@ -26,8 +26,8 @@ ifneq ($(OS),Windows_NT) # For .app bundles, the Info.plist is a separate file, for executables it is appended as a section #you can check that the Info.plist section has been inserted with either of these commands # otool -l ./dcm2niix | grep info_plist -B1 -A10 - # launchctl plist ./dcm2niix - #MacOS links g++ to clang++, for gcc install via homebrew and replace g++ with /usr/local/bin/gcc-9 + # launchctl plist ./dcm2niix + #MacOS links g++ to clang++, for gcc install via homebrew and replace g++ with /usr/local/bin/gcc-9 endif endif all: diff --git a/console/nifti1_io_core.cpp b/console/nifti1_io_core.cpp index 9935af68..5f85a78c 100644 --- a/console/nifti1_io_core.cpp +++ b/console/nifti1_io_core.cpp @@ -855,10 +855,3 @@ vec3 nifti_mat33_eig3(double bxx, double bxy, double bxz, double byy, double byz //printf("bvec = [%g 0 0; 0 %g 0; 0 0 %g]\n", v3.v[0], v3.v[1], v3.v[2]); return v3; } - - - - - - - diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index cadd048b..3c7395ae 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -112,7 +112,7 @@ unsigned char * imagetoimg(opj_image_t *image) { return NULL; } //n.b. Analyze rgb-24 are PLANAR e.g. RRR..RGGG..GBBB..B not RGBRGBRGB...RGB - int pix = 0; //ouput pixel + int pix = 0; //output pixel for (int cmptno = 0; cmptno < numcmpts; ++cmptno) { int cpix = 0; //component pixel int *v = image->comps[cmptno].data; @@ -1505,7 +1505,7 @@ int headerDcm2Nii(struct TDICOMdata d, struct nifti_1_header *h, bool isComputeS return EXIT_FAILURE; } if ((h->datatype == DT_UINT16) && (d.bitsStored > 0) && (d.bitsStored < 16)) - h->datatype = DT_INT16; // DT_INT16 is more widely supported, same represenation for values 0..32767 + h->datatype = DT_INT16; // DT_INT16 is more widely supported, same representation for values 0..32767 for (int i = 0; i < 8; i++) { h->pixdim[i] = 0.0f; h->dim[i] = 0; @@ -2870,7 +2870,7 @@ unsigned char *nii_rgb2planar(unsigned char *bImg, struct nifti_1_header *hdr, i } //nii_rgb2Planar() unsigned char *nii_iVaries(unsigned char *img, struct nifti_1_header *hdr, struct TDTI4D *dti4D) { - //each DICOM image can have its own intesity scaling, whereas NIfTI requires the same scaling for all images in a file + //each DICOM image can have its own intensity scaling, whereas NIfTI requires the same scaling for all images in a file //WARNING: do this BEFORE nii_check16bitUnsigned!!!! //if (hdr->datatype != DT_INT16) return img; int dim3to7 = 1; diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 9fbb8e32..32cadb37 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -43,7 +43,7 @@ extern "C" { #define kCCsuf " CompilerNA" //unknown compiler! #endif #if defined(__arm__) || defined(__ARM_ARCH) - #define kCPUsuf " ARM" + #define kCPUsuf " ARM" #elif defined(__x86_64) #define kCPUsuf " x86-64" #else @@ -228,7 +228,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; float frameDuration, ecat_isotope_halflife, ecat_dosage; float pixelPaddingValue; // used for both FloatPixelPaddingValue (0028, 0122) and PixelPaddingValue (0028, 0120); NaN if not present. double acquisitionDuration, triggerDelayTime, RWVScale, RWVIntercept, dateTime, acquisitionTime, acquisitionDate, bandwidthPerPixelPhaseEncode; - char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[kDICOMStr], decayCorrection[kDICOMStr], attenuationCorrectionMethod[kDICOMStr],reconstructionMethod[kDICOMStr]; + char parallelAcquisitionTechnique[kDICOMStr], radiopharmaceutical[kDICOMStr], convolutionKernel[kDICOMStr], unitsPT[kDICOMStr], decayCorrection[kDICOMStr], attenuationCorrectionMethod[kDICOMStr],reconstructionMethod[kDICOMStr]; char imageOrientationText[kDICOMStr], coilElements[kDICOMStr], coilName[kDICOMStr], phaseEncodingDirectionDisplayedUIH[kDICOMStr], imageBaseName[kDICOMStr], scanOptions[kDICOMStr], stationName[kDICOMStr], softwareVersions[kDICOMStr], deviceSerialNumber[kDICOMStr], institutionName[kDICOMStr], referringPhysicianName[kDICOMStr], instanceUID[kDICOMStr], seriesInstanceUID[kDICOMStr], studyInstanceUID[kDICOMStr], bodyPartExamined[kDICOMStr], procedureStepDescription[kDICOMStr], imageType[kDICOMStr], institutionalDepartmentName[kDICOMStr], manufacturersModelName[kDICOMStr], patientID[kDICOMStr], patientOrient[kDICOMStr], patientName[kDICOMStr], accessionNumber[kDICOMStr], seriesDescription[kDICOMStr], studyID[kDICOMStr], sequenceName[kDICOMStr], protocolName[kDICOMStr],sequenceVariant[kDICOMStr],scanningSequence[kDICOMStr], patientBirthDate[kDICOMStr], patientAge[kDICOMStr], studyDate[kDICOMStr],studyTime[kDICOMStr]; char institutionAddress[kDICOMStrLarge], imageComments[kDICOMStrLarge]; uint32_t dimensionIndexValues[MAX_NUMBER_OF_DIMENSIONS]; @@ -246,7 +246,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; void getFileNameX( char *pathParent, const char *path, int maxLen); struct TDICOMdata readDICOMv(char * fname, int isVerbose, int compressFlag, struct TDTI4D *dti4D); struct TDICOMdata readDICOMx(char * fname, struct TDCMprefs* prefs, struct TDTI4D *dti4D); - + struct TDICOMdata readDICOM(char * fname); struct TDICOMdata clear_dicom_data(void); struct TDICOMdata nii_readParRec (char * parname, int isVerbose, struct TDTI4D *dti4D, bool isReadPhase); diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 359bdf7f..4a8dbd4e 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -284,7 +284,7 @@ void geCorrectBvecs(struct TDICOMdata *d, int sliceDir, struct TDTI *vx, int isV if ((vLen > 0.03) && (vLen < 0.97)) { //bVal scaled by norm(g)^2 issue163,245 float bValtemp = 0, bVal = 0, bVecScale = 0; - // rounding by 5 with mimimum of 5 if b-value > 0 + // rounding by 5 with minimum of 5 if b-value > 0 bValtemp = vx[i].V[0] * (vLen * vLen); if (bValtemp > 0 && bValtemp < 5) { bVal = 5; @@ -1408,7 +1408,7 @@ tse3d: T2*/ json_Float(fp, "\t\"RepetitionTimeExcitation\": %g,\n", dti4D->repetitionTimeExcitation); json_Float(fp, "\t\"RepetitionTimeInversion\": %g,\n", dti4D->repetitionTimeInversion); json_Bool(fp, "\t\"MTState\": %s,\n", d.mtState); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP] - //SpoilingState + //SpoilingState bool isSpoiled = (d.spoiling > kSPOILING_NONE); if ((d.spoiling == kSPOILING_UNKOWN) && (strstr(d.sequenceVariant, "\\SP") != NULL)) //BIDS suggests 0018,9016 Siemens V-series do not populate this, (0018,0021) CS [SK\MTC\SP] isSpoiled = true; @@ -2584,11 +2584,11 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s if (d3 < 3) return true; //always consistent float dx = intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]); - bool isConsistent = !isSameFloatGE(dx, 0.0); //slice distance of zero is not consistent with XYZT order (perhaps XYTZ) + bool isConsistent = !isSameFloatGE(dx, 0.0); //slice distance of zero is not consistent with XYZT order (perhaps XYTZ) bool isAscending1 = (dx > 0); for (int v = 0; v < d4; v++) { int volStart = v * d3; - if (!isSameFloatGE(intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[volStart].indx]), 0.0)) + if (!isSameFloatGE(intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[volStart].indx]), 0.0)) isConsistent = false; //XYZT requires first slice of each volume is at same position for (int i = 1; i < d3; i++) { dx = intersliceDistanceSigned(dcmList[dcmSort[volStart + i - 1].indx], dcmList[dcmSort[volStart + i].indx]); @@ -2620,11 +2620,11 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s } bool isUseInstanceNumberForVolume = false; if ((d4 > 1) && (maxPhase == 1) && (minVol == maxVolNotADC) && (minInstance < maxInstance)) { - printWarning("Volume number does not vary (0019,10A2; 0020,0100; 2005,1063; 2005,1413), assuming meaningful instance number (0020,0013).\n"); + printWarning("Volume number does not vary (0019,10A2; 0020,0100; 2005,1063; 2005,1413), assuming meaningful instance number (0020,0013).\n"); isUseInstanceNumberForVolume = true; } bool isVerbose = (verbose > 1); //issue533 - if (isVerbose) + if (isVerbose) printMessage("Ranges volume %d..%d instance %d..%d\n", minVol, maxVolNotADC, minInstance, maxInstance); //TODO bool isASL = (dcmList[dcmSort[0].indx].aslFlags != kASL_FLAG_NONE); //we will renumber volumes for Philips ASL (Contrast/Label, phase) and DWI (derived trace) @@ -2645,13 +2645,13 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s if (isASL) { #ifdef myMatchEnhanced00209157 //issue533: make classic DICOMs match enhanced DICOM volume order //disk order: slice < repeat < phase < label/control - vol += (phase - 1) * maxVol; - if (isAslLabel) + vol += (phase - 1) * maxVol; + if (isAslLabel) vol += maxPhase * maxVol; #else //"temporal" disk order: slice < phase < label/control < repeat : should match instance number - vol = phase; - if (isAslLabel) + vol = phase; + if (isAslLabel) vol += maxPhase; vol += (rawvol - 1) * (2 * maxPhase); #endif @@ -2669,8 +2669,8 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s floatSort[i].index = i; } //n.b. should we change dim[3] and dim[4] if number of volumes = dim[3]? - if ((maxVolOut-minVolOut+1) != d4) - printError("Check sorted order: 4D dataset has %d volumes, but volume index ranges from %d..%d\n", d4, minVolOut, maxVolOut); + if ((maxVolOut-minVolOut+1) != d4) + printError("Check sorted order: 4D dataset has %d volumes, but volume index ranges from %d..%d\n", d4, minVolOut, maxVolOut); //printf("dx = %g instance %d %d\n", intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]), dcmList[dcmSort[0].indx].imageNum, dcmList[dcmSort[1].indx].imageNum); TDCMsort *dcmSortIn = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); for (int i = 0; i < nConvert; i++) @@ -5112,7 +5112,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o //used for oldSliceTimingGE if (!opts.isIgnorex0021x105E) { if ((geMajorVersionInt >= 28) && (d->CSA.sliceTiming[0] >= 0.0)) { - //if (opts.isVerbose > 1) + //if (opts.isVerbose > 1) printMessage("GEversion %.1f, slice timing from DICOM (0021,105E).\n", geMajorVersion); return; //trust slice timings for versions > 27, see issue 336 } @@ -5150,7 +5150,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o // Estimate GE Slice Time only for EPI Multi-Phase (epi) or EPI fMRI/BrainWave (epiRT) // // BrainWave (epiRT) - if (d->epiVersionGE >= kGE_EPI_PEPOLAR_FWD) + if (d->epiVersionGE >= kGE_EPI_PEPOLAR_FWD) printWarning("GE ABCD pepolar research sequence handling is experimental\n");// else if ((d->epiVersionGE == 1) || (strstr(ioptGE, "FMRI") != NULL)) { //-1 = not epi, 0 = epi, 1 = epiRT d->epiVersionGE = 1; @@ -5168,7 +5168,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o } // EPI Multi-Phase (epi) with Variable Delays (Unsupported) if (groupDelay < -0.5) { - printWarning("SliceTiming Unspported: GE Multi-Phase EPI with Variable Delays\n"); + printWarning("SliceTiming Unsupported: GE Multi-Phase EPI with Variable Delays\n"); d->CSA.sliceTiming[0] = -1; return; } @@ -5634,7 +5634,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if ((nConvert > 1) && ((dcmList[indx0].modality == kMODALITY_PT) || ((opts.isForceOnsetTimes) && (dcmList[indx0].manufacturer != kMANUFACTURER_GE)))) { if (dcmList[dcmSort[0].indx].manufacturer == kMANUFACTURER_PHILIPS) { ensureSequentialSlicePositions(hdr0.dim[3], hdr0.dim[4], dcmSort, dcmList, opts.isVerbose); //issue529 - indx0 = dcmSort[0].indx; + indx0 = dcmSort[0].indx; } //printf("Bogo529\n"); return EXIT_SUCCESS; //note: GE 0008,0032 unreliable, see mb=6 data from sw27.0 20201026 @@ -6039,7 +6039,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if (nConvert > 0) { float dx = sliceMMarray[1] - sliceMMarray[0]; float thr = fabs(dx) * 0.1; - for (int i = 2; i < nConvert; i++) + for (int i = 2; i < nConvert; i++) if (fabs(dx- (sliceMMarray[i]-sliceMMarray[i-1])) > (thr) ) { printWarning("Unable to rotate 3D volume: slices not equidistant: %g != %g\n", dx, sliceMMarray[i]-sliceMMarray[i-1]); isSliceEquidistant = false; @@ -6298,7 +6298,7 @@ int saveDcm2Nii(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata dcmLi dcmList[indx].isHasPhase = dti4D->isPhase[i]; dcmList[indx].isHasReal = dti4D->isReal[i]; dcmList[indx].isHasImaginary = dti4D->isImaginary[i]; - dcmList[indx].triggerDelayTime = dti4D->triggerDelayTime[i]; + dcmList[indx].triggerDelayTime = dti4D->triggerDelayTime[i]; dcmList[indx].isHasMagnitude = false; dcmList[indx].echoNum = echoNum[i]; break; @@ -7556,14 +7556,14 @@ void readFindPigz(struct TDCMopts *opts, const char *argv[]) { #else char str[PATH_MAX]; //possible pigz names - const char *nams[] = { + const char *names[] = { "pigz", "pigz_mricron", "pigz_afni", }; -#define n_nam (sizeof(nams) / sizeof(const char *)) +#define n_nam (sizeof(names) / sizeof(const char *)) for (int n = 0; n < (int)n_nam; n++) { - if (findpathof(str, nams[n])) { + if (findpathof(str, names[n])) { strcpy(opts->pigzname, str); //printMessage("Found pigz: %s\n", str); return; @@ -7584,16 +7584,16 @@ void readFindPigz(struct TDCMopts *opts, const char *argv[]) { strcat(exepth, appendChar); //see if pigz in any path for (int n = 0; n < (int)n_nam; n++) { - //printf ("%d: %s\n", i, nams[n]); + //printf ("%d: %s\n", i, names[n]); for (int p = 0; p < (int)n_pth; p++) { strcpy(str, pths[p]); - strcat(str, nams[n]); + strcat(str, names[n]); if (is_exe(str)) goto pigzFound; } //p //check exepth strcpy(str, exepth); - strcat(str, nams[n]); + strcat(str, names[n]); if (is_exe(str)) goto pigzFound; } //n diff --git a/console/nii_ortho.h b/console/nii_ortho.h index 6157ed21..a0c21869 100644 --- a/console/nii_ortho.h +++ b/console/nii_ortho.h @@ -4,11 +4,11 @@ #ifdef __cplusplus extern "C" { #endif - + #ifndef USING_R #include "nifti1.h" #endif - + void mat2sForm (struct nifti_1_header *h, mat44 s); bool isMat44Canonical(mat44 R); unsigned char * nii_setOrtho(unsigned char* img, struct nifti_1_header *h); diff --git a/cpfiles.command b/cpfiles.command index fd885e69..8f6dadcd 100755 --- a/cpfiles.command +++ b/cpfiles.command @@ -15,4 +15,4 @@ cp *.c ../xcode/dcm2/core cp *.cpp ../xcode/dcm2/core cp *.h ../xcode/dcm2/core -#myDisableMiniZ \ No newline at end of file +#myDisableMiniZ diff --git a/docs/source/dcm2niibatch.rst b/docs/source/dcm2niibatch.rst index 1e6bc14e..574d5a46 100644 --- a/docs/source/dcm2niibatch.rst +++ b/docs/source/dcm2niibatch.rst @@ -77,5 +77,3 @@ Licensing Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. - - diff --git a/docs/source/dcm2niix.rst b/docs/source/dcm2niix.rst index a5d15e00..525d5604 100644 --- a/docs/source/dcm2niix.rst +++ b/docs/source/dcm2niix.rst @@ -62,7 +62,7 @@ Options If "y", create default file on completion If "n", default will not be written If "o", only reset and write defaults - If "i", the values of the defualts file are ignored + If "i", the values of the defaults file are ignored : reset defaults], default n) -h Show help @@ -123,4 +123,4 @@ Licensing Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. -The dcm2niix project is distributed under the BSD 2-Clause License. \ No newline at end of file +The dcm2niix project is distributed under the BSD 2-Clause License. diff --git a/docs/source/index.rst b/docs/source/index.rst index 73b31743..fe8fdf64 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,4 +19,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/license.txt b/license.txt index 353b3877..f6a2737f 100644 --- a/license.txt +++ b/license.txt @@ -41,4 +41,4 @@ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rmfiles.command b/rmfiles.command index 6df2e785..5dbbb324 100755 --- a/rmfiles.command +++ b/rmfiles.command @@ -13,4 +13,3 @@ rm ./xcode/dcm2/core/*.c rm ./xcode/dcm2/core/*.cpp rm ./xcode/dcm2/core/*.h rm ./xcode/dcm2/core/tinydir.h - From c662f4cd099017e11011be8a544f99fa94a55aa8 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 14 Sep 2021 11:24:49 -0400 Subject: [PATCH 06/20] Verbose = 2 will report ProtocolBlockGE (0025,101B) for all GE scans (not only if slice timing is calculated) --- console/miniz.c | 2 +- console/nifti1.h | 2 +- console/nii_dicom_batch.cpp | 22 +++++++++++++++++++--- console/ujpeg.cpp | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/console/miniz.c b/console/miniz.c index 9ab92891..47d10109 100644 --- a/console/miniz.c +++ b/console/miniz.c @@ -15,7 +15,7 @@ Since DICOM images are inherently limited to 2gb, dcm2niix will keep using v1.15 * Change History 10/13/13 v1.15 r4 - Interim bugfix release while I work on the next major release with Zip64 support (almost there!): - Critical fix for the MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY bug (thanks kahmyong.moon@hp.com) which could cause locate files to not find files. This bug - would only have occured in earlier versions if you explicitly used this flag, OR if you used mz_zip_extract_archive_file_to_heap() or mz_zip_add_mem_to_archive_file_in_place() + would only have occurred in earlier versions if you explicitly used this flag, OR if you used mz_zip_extract_archive_file_to_heap() or mz_zip_add_mem_to_archive_file_in_place() (which used this flag). If you can't switch to v1.15 but want to fix this bug, just remove the uses of this flag from both helper funcs (and of course don't use the flag). - Bugfix in mz_zip_reader_extract_to_mem_no_alloc() from kymoon when pUser_read_buf is not NULL and compressed size is > uncompressed size - Fixing mz_zip_reader_extract_*() funcs so they don't try to extract compressed data from directory entries, to account for weird zipfiles which contain zero-size compressed data on dir entries. diff --git a/console/nifti1.h b/console/nifti1.h index 680d1637..eb935e05 100644 --- a/console/nifti1.h +++ b/console/nifti1.h @@ -873,7 +873,7 @@ typedef struct { unsigned char r,g,b; } rgb_byte ; as a displacement field or vector: - dataset must have a 5th dimension - intent_code must be NIFTI_INTENT_DISPVECT - - dim[5] must be the dimensionality of the displacment + - dim[5] must be the dimensionality of the displacement vector (e.g., 3 for spatial displacement, 2 for in-plane) */ #define NIFTI_INTENT_DISPVECT 1006 /* specifically for displacements */ diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 4a8dbd4e..9228d6c3 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -5086,16 +5086,30 @@ void sliceTimingGE_Testx0021x105E(struct TDICOMdata *d, struct TDCMopts opts, st printMessage("\t%g\t%g\n", d->CSA.sliceTiming[v], sliceTiming[v]); } +void reportProtocolBlockGE(struct TDICOMdata *d, const char *filename) { +#ifdef myReadGeProtocolBlock + if ((d->manufacturer != kMANUFACTURER_GE) || (d->protocolBlockStartGE < 1) || (d->protocolBlockLengthGE < 19)) + return; + int viewOrderGE = -1; + int sliceOrderGE = -1; + int mbAccel = -1; + int nSlices = -1; + float groupDelay = 0.0; + char ioptGE[3000] = ""; + geProtocolBlock(filename, d->protocolBlockStartGE, d->protocolBlockLengthGE, 2, &sliceOrderGE, &viewOrderGE, &mbAccel, &nSlices, &groupDelay, ioptGE); +#endif +} + void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts opts, struct nifti_1_header *hdr, struct TDCMsort *dcmSort, struct TDICOMdata *dcmList) { //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->manufacturer != kMANUFACTURER_GE) + return; if ((d->is3DAcq) || (d->isLocalizer) || (hdr->dim[4] < 2)) return; //no need for slice times if (hdr->dim[3] < 2) return; - if (d->manufacturer != kMANUFACTURER_GE) - return; if ((d->protocolBlockStartGE < 128) || (d->protocolBlockLengthGE < 10)) { d->CSA.sliceTiming[0] = -1; printWarning("Unable to determine GE Slice timing, no Protocol Data Block GE (0025,101B): %s\n", filename); @@ -5134,7 +5148,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o float groupDelay = 0.0; char ioptGE[3000] = ""; //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, &mbAccel, &nSlices, &groupDelay, ioptGE); + int ok = geProtocolBlock(filename, d->protocolBlockStartGE, d->protocolBlockLengthGE, 0, &sliceOrderGE, &viewOrderGE, &mbAccel, &nSlices, &groupDelay, ioptGE); if (ok != EXIT_SUCCESS) { d->CSA.sliceTiming[0] = -1; printWarning("Unable to estimate slice times: issue decoding GE protocol block.\n"); @@ -5902,6 +5916,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d if (hdr0.dim[4] > 1) //for 4d datasets, last volume should be acquired before first checkDateTimeOrder(&dcmList[dcmSort[0].indx], &dcmList[dcmSort[nConvert - 1].indx]); } + if (opts.isVerbose > 1) + reportProtocolBlockGE(&dcmList[indx0], nameList->str[dcmSort[0].indx]); int sliceDir = sliceTimingCore(dcmSort, dcmList, &hdr0, opts.isVerbose, nameList->str[dcmSort[0].indx], nConvert, opts); #ifdef myReportSliceFilenames if (sliceDir < 0) { diff --git a/console/ujpeg.cpp b/console/ujpeg.cpp index 2c44a4af..b082427b 100644 --- a/console/ujpeg.cpp +++ b/console/ujpeg.cpp @@ -107,7 +107,7 @@ /////////////////////////////////////////////////////////////////////////////// // HEADER SECTION // -// copy and pase this into nanojpeg.h if you want // +// copy and paste this into nanojpeg.h if you want // /////////////////////////////////////////////////////////////////////////////// #ifndef _NANOJPEG_H From 4445198545edb34099995865392bd8f0656daf8b Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 16 Sep 2021 10:55:31 -0400 Subject: [PATCH 07/20] DWI vector removal detection (https://github.com/rordenlab/dcm2niix/issues/542) --- console/nii_dicom.cpp | 6 ++++++ console/nii_dicom_batch.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 3c7395ae..9bb35b29 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4401,6 +4401,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); int userData12GE = 0; int overlayRows = 0; int overlayCols = 0; + bool isNeologica = false; bool isTriggerSynced = false; bool isProspectiveSynced = false; bool isDICOMANON = false; //issue383 @@ -5138,6 +5139,8 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; case kStudyDate: dcmStr(lLength, &buffer[lPos], d.studyDate); + if (((int)strlen(d.studyDate) > 7) && (strstr(d.studyDate, "19000101") != NULL)) + isNeologica = true; break; case kModality: if (lLength < 2) @@ -7448,6 +7451,9 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //in practice 0020,0110 not used //https://github.com/bids-standard/bep001/blob/repetitiontime/Proposal_RepetitionTime.md } + //issue 542 + if ((d.manufacturer == kMANUFACTURER_GE) && (isNeologica) && (!isSameFloat(d.CSA.dtiV[0], 0.0f)) && ((isSameFloat(d.CSA.dtiV[1], 0.0f)) && (isSameFloat(d.CSA.dtiV[2], 0.0f)) && (isSameFloat(d.CSA.dtiV[3], 0.0f)) ) ) + printWarning("GE DWI vectors may have been removed by Neologica DICOM Anonymizer Pro (Issue 542)\n"); //start: issue529 TODO JJJJ if ((!isSameFloat(d.CSA.dtiV[0], 0.0f)) && ((isSameFloat(d.CSA.dtiV[1], 0.0f)) && (isSameFloat(d.CSA.dtiV[2], 0.0f)) && (isSameFloat(d.CSA.dtiV[3], 0.0f)) ) ) gradientOrientationNumberPhilips = kMaxDTI4D + 1; //Philips includes derived Trace/ADC images into raw DTI, these should be removed... diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 9228d6c3..d2ab4feb 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -2209,7 +2209,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st bvals[i] = kADCval; } else vx[i].V[0] = 0; //e.g. GE raw B=0 where bval=900, bvec=0,0,0 - } //see issue 245 + } //see issue 245, however this does impact some anonymized files where bvec but not bval removed https://neurostars.org/t/dcm2bids-after-conversion-to-bids-bvals-are-zeros/20198/11 if (((dcmList[indx0].manufacturer == kMANUFACTURER_PHILIPS)) && (isADCnotDTI(vx[i]))) { *numADC = *numADC + 1; bvals[i] = kADCval; From 9f2a6fa52aca93b78045410e50f2ed3241d16faa Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Fri, 17 Sep 2021 16:01:42 -0400 Subject: [PATCH 08/20] Experimental support for MGH format export --- Mediso/README.md | 9 ++ console/main_console.cpp | 6 +- console/nii_dicom.h | 2 +- console/nii_dicom_batch.cpp | 298 +++++++++++++++++++++++++++++++----- console/nii_dicom_batch.h | 12 +- license.txt | 6 +- 6 files changed, 286 insertions(+), 47 deletions(-) create mode 100644 Mediso/README.md diff --git a/Mediso/README.md b/Mediso/README.md new file mode 100644 index 00000000..02f83576 --- /dev/null +++ b/Mediso/README.md @@ -0,0 +1,9 @@ +## About + +dcm2niix attempts to convert all DICOM images to NIfTI. However, different manufacturers handle the format differently. [Mediso](https://mediso.com/usa/en/) is a manufacturer that supports preclinical tools for PET, MRI, SPECT and CT. + +In general, this manufacturer uses public tags and generates simple DICOM headers. While these files do not contain the rich meta data available from other manufacturers, they are simple to parse. + +## Sample Datasets + + - The [ftp://medical.nema.org/MEDICAL](ftp://medical.nema.org/MEDICAL) server provides reference images in Dicom/Datasets/WG30/Mediso diff --git a/console/main_console.cpp b/console/main_console.cpp index 1f25b041..a5aaf799 100644 --- a/console/main_console.cpp +++ b/console/main_console.cpp @@ -80,7 +80,7 @@ void showHelp(const char *argv[], struct TDCMopts opts) { printf(" -ba : anonymize BIDS (y/n, default %c)\n", bool2Char(opts.isAnonymizeBIDS)); printf(" -c : comment stored in NIfTI aux_file (provide up to 24 characters e.g. '-c first_visit')\n"); printf(" -d : directory search depth. Convert DICOMs in sub-folders of in_folder? (0..9, default %d)\n", opts.dirSearchDepth); - printf(" -e : export as NRRD instead of NIfTI (y/n, default n)\n"); + printf(" -e : export as NRRD (y) or MGH (o) instead of NIfTI (y/n/o, default n)\n"); #ifdef mySegmentByAcq #define kQstr " %%q=sequence number," #else @@ -356,7 +356,9 @@ int main(int argc, const char *argv[]) { if (invalidParam(i, argv)) return 0; if ((argv[i][0] == 'y') || (argv[i][0] == 'Y') || (argv[i][0] == '1')) - opts.isSaveNRRD = true; + opts.saveFormat = kSaveFormatNRRD; + if ((argv[i][0] == 'o') || (argv[i][0] == 'O') || (argv[i][0] == '2')) + opts.saveFormat = kSaveFormatMGH; } else if ((argv[i][1] == 'g') && ((i + 1) < argc)) { i++; if (invalidParam(i, argv)) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 32cadb37..b132f70c 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210909" +#define kDCMdate "v1.0.20210917" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index d2ab4feb..cf1d2806 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -1949,6 +1949,31 @@ tse3d: T2*/ #ifndef USING_R +void swapEndian(struct nifti_1_header *hdr, unsigned char *im, bool isNative) { + //swap endian from big->little or little->big + // must be told which is native to detect datatype and number of voxels + // one could also auto-detect: hdr->sizeof_hdr==348 + if (!isNative) + swap_nifti_header(hdr); + int nVox = 1; + for (int i = 1; i < 8; i++) + if (hdr->dim[i] > 1) + nVox = nVox * hdr->dim[i]; + int bitpix = hdr->bitpix; + int datatype = hdr->datatype; + if (isNative) + swap_nifti_header(hdr); + if (datatype == DT_RGBA32) + return; + //n.b. do not swap 8-bit, 24-bit RGB, and 32-bit RGBA + if (bitpix == 16) + nifti_swap_2bytes(nVox, im); + if (bitpix == 32) + nifti_swap_4bytes(nVox, im); + if (bitpix == 64) + nifti_swap_8bytes(nVox, im); +} + void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char *filename) { struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); dti4D->sliceOrder[0] = -1; @@ -2345,7 +2370,7 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st images->addDeferredAttribute("bValues", bValues); images->addDeferredAttribute("bVectors", bVectors, numDti, 3); #else - if (opts.isSaveNRRD) { + if (opts.saveFormat != kSaveFormatNIfTI) { if (numDti < kMaxDTI4D) { dcmList[indx0].CSA.numDti = numDti; for (int i = 0; i < numDti; i++) //for each direction @@ -3254,7 +3279,7 @@ void nii_createDummyFilename(char *niiFilename, struct TDCMopts opts) { nii_createFilename(d, niiFilenameBase, opts); strcpy(niiFilename, "Example output filename: '"); strcat(niiFilename, niiFilenameBase); - if (opts.isSaveNRRD) { + if (opts.saveFormat != kSaveFormatNIfTI) { if (opts.isGz) strcat(niiFilename, ".nhdr'"); else @@ -3316,7 +3341,6 @@ void writeNiiGz(char *baseName, struct nifti_1_header hdr, unsigned char *src_bu free(pCmp); return; } - //unsigned char *pHdr = (unsigned char *)malloc(hdrPadBytes); unsigned char *pHdr; if (!isSkipHeader) { //add header @@ -3588,6 +3612,202 @@ int pigz_File(char *fname, struct TDCMopts opts, size_t imgsz) { return EXIT_SUCCESS; } // pigz_File() +#define kMGHpad 97 +//typedef struct __attribute__((packed)) { +struct __attribute__((__packed__)) Tmgh { + int32_t version, width,height,depth,nframes,type,dof; + int16_t goodRASFlag; + float spacingX,spacingY,spacingZ,xr,xa,xs,yr,ya,ys,zr,za,zs,cr,ca,cs; + int16_t pad[kMGHpad]; +}; + +struct __attribute__((__packed__)) TmghFooter { + float TR, FlipAngle, TE, TI; +}; + +void writeMghGz(char *baseName, struct Tmgh hdr, struct TmghFooter footer, unsigned char *src_buffer, unsigned long src_len, int gzLevel) { + //create gz file in RAM, save to disk http://www.zlib.net/zlib_how.html + // in general this single-threaded approach is slower than PIGZ but is useful for slow (network attached) disk drives + char fname[2048] = {""}; + strcpy(fname, baseName); + unsigned long hdrPadBytes = sizeof(hdr); //348 byte header + 4 byte pad + unsigned long cmp_len = mz_compressBound(src_len + hdrPadBytes); + unsigned char *pCmp = (unsigned char *)malloc(cmp_len); + z_stream strm; + strm.total_in = 0; + strm.total_out = 0; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.next_out = pCmp; // output char array + strm.avail_out = (unsigned int)cmp_len; // size of output + int zLevel = MZ_DEFAULT_LEVEL; //Z_DEFAULT_COMPRESSION; + if ((gzLevel > 0) && (gzLevel < 11)) + zLevel = gzLevel; + if (zLevel > MZ_UBER_COMPRESSION) + zLevel = MZ_UBER_COMPRESSION; + if (deflateInit(&strm, zLevel) != Z_OK) { + free(pCmp); + return; + } + unsigned char *pHdr; + //add header + strm.avail_in = (unsigned int)sizeof(hdr); // size of input + strm.next_in = (uint8_t *) &hdr.version; + deflate(&strm, Z_NO_FLUSH); + //add image + strm.avail_in = (unsigned int)src_len; // size of input + strm.next_in = (uint8_t *)src_buffer; // input image -- TPX strm.next_in = (Bytef *)src_buffer; + deflate(&strm, Z_FINISH); + //add footer + strm.avail_in = (unsigned int)sizeof(footer); // size of input + strm.next_in = (uint8_t *) &footer.TR; + deflate(&strm, Z_NO_FLUSH); + //finish up + deflateEnd(&strm); + unsigned long file_crc32 = mz_crc32(0L, Z_NULL, 0); + file_crc32 = mz_crc32(file_crc32, (uint8_t *) &hdr.version, (unsigned int)sizeof(hdr)); + file_crc32 = mz_crc32(file_crc32, src_buffer, (unsigned int)src_len); + file_crc32 = mz_crc32(file_crc32, (uint8_t *) &footer.TR, (unsigned int)sizeof(footer)); + cmp_len = strm.total_out; + if (cmp_len <= 0) { + free(pCmp); + free(src_buffer); + return; + } + FILE *fileGz = fopen(fname, "wb"); + if (!fileGz) { + free(pCmp); + free(src_buffer); + return; + } + //write header http://www.gzip.org/zlib/rfc-gzip.html + fputc((char)0x1f, fileGz); //ID1 + fputc((char)0x8b, fileGz); //ID2 + fputc((char)0x08, fileGz); //CM - use deflate compression method + fputc((char)0x00, fileGz); //FLG - no addition fields + fputc((char)0x00, fileGz); //MTIME0 + fputc((char)0x00, fileGz); //MTIME1 + fputc((char)0x00, fileGz); //MTIME2 + fputc((char)0x00, fileGz); //MTIME2 + fputc((char)0x00, fileGz); //XFL + fputc((char)0xff, fileGz); //OS + //write Z-compressed data + fwrite(&pCmp[2], sizeof(char), cmp_len - 6, fileGz); //-6 as LZ78 format has 2 bytes header (typically 0x789C) and 4 bytes tail (ADLER 32) + //write tail: write redundancy check and uncompressed size as bytes to ensure LITTLE-ENDIAN order + fputc((unsigned char)(file_crc32), fileGz); + fputc((unsigned char)(file_crc32 >> 8), fileGz); + fputc((unsigned char)(file_crc32 >> 16), fileGz); + fputc((unsigned char)(file_crc32 >> 24), fileGz); + fputc((unsigned char)(strm.total_in), fileGz); + fputc((unsigned char)(strm.total_in >> 8), fileGz); + fputc((unsigned char)(strm.total_in >> 16), fileGz); + fputc((unsigned char)(strm.total_in >> 24), fileGz); + fclose(fileGz); + free(pCmp); +} //writeMghGz() + +int nii_saveMGH(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d, struct TDTI4D *dti4D, int numDTI) { +// FreeeSurfer does not use a permissive license, so we must reverse engineer code +// https://surfer.nmr.mgh.harvard.edu/fswiki/FsTutorial/MghFormat + int n, nDim = hdr.dim[0]; + //printMessage("NRRD writer is experimental\n"); + if (nDim < 1) + return EXIT_FAILURE; + bool isGz = opts.isGz; + size_t imgsz = nii_ImgBytes(hdr); + if ((isGz) && (imgsz >= 2147483647)) { + printWarning("Saving huge image uncompressed (many GZip tools have 2 Gb limit).\n"); + isGz = false; + } + //fill the footer + TmghFooter footer; + footer.TR = d.TR; + footer.FlipAngle = d.flipAngle; + footer.TE = d.TE; + footer.TI = d.TI; + #ifdef __LITTLE_ENDIAN__ //mgh data ALWAYS big endian! + nifti_swap_4bytes(4, &footer.TR); + #endif + //fill the header + Tmgh mgh; + mgh.version = 1; + mgh.width = hdr.dim[1]; + mgh.height = hdr.dim[2]; + mgh.depth = hdr.dim[3]; + mgh.nframes = max(hdr.dim[4],1); + if (hdr.datatype == DT_UINT8) + mgh.type = 0; + else if (hdr.datatype == DT_INT16) + mgh.type = 4; + else if (hdr.datatype == DT_INT32) + mgh.type = 1; + else if (hdr.datatype == DT_FLOAT32) + mgh.type = 3; + else { + printError("MGH format does not support NIfTI datatype %d\n", hdr.datatype); + return EXIT_FAILURE; + } + mgh.dof = 0; + mgh.goodRASFlag = 1; + float xmm = hdr.pixdim[1]; + float ymm = hdr.pixdim[2]; + float zmm = hdr.pixdim[3]; + //avoid divide by zero errors: + if (xmm <= 0.0) xmm = 1.0; + if (ymm <= 0.0) ymm = 1.0; + if (zmm <= 0.0) zmm = 1.0; + mgh.spacingX = xmm; + mgh.spacingY = ymm; + mgh.spacingZ = zmm; + mgh.xr = hdr.srow_x[0] / xmm; + mgh.xa = hdr.srow_y[0] / xmm; + mgh.xs = hdr.srow_z[0] / xmm; + mgh.yr = hdr.srow_x[1] / ymm; + mgh.ya = hdr.srow_y[1] / ymm; + mgh.ys = hdr.srow_z[1] / ymm; + mgh.zr = hdr.srow_x[2] / zmm; + mgh.za = hdr.srow_y[2] / zmm; + mgh.zs = hdr.srow_z[2] / zmm; + float vec[3]; + vec[0] = hdr.dim[1] * 0.5; + vec[1] = hdr.dim[2] * 0.5; + vec[2] = hdr.dim[3] * 0.5; + mgh.cr = hdr.srow_x[0]*vec[0] + hdr.srow_x[1]*vec[1] + hdr.srow_x[2]*vec[2] + hdr.srow_x[3]; + mgh.ca = hdr.srow_y[0]*vec[0] + hdr.srow_y[1]*vec[1] + hdr.srow_y[2]*vec[2] + hdr.srow_y[3]; + mgh.cs = hdr.srow_z[0]*vec[0] + hdr.srow_z[1]*vec[1] + hdr.srow_z[2]*vec[2] + hdr.srow_z[3]; + #ifdef __LITTLE_ENDIAN__ //mgh data ALWAYS big endian! + nifti_swap_4bytes(7, &mgh.version); + nifti_swap_2bytes(1, &mgh.goodRASFlag); + nifti_swap_4bytes(15, &mgh.spacingX); + #endif + for (int i = 0; i < kMGHpad; i++) + mgh.pad[i] = 0; + //write the data + char fname[2048] = {""}; + strcpy(fname, niiFilename); + #ifdef __LITTLE_ENDIAN__ //mgh data ALWAYS big endian! + swapEndian(&hdr, im, true); //byte-swap endian (e.g. little->big) + #endif + if (isGz) { + strcat(fname, ".mgz"); + writeMghGz(fname, mgh, footer, im, imgsz, opts.gzLevel); + } else { + strcat(fname, ".mgh"); + FILE *fp = fopen(fname, "wb"); + if (!fp) + return EXIT_FAILURE; + fwrite(&mgh, sizeof(Tmgh), 1, fp); + fwrite(&im[0], imgsz, 1, fp); + fwrite(&footer, sizeof(TmghFooter), 1, fp); + fclose(fp); + } + #ifdef __LITTLE_ENDIAN__ //mgh data ALWAYS big endian! + swapEndian(&hdr, im, false); //byte-swap endian (e.g. little->big) + #endif + return EXIT_SUCCESS; +} // nii_saveMGH() + int nii_saveNRRD(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d, struct TDTI4D *dti4D, int numDTI) { int n, nDim = hdr.dim[0]; //printMessage("NRRD writer is experimental\n"); @@ -3852,6 +4072,12 @@ int nii_saveNRRD(char *niiFilename, struct nifti_1_header hdr, unsigned char *im return pigz_File(fname, opts, imgsz); } // nii_saveNRRD() +int nii_saveForeign(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d, struct TDTI4D *dti4D, int numDTI) { + if (opts.saveFormat == kSaveFormatMGH) + return nii_saveMGH(niiFilename, hdr, im, opts, d, dti4D, numDTI); + return nii_saveNRRD(niiFilename, hdr, im, opts, d, dti4D, numDTI); +}// nii_saveForeign() + #endif void removeSclSlopeInter(struct nifti_1_header *hdr, unsigned char *img) { @@ -3912,37 +4138,12 @@ void removeSclSlopeInter(struct nifti_1_header *hdr, unsigned char *img) { #ifndef USING_R -void swapEndian(struct nifti_1_header *hdr, unsigned char *im, bool isNative) { - //swap endian from big->little or little->big - // must be told which is native to detect datatype and number of voxels - // one could also auto-detect: hdr->sizeof_hdr==348 - if (!isNative) - swap_nifti_header(hdr); - int nVox = 1; - for (int i = 1; i < 8; i++) - if (hdr->dim[i] > 1) - nVox = nVox * hdr->dim[i]; - int bitpix = hdr->bitpix; - int datatype = hdr->datatype; - if (isNative) - swap_nifti_header(hdr); - if (datatype == DT_RGBA32) - return; - //n.b. do not swap 8-bit, 24-bit RGB, and 32-bit RGBA - if (bitpix == 16) - nifti_swap_2bytes(nVox, im); - if (bitpix == 32) - nifti_swap_4bytes(nVox, im); - if (bitpix == 64) - nifti_swap_8bytes(nVox, im); -} - int nii_saveNII(char *niiFilename, struct nifti_1_header hdr, unsigned char *im, struct TDCMopts opts, struct TDICOMdata d) { if (opts.isOnlyBIDS) return EXIT_SUCCESS; - if (opts.isSaveNRRD) { + if (opts.saveFormat != kSaveFormatNIfTI) { struct TDTI4D *dti4D = (struct TDTI4D *)malloc(sizeof(struct TDTI4D)); - int ret = nii_saveNRRD(niiFilename, hdr, im, opts, d, dti4D, 0); + int ret = nii_saveForeign(niiFilename, hdr, im, opts, d, dti4D, 0); free(dti4D); return ret; } @@ -4103,6 +4304,31 @@ void nii_mask12bit(unsigned char *img, struct nifti_1_header *hdr) { img16[i] = img16[i] & 4095; //12 bit data ranges from 0..4095, any other values are overflow } +unsigned char * nii_uint16toFloat32(unsigned char *img, struct nifti_1_header *hdr, int isVerbose) { + if (hdr->datatype != DT_UINT16) + return img; + int dim3to7 = 1; + for (int i = 3; i < 8; i++) + if (hdr->dim[i] > 1) + dim3to7 = dim3to7 * hdr->dim[i]; + int nVox = hdr->dim[1] * hdr->dim[2] * dim3to7; + if (nVox < 1) + return img; + unsigned short *img16 = (unsigned short *)img; + unsigned char *imOut = (unsigned char *)malloc(nVox * 4); // *4 as 32-bits per voxel, sizeof(float) ) + float *imOut32 = (float *)imOut; + for (int i = 0; i < nVox; i++) + imOut32[i] = (hdr->scl_slope * img16[i]) + hdr->scl_inter; + free(img); + hdr->scl_slope = 1.0; + hdr->scl_inter = 1.0; + hdr->datatype = DT_FLOAT32; + hdr->bitpix = 32; + if (isVerbose) + printMessage("Converted uint16 to float32\n"); + return imOut; +} // nii_uint16toFloat32() + void nii_scale16bitSigned(unsigned char *img, struct nifti_1_header *hdr, int isVerbose) { //lossless scaling of INT16 data: e.g. input with range -100...3200 and scl_slope=1 // will be stored as -1000...32000 with scl_slope 0.1 @@ -6024,6 +6250,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d PhilipsPrecise(&dcmList[dcmSort[0].indx], opts.isPhilipsFloatNotDisplayScaling, &hdr0, opts.isVerbose); if ((dcmList[dcmSort[0].indx].bitsStored == 12) && (dcmList[dcmSort[0].indx].bitsAllocated == 16)) nii_mask12bit(imgM, &hdr0); + if ((opts.saveFormat == kSaveFormatMGH) && (hdr0.datatype == DT_UINT16)) + imgM = nii_uint16toFloat32(imgM, &hdr0, opts.isVerbose); if ((opts.isMaximize16BitRange == kMaximize16BitRange_True) && (hdr0.datatype == DT_INT16)) { nii_scale16bitSigned(imgM, &hdr0, opts.isVerbose); //allow INT16 to use full dynamic range } else if ((opts.isMaximize16BitRange == kMaximize16BitRange_True) && (hdr0.datatype == DT_UINT16) && (!dcmList[dcmSort[0].indx].isSigned)) { @@ -6052,7 +6280,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d bool isSetOrtho = false; if ((opts.isRotate3DAcq) && (dcmList[dcmSort[0].indx].is3DAcq) && (!dcmList[dcmSort[0].indx].isEPI) && (hdr0.dim[3] > 1) && (hdr0.dim[0] < 4)) { bool isSliceEquidistant = true; //issue539 - if (nConvert > 0) { + if ((nConvert > 0) && (sliceMMarray != NULL)){ float dx = sliceMMarray[1] - sliceMMarray[0]; float thr = fabs(dx) * 0.1; for (int i = 2; i < nConvert; i++) @@ -6104,7 +6332,7 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d int returnCode = EXIT_FAILURE; #ifndef myNoSave // Indicates success or failure of the (last) save - if (opts.isSaveNRRD) + if (opts.saveFormat != kSaveFormatNIfTI) removeSclSlopeInter(&hdr0, imgM); //printMessage(" x--> %d ----\n", nConvert); if (!opts.isRGBplanar) //save RGB as packed RGBRGBRGB... instead of planar RRR..RGGG..GBBB..B @@ -6182,8 +6410,8 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d #ifndef USING_R if (iVaries) printMessage("Saving as 32-bit float (slope, intercept or bits allocated varies).\n"); - if (opts.isSaveNRRD) - returnCode = nii_saveNRRD(pathoutname, hdr0, imgM, opts, dcmList[dcmSort[0].indx], dti4D, dcmList[indx0].CSA.numDti); + if (opts.saveFormat != kSaveFormatNIfTI) + returnCode = nii_saveForeign(pathoutname, hdr0, imgM, opts, dcmList[dcmSort[0].indx], dti4D, dcmList[indx0].CSA.numDti); else if (opts.isSave3D) returnCode = nii_saveNII3D(pathoutname, hdr0, imgM, opts, dcmList[dcmSort[0].indx]); else @@ -7671,7 +7899,7 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe opts->isAddNamePostFixes = true; //e.g. "_e2" added for second echo opts->isTestx0021x105E = false; //GE test slice times stored in 0021,105E opts->isIgnoreTriggerTimes = false; - opts->isSaveNRRD = false; + opts->saveFormat = kSaveFormatNIfTI; opts->isPipedGz = false; //e.g. pipe data directly to pigz instead of saving uncompressed to disk opts->isSave3D = false; opts->dirSearchDepth = 5; diff --git a/console/nii_dicom_batch.h b/console/nii_dicom_batch.h index d62c16b1..b3423c81 100644 --- a/console/nii_dicom_batch.h +++ b/console/nii_dicom_batch.h @@ -30,13 +30,16 @@ extern "C" { #define kMaximize16BitRange_False 0 //e.g. raw UINT16 values 0..4095 saved as INT16 (e.g. AFNI preserves INT16 "short", converts UINT16 to float32) #define kMaximize16BitRange_True 1 //e.g. raw UINT16 values 0..4095 saved as 0..61425 UINT16 (SPM free precision) #define kMaximize16BitRange_Raw 2 //e.g. raw UINT16 values 0..4095 saved as UINT16 (retains raw data type, AFNI would convert to float32) -#define kMaximize16BitRange_Float32 3 //save 16-bit INT16 and UINT16 as FLOAT32 (AFNI will be happy, retain scale factors in Philips data where slope varies between slices) + +#define kSaveFormatNIfTI 0 +#define kSaveFormatNRRD 1 +#define kSaveFormatMGH 2 #define MAX_NUM_SERIES 16 struct TDCMopts { - bool isIgnoreTriggerTimes, isTestx0021x105E, isAddNamePostFixes, isSaveNativeEndian, isSaveNRRD, isOneDirAtATime, isRenameNotConvert, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isForceOnsetTimes,isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isIgnoreSeriesInstanceUID, isRotate3DAcq, isCrop; - int isMaximize16BitRange, isForceStackSameSeries, nameConflictBehavior, isVerbose, isProgress, compressFlag, dirSearchDepth, gzLevel; //support for compressed data 0=none, + bool isIgnoreTriggerTimes, isTestx0021x105E, isAddNamePostFixes, isSaveNativeEndian, isOneDirAtATime, isRenameNotConvert, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isForceOnsetTimes,isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isIgnoreSeriesInstanceUID, isRotate3DAcq, isCrop; + int saveFormat, isMaximize16BitRange, 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]; double seriesNumber[MAX_NUM_SERIES]; //requires double must store -1 (report but do not convert) as well as seriesUidCrc (uint32) long numSeries; @@ -55,13 +58,10 @@ extern "C" { void setDefaultOpts (struct TDCMopts *opts, const char * argv[]); //either "setDefaultOpts(opts,NULL)" or "setDefaultOpts(opts,argv)" where argv[0] is path to search void readIniFile (struct TDCMopts *opts, const char * argv[]); int nii_saveNIIx(char * niiFilename, struct nifti_1_header hdr, unsigned char* im, struct TDCMopts opts); - //void readIniFile (struct TDCMopts *opts); int nii_loadDir(struct TDCMopts *opts); - //void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct TDTI4D *dti4D, struct nifti_1_header *h, const char * filename); void nii_SaveBIDS(char pathoutname[], struct TDICOMdata d, struct TDCMopts opts, struct nifti_1_header *h, const char * filename); int nii_createFilename(struct TDICOMdata dcm, char * niiFilename, struct TDCMopts opts); void nii_createDummyFilename(char * niiFilename, struct TDCMopts opts); - //void findExe(char name[512], const char * argv[]); #ifdef __cplusplus } #endif diff --git a/license.txt b/license.txt index f6a2737f..93120bd9 100644 --- a/license.txt +++ b/license.txt @@ -6,10 +6,10 @@ nifti.h, nifti1_io.h/nifti1_io_core.cpp is public domain ujpeg.h/ujpeg.cpp uses the MIT license (see file for license text) http://keyj.emphy.de/nanojpeg/ miniz.c is optional, it is public domain (http://unlicense.org) - https://code.google.com/p/miniz/ + https://github.com/richgel999/miniz OpenJpeg is optional, it uses a BSD license https://github.com/uclouvain/openjpeg/blob/master/LICENSE -Charls is optional, it uses a BSD license +CharLS is optional, it uses a BSD license https://github.com/team-charls/charls/blob/master/LICENSE.md zlib is optional and includes a simple license https://www.zlib.net/zlib_license.html @@ -18,7 +18,7 @@ Jasper is optional and provides its own license --- The Software has been developed for research purposes only and is not a clinical tool -Copyright (c) 2014-2016 Chris Rorden. All rights reserved. +Copyright (c) 2014-2021 Chris Rorden. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions From 5f89dc5ad68f67f38a7225a0fdd555f1bc0692f8 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Fri, 17 Sep 2021 16:23:27 -0400 Subject: [PATCH 09/20] Support packed structs for Windows and Unix (https://newbedev.com/visual-c-equivalent-of-gcc-s-attribute-packed) --- console/nii_dicom_batch.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index cf1d2806..0788aff8 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -3613,17 +3613,25 @@ int pigz_File(char *fname, struct TDCMopts opts, size_t imgsz) { } // pigz_File() #define kMGHpad 97 -//typedef struct __attribute__((packed)) { -struct __attribute__((__packed__)) Tmgh { + +#ifdef _MSC_VER +# define PACKED_STRUCT(name) \ + __pragma(pack(push, 1)) struct name __pragma(pack(pop)) +#elif defined(__GNUC__) +# define PACKED_STRUCT(name) struct __attribute__((packed)) name +#endif + +PACKED_STRUCT(Tmgh) { int32_t version, width,height,depth,nframes,type,dof; int16_t goodRASFlag; float spacingX,spacingY,spacingZ,xr,xa,xs,yr,ya,ys,zr,za,zs,cr,ca,cs; int16_t pad[kMGHpad]; }; -struct __attribute__((__packed__)) TmghFooter { +PACKED_STRUCT(TmghFooter) { float TR, FlipAngle, TE, TI; }; + void writeMghGz(char *baseName, struct Tmgh hdr, struct TmghFooter footer, unsigned char *src_buffer, unsigned long src_len, int gzLevel) { //create gz file in RAM, save to disk http://www.zlib.net/zlib_how.html From 30bed630ec403c72937aaa71cb351f1ca60d4c9d Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Fri, 17 Sep 2021 16:48:02 -0400 Subject: [PATCH 10/20] Yet another attempt to use packed structures for Windows MSVC 19 and UNIX. --- console/nii_dicom_batch.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 0788aff8..82f97a91 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -3614,26 +3614,24 @@ int pigz_File(char *fname, struct TDCMopts opts, size_t imgsz) { #define kMGHpad 97 -#ifdef _MSC_VER -# define PACKED_STRUCT(name) \ - __pragma(pack(push, 1)) struct name __pragma(pack(pop)) -#elif defined(__GNUC__) -# define PACKED_STRUCT(name) struct __attribute__((packed)) name +#ifdef __GNUC__ +#define PACKD(...) __VA_ARGS__ __attribute__((__packed__)) +#else +#define PACKD(...) __pragma(pack(push, 1)) __VA_ARGS__ __pragma(pack(pop)) #endif -PACKED_STRUCT(Tmgh) { +PACKD(typedef struct { int32_t version, width,height,depth,nframes,type,dof; int16_t goodRASFlag; float spacingX,spacingY,spacingZ,xr,xa,xs,yr,ya,ys,zr,za,zs,cr,ca,cs; int16_t pad[kMGHpad]; -}; +}) Tmgh; -PACKED_STRUCT(TmghFooter) { +PACKD(typedef struct { float TR, FlipAngle, TE, TI; -}; +}) TmghFooter; - -void writeMghGz(char *baseName, struct Tmgh hdr, struct TmghFooter footer, unsigned char *src_buffer, unsigned long src_len, int gzLevel) { +void writeMghGz(char *baseName, Tmgh hdr, TmghFooter footer, unsigned char *src_buffer, unsigned long src_len, int gzLevel) { //create gz file in RAM, save to disk http://www.zlib.net/zlib_how.html // in general this single-threaded approach is slower than PIGZ but is useful for slow (network attached) disk drives char fname[2048] = {""}; From 001336fbf7c860bfa6d5fc175f5494b78a232edc Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 28 Sep 2021 12:41:12 -0400 Subject: [PATCH 11/20] Issue 544 (https://github.com/rordenlab/dcm2niix/issues/544) --- console/nii_dicom.h | 2 +- console/nii_dicom_batch.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index b132f70c..0d13dd6c 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210917" +#define kDCMdate "v1.0.20210928" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 82f97a91..1fb501df 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -5186,15 +5186,22 @@ void sliceTimingXA(struct TDCMsort *dcmSort, struct TDICOMdata *dcmList, struct dcmList[indx0].CSA.sliceTiming[v] -= mn; } //sliceTimingXA() -void sliceTimeGE(struct TDICOMdata *d, int mb, int dim3, float TR, bool isInterleaved, bool is27r3, float groupDelaysec) { +void sliceTimeGE(struct TDICOMdata *d, int mb, int dim3, float TR, bool isInterleaved, float geMajorVersion, bool is27r3, float groupDelaysec) { //mb : multiband factor //dim3 : number of slices in volume //TRsec : repetition time in seconds //isInterleaved : interleaved or sequential slice order + //geMajorVersion: version, e.g. 29.0 //is27r3 : software release 27.0 R03 or later float sliceTiming[kMaxEPI3D]; //multiband can be fractional! 'extra' slices discarded int nExcitations = ceil(float(dim3) / float(mb)); + if ((mb > 1) && (geMajorVersion < 26.0)) { + printWarning("Unable to determine slice times for early GE HyperBand.\n"); + return; + } + if ((mb > 1) && (!is27r3) && ((nExcitations % 2) == 0) ) //number of slices divided by MB factor should is Even + nExcitations ++; //https://osf.io/q4d53/wiki/home/; Figure 3 of https://pubmed.ncbi.nlm.nih.gov/26308571/ int nDiscardedSlices = (nExcitations * mb) - dim3; float secPerSlice = (TR - groupDelaysec) / (nExcitations); if (!isInterleaved) { @@ -5435,7 +5442,7 @@ void sliceTimingGE(struct TDICOMdata *d, const char *filename, struct TDCMopts o printMessage("GEiopt: %s, groupDelay (%g), internalepiVersionGE (%d), epiVersionGE(%d)\n", ioptGE, groupDelay, d->internalepiVersionGE, d->epiVersionGE); printMessage("GEversion %s%.1f_R0%d, TRms %g, interleaved %d, multiband %d, groupdelayms %g\n", geVersionPrefix, geMajorVersion, geReleaseVersionInt, d->TR, isInterleaved, d->CSA.multiBandFactor, d->groupDelay); } - sliceTimeGE(d, d->CSA.multiBandFactor, hdr->dim[3], d->TR, isInterleaved, is27r3, d->groupDelay); + sliceTimeGE(d, d->CSA.multiBandFactor, hdr->dim[3], d->TR, isInterleaved, geMajorVersion, is27r3, d->groupDelay); sliceTimingGE_Testx0021x105E(d, opts, hdr, dcmSort, dcmList); #endif } //sliceTimingGE() From ce5adb1c7044908251bd157e20bf05bc27642b4b Mon Sep 17 00:00:00 2001 From: Jaemin Shin Date: Tue, 28 Sep 2021 14:52:07 -0400 Subject: [PATCH 12/20] Slice Times for early GE HyperBand fMRI --- console/nii_dicom_batch.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 1fb501df..0c30aa3a 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -5198,8 +5198,12 @@ void sliceTimeGE(struct TDICOMdata *d, int mb, int dim3, float TR, bool isInterl int nExcitations = ceil(float(dim3) / float(mb)); if ((mb > 1) && (geMajorVersion < 26.0)) { printWarning("Unable to determine slice times for early GE HyperBand.\n"); + d->CSA.sliceTiming[0] = -1; return; - } + } + if ((mb > 1) && (!is27r3)) { + printWarning("Slice times for this GE HyperBand dataset (version %g) are NOT yet fully validated.\n", geMajorVersion); + } if ((mb > 1) && (!is27r3) && ((nExcitations % 2) == 0) ) //number of slices divided by MB factor should is Even nExcitations ++; //https://osf.io/q4d53/wiki/home/; Figure 3 of https://pubmed.ncbi.nlm.nih.gov/26308571/ int nDiscardedSlices = (nExcitations * mb) - dim3; From 0ac10fd3807cb198bc3d7ad392da96d5cc1ee45d Mon Sep 17 00:00:00 2001 From: Jaemin Shin Date: Tue, 28 Sep 2021 22:30:33 -0400 Subject: [PATCH 13/20] GE early HyperBand --- console/nii_dicom_batch.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 0c30aa3a..fc16a4e4 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -5201,11 +5201,10 @@ void sliceTimeGE(struct TDICOMdata *d, int mb, int dim3, float TR, bool isInterl d->CSA.sliceTiming[0] = -1; return; } - if ((mb > 1) && (!is27r3)) { - printWarning("Slice times for this GE HyperBand dataset (version %g) are NOT yet fully validated.\n", geMajorVersion); - } - if ((mb > 1) && (!is27r3) && ((nExcitations % 2) == 0) ) //number of slices divided by MB factor should is Even + if ((mb > 1) && (!is27r3) && ((nExcitations % 2) == 0) ) { //number of slices divided by MB factor should is Even nExcitations ++; //https://osf.io/q4d53/wiki/home/; Figure 3 of https://pubmed.ncbi.nlm.nih.gov/26308571/ + printWarning("Slice times for this GE HyperBand dataset (version %g) are NOT yet fully validated.\n", geMajorVersion); + } int nDiscardedSlices = (nExcitations * mb) - dim3; float secPerSlice = (TR - groupDelaysec) / (nExcitations); if (!isInterleaved) { From 532655c4b23d73b3612d020bf77c663a72363a56 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 30 Sep 2021 14:43:39 -0400 Subject: [PATCH 14/20] Use both 2005,1412 and 2005,1413 to sort Philips DWI (https://github.com/rordenlab/dcm2niix/issues/546) --- console/nii_dicom.cpp | 1 + console/nii_dicom_batch.cpp | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 9bb35b29..2ee04ef5 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -7459,6 +7459,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); gradientOrientationNumberPhilips = kMaxDTI4D + 1; //Philips includes derived Trace/ADC images into raw DTI, these should be removed... //if (sliceNumberMrPhilips == 1) printf("instance\t %d\ttime\t%g\tvolume\t%d\tgradient\t%d\tphase\t%d\tisLabel\t%d\n", d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, gradientOrientationNumberPhilips, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); //if (sliceNumberMrPhilips == 1) printf("ACQtime\t%g\tinstance\t %d\ttime\t%g\tvolume\t%d\tphase\t%d\tisLabel\t%d\n", d.acquisitionTime, d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); + d.phaseNumber = (d.phaseNumber > philMRImageDiffBValueNumber) ? d.phaseNumber : philMRImageDiffBValueNumber; //we need both BValueNumber(2005,1412) and GradientOrientationNumber(2005,1413) to resolve volumes: issue546 d.rawDataRunNumber = (d.rawDataRunNumber > volumeNumber) ? d.rawDataRunNumber : volumeNumber; d.rawDataRunNumber = (d.rawDataRunNumber > gradientOrientationNumberPhilips) ? d.rawDataRunNumber : gradientOrientationNumberPhilips; if ((d.rawDataRunNumber < 0) && (d.manufacturer == kMANUFACTURER_PHILIPS) && (nDimIndxVal > 1) && (d.dimensionIndexValues[nDimIndxVal - 1] > 0)) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index fc16a4e4..971e7966 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -2657,6 +2657,8 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s int maxVolOut = -1; bool isUsePhaseForVol = false; if ((!isASL) && (minVol == maxVol) && (maxPhase > 1)) isUsePhaseForVol = true;//e.g. TurboQUASAR + bool isPhaseIsBValNumber =false; + if ((!isASL) && (minVol < maxVol) && (maxPhase > 1)) isPhaseIsBValNumber = true;//DWI track both gradient number and vector number issue 546 if (isVerbose) printMessage("InstanceNumber\tPosition\tVolume\tRepeat\tASLlabel\tPhase\tTriggerTime\n"); for (int i = 0; i < nConvert; i++) { @@ -2665,6 +2667,7 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s int instance = dcmList[dcmSort[i].indx].imageNum; int phase = max(1, dcmList[dcmSort[i].indx].phaseNumber); if (isUsePhaseForVol) vol = phase; + if (isPhaseIsBValNumber) vol += phase * maxVol; int isAslLabel = dcmList[dcmSort[i].indx].aslFlags == kASL_FLAG_PHILIPS_LABEL; dx = intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[i].indx]); if (isASL) { @@ -2694,7 +2697,7 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s floatSort[i].index = i; } //n.b. should we change dim[3] and dim[4] if number of volumes = dim[3]? - if ((maxVolOut-minVolOut+1) != d4) + if ((!isPhaseIsBValNumber) && ((maxVolOut-minVolOut+1) != d4)) printError("Check sorted order: 4D dataset has %d volumes, but volume index ranges from %d..%d\n", d4, minVolOut, maxVolOut); //printf("dx = %g instance %d %d\n", intersliceDistanceSigned(dcmList[dcmSort[0].indx], dcmList[dcmSort[1].indx]), dcmList[dcmSort[0].indx].imageNum, dcmList[dcmSort[1].indx].imageNum); TDCMsort *dcmSortIn = (TDCMsort *)malloc(nConvert * sizeof(TDCMsort)); @@ -2709,6 +2712,7 @@ bool ensureSequentialSlicePositions(int d3, int d4, struct TDCMsort dcmSort[], s return false; } // ensureSequentialSlicePositions() + 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,... int nConvert = d3 * d4; From c5a5baaae92c91f54f961926f9a9f24a1125f2c3 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Sat, 2 Oct 2021 10:14:46 -0400 Subject: [PATCH 15/20] Handle enhanced DICOM using syntax 1.2.840.10008.1.2.4.70 --- console/nii_dicom.cpp | 23 ++++++++++++++++------- console/nii_dicom.h | 1 + 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 2ee04ef5..adb2bc4b 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -1691,6 +1691,7 @@ struct TDICOMdata nii_readParRec(char *parname, int isVerbose, struct TDTI4D *dt dti4D->volumeOnsetTime[0] = -1; dti4D->decayFactor[0] = -1; dti4D->frameDuration[0] = -1; + //dti4D->fragmentOffset[0] = -1; dti4D->intenScale[0] = 0.0; strcpy(d.protocolName, ""); //erase dummy with empty strcpy(d.seriesDescription, ""); //erase dummy with empty @@ -3111,7 +3112,7 @@ struct TJPEG { long size; }; -TJPEG *decode_JPEG_SOF_0XC3_stack(const char *fn, int skipBytes, bool isVerbose, int frames, bool isLittleEndian) { +TJPEG *decode_JPEG_SOF_0XC3_stack(const char *fn, int skipBytes, int isVerbose, int frames, bool isLittleEndian) { #define abortGoto() free(lOffsetRA); return NULL; TJPEG *lOffsetRA = (TJPEG *)malloc(frames * sizeof(TJPEG)); FILE *reader = fopen(fn, "rb"); @@ -3137,7 +3138,7 @@ TJPEG *decode_JPEG_SOF_0XC3_stack(const char *fn, int skipBytes, bool isVerbose, int tagLength = dcmInt(4, &lRawRA[lRawPos], isLittleEndian); long tagEnd = lRawPos + tagLength + 4; if (isVerbose) - printMessage("Tag %#x length %d end at %ld\n", tag, tagLength, tagEnd + skipBytes); + printMessage("Frame %d Tag %#x length %d end at %ld\n", frame + 1, tag, tagLength, tagEnd + skipBytes); lRawPos += 4; //read tag length if ((lRawRA[lRawPos] != 0xFF) || (lRawRA[lRawPos + 1] != 0xD8) || (lRawRA[lRawPos + 2] != 0xFF)) { if (isVerbose) @@ -3157,7 +3158,7 @@ TJPEG *decode_JPEG_SOF_0XC3_stack(const char *fn, int skipBytes, bool isVerbose, return lOffsetRA; } -unsigned char *nii_loadImgJPEGC3(char *imgname, struct nifti_1_header hdr, struct TDICOMdata dcm, bool isVerbose) { +unsigned char *nii_loadImgJPEGC3(char *imgname, struct nifti_1_header hdr, struct TDICOMdata dcm, int isVerbose) { //arcane and inefficient lossless compression method popularized by dcmcjpeg, examples at http://www.osirix-viewer.com/resources/dicom-image-library/ int dimX, dimY, bits, frames; //clock_t start = clock(); @@ -3178,7 +3179,6 @@ unsigned char *nii_loadImgJPEGC3(char *imgname, struct nifti_1_header hdr, struc printMessage("Unable to decode JPEG. Please use dcmdjpeg to uncompress data.\n"); return NULL; } - //printMessage("JPEG %fms\n", ((double)(clock()-start))/1000); if (hdr.dim[3] != frames) { //multi-slice image saved as multiple image fragments rather than a single image //printMessage("Unable to decode all slices (%d/%d). Please use dcmdjpeg to uncompress data.\n", frames, hdr.dim[3]); if (ret != NULL) @@ -3595,7 +3595,7 @@ unsigned char *nii_loadImgXL(char *imgname, struct nifti_1_header *hdr, struct T if (hdr->datatype == DT_RGB24) //convert to planar img = nii_rgb2planar(img, hdr, dcm.isPlanarRGB); //do this BEFORE Y-Flip, or RGB order can be flipped } else if (dcm.compressionScheme == kCompressC3) - img = nii_loadImgJPEGC3(imgname, *hdr, dcm, (isVerbose > 0)); + img = nii_loadImgJPEGC3(imgname, *hdr, dcm, isVerbose); else #ifndef myDisableOpenJPEG if (((dcm.compressionScheme == kCompress50) || (dcm.compressionScheme == kCompressYes)) && (compressFlag != kCompressNone)) @@ -4043,6 +4043,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D dti4D->volumeOnsetTime[0] = -1; dti4D->decayFactor[0] = -1; dti4D->frameDuration[0] = -1; + //dti4D->fragmentOffset[0] = -1; dti4D->intenScale[0] = 0.0; struct TVolumeDiffusion volDiffusion = initTVolumeDiffusion(&d, dti4D); struct stat s; @@ -4810,6 +4811,10 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); lPos = lPos + 4; lLength = d.imageBytes; if (d.imageBytes > 128) { + /*if (encapsulatedDataFragments < kMaxDTI4D) { + dti4D->fragmentOffset[encapsulatedDataFragments] = (int)lPos + (int)lFileOffset; + dti4D->fragmentLength[encapsulatedDataFragments] = lLength; + }*/ encapsulatedDataFragments++; if (encapsulatedDataFragmentStart == 0) encapsulatedDataFragmentStart = (int)lPos + (int)lFileOffset; @@ -6754,9 +6759,9 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //http://www.dclunie.com/medical-image-faq/html/part6.html //unlike raw data, Encapsulated data is stored as Fragments contained in Items that are the Value field of Pixel Data if ((d.compressionScheme != kCompressNone) && (!isIconImageSequence)) { - lLength = 0; isEncapsulatedData = true; encapsulatedDataImageStart = (int)lPos + (int)lFileOffset; + lLength = 0; } isIconImageSequence = false; break; @@ -6914,10 +6919,14 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //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) && (encapsulatedDataFragments == numberOfFrames) && (encapsulatedDataFragments < kMaxDTI4D)) { + printWarning("Compressed image stored as %d fragments: if conversion fails decompress with gdcmconv, Osirix, dcmdjpeg or dcmjp2k %s\n", encapsulatedDataFragments, fname); + d.imageStart = encapsulatedDataFragmentStart; + } else if (encapsulatedDataFragments > 1) { printError("Compressed image stored as %d fragments: decompress with gdcmconv, Osirix, dcmdjpeg or dcmjp2k %s\n", encapsulatedDataFragments, fname); } else { d.imageStart = encapsulatedDataFragmentStart; + //dti4D->fragmentOffset[0] = -1; } } else if ((isEncapsulatedData) && (d.imageStart < 128)) { //http://www.dclunie.com/medical-image-faq/html/part6.html diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 0d13dd6c..c7e9855e 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -176,6 +176,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8; struct TDTI S[kMaxDTI4D]; int sliceOrder[kMaxSlice2D]; // [7,3,2] means the first slice on disk should be moved to 7th position int gradDynVol[kMaxDTI4D]; //used to parse dimensions of Philips data, e.g. file with multiple dynamics, echoes, phase+magnitude + //int fragmentOffset[kMaxDTI4D], fragmentLength[kMaxDTI4D]; //for images with multiple compressed fragments float frameDuration[kMaxDTI4D], decayFactor[kMaxDTI4D], volumeOnsetTime[kMaxDTI4D], triggerDelayTime[kMaxDTI4D], TE[kMaxDTI4D], RWVScale[kMaxDTI4D], RWVIntercept[kMaxDTI4D], intenScale[kMaxDTI4D], intenIntercept[kMaxDTI4D], intenScalePhilips[kMaxDTI4D]; bool isReal[kMaxDTI4D]; bool isImaginary[kMaxDTI4D]; From 7fce63a008fab1f74a203136bbab824000e3cf7e Mon Sep 17 00:00:00 2001 From: Jaemin Shin Date: Mon, 4 Oct 2021 13:56:34 -0400 Subject: [PATCH 16/20] Remove warning for early GE HyperBand dataset after validation --- console/nii_dicom_batch.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 971e7966..518d0dc1 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -5207,7 +5207,6 @@ void sliceTimeGE(struct TDICOMdata *d, int mb, int dim3, float TR, bool isInterl } if ((mb > 1) && (!is27r3) && ((nExcitations % 2) == 0) ) { //number of slices divided by MB factor should is Even nExcitations ++; //https://osf.io/q4d53/wiki/home/; Figure 3 of https://pubmed.ncbi.nlm.nih.gov/26308571/ - printWarning("Slice times for this GE HyperBand dataset (version %g) are NOT yet fully validated.\n", geMajorVersion); } int nDiscardedSlices = (nExcitations * mb) - dim3; float secPerSlice = (TR - groupDelaysec) / (nExcitations); From 5e6e87d5af796871b5dd83a58e8420f250a02c4c Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Tue, 5 Oct 2021 09:29:34 -0400 Subject: [PATCH 17/20] Version bump, describe issue 546 in documentation --- Philips/README.md | 4 ++++ console/nii_dicom.cpp | 1 + console/nii_dicom.h | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Philips/README.md b/Philips/README.md index 2b6e6bc8..cb34cd98 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -142,6 +142,10 @@ Philips DICOMs do not contain all the information desired by many neuroscientist Research users may want to explore the direct NIfTI export provided by Philips. This tool may have access to sequence information not provided in the DICOM export. However, this [manufacturer provided NIfTI export](https://github.com/rordenlab/dcm2niix/issues/529) is limited to certain image types and sequences and does not support features like FSL format diffusion bvec/bvals or BIDS sidecars. +The BIDS tag [PartialFourier](https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#in-plane-spatial-encoding) expects a fractional value. For example, an acquisition with 5/8 partial Fourier should be reported as "0.625". However, Philips DICOM only reports if partial Fourier is enabled or not, and does not reveal the fraction. + +Philips DICOMs do not allow one to determine the temporal order of volumes for diffusion (DWI, DTI) and arterial spin labelling (ASL) sequences. This can hinder tools that attempt to model motion motion or T1 effects. For ASL data, dcm2niix will attempt to store volumes in the order phase < control/label < repeat. [For ASL examples and a table describing the ordering hierarchy, see this dataset](https://github.com/neurolabusc/dcm_qa_philips_asl). For diffusion images, dcm2niix may order Philips volumes differently depending on if the data is stored as classic or enhanced DICOM. For enhanced DICOM, dcm2niix follows the hierarchy specified by DimensionIndexValues (0020,9157). On the other hand, for classic DICOMs, volumes with identical b-value index (2005,1412) will be stored sequentially, with ties sorted based on gradient direction number (2005,1413). [For diffusion examples, see series 22 and 23 from this dataset](https://github.com/neurolabusc/dcm_qa_philips_enh). These two different sorting methods may not necessarily sort data identically. + [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 or the [sequence PDF](http://adni.loni.usc.edu/wp-content/uploads/2017/09/ADNI-3-Basic-Philips-R5.pdf). For precise timing, it is also worth knowing if equidistant "temporal slice spacing" is set and whether "prospect. motion corr." is on or off (if on, a short delay occurs between volumes). 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/console/nii_dicom.cpp b/console/nii_dicom.cpp index adb2bc4b..2550ab97 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -7468,6 +7468,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); gradientOrientationNumberPhilips = kMaxDTI4D + 1; //Philips includes derived Trace/ADC images into raw DTI, these should be removed... //if (sliceNumberMrPhilips == 1) printf("instance\t %d\ttime\t%g\tvolume\t%d\tgradient\t%d\tphase\t%d\tisLabel\t%d\n", d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, gradientOrientationNumberPhilips, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); //if (sliceNumberMrPhilips == 1) printf("ACQtime\t%g\tinstance\t %d\ttime\t%g\tvolume\t%d\tphase\t%d\tisLabel\t%d\n", d.acquisitionTime, d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); + //if (sliceNumberMrPhilips == 1) printf("%d\t%d\t%g\n", philMRImageDiffBValueNumber, gradientOrientationNumberPhilips, d.CSA.dtiV[0]); //issue546 d.phaseNumber = (d.phaseNumber > philMRImageDiffBValueNumber) ? d.phaseNumber : philMRImageDiffBValueNumber; //we need both BValueNumber(2005,1412) and GradientOrientationNumber(2005,1413) to resolve volumes: issue546 d.rawDataRunNumber = (d.rawDataRunNumber > volumeNumber) ? d.rawDataRunNumber : volumeNumber; d.rawDataRunNumber = (d.rawDataRunNumber > gradientOrientationNumberPhilips) ? d.rawDataRunNumber : gradientOrientationNumberPhilips; diff --git a/console/nii_dicom.h b/console/nii_dicom.h index c7e9855e..6be07446 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210928" +#define kDCMdate "v1.0.20210930" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic From acdc1ebf8c365b59b8c77c01e781cff338433d3e Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Wed, 6 Oct 2021 16:11:24 -0400 Subject: [PATCH 18/20] Extract volume order for Philips DWI (from R5.6 onward) --- Philips/README.md | 2 ++ console/nii_dicom.cpp | 23 ++++++++++++++++++++--- console/nii_dicom.h | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Philips/README.md b/Philips/README.md index cb34cd98..b48aa7d8 100644 --- a/Philips/README.md +++ b/Philips/README.md @@ -136,6 +136,8 @@ MyCustomDirections ``` +Important tags for Philips DICOM include b-value index (2005,1412) and gradient direction number (2005,1413). Knowing both 2005,1412 nd 2005,1413 uniquely identifies a volume in a series (two volumes can share either b-value or gradient direction, but can not be identical in both dimensions). With software release R5.6 and later there are additional useful tags: DIFFUSION2_KDTI (2005, 1595, Y/N) specifies whether acquisition ordering is enabled for a series. NR_OF_DIFFUSION_ORDER (2005,1599) specifies the number of vectors in a series. DIFFUSION_ORDER (2005,1596) specifies the acquisition ordering of a series. + ## Missing Information 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. diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 2550ab97..34847853 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -4372,6 +4372,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); #define kMRImageDiffBValueNumber 0x2005 + (0x1412 << 16) //IS #define kMRImageGradientOrientationNumber 0x2005+(0x1413 << 16) //IS #define kMRImageLabelType 0x2005 + (0x1429 << 16) //CS ASL LBL_CTL https://github.com/physimals/dcm_convert_phillips/ +#define kMRImageDiffVolumeNumber 0x2005+(0x1596 << 16) //IS #define kSharedFunctionalGroupsSequence 0x5200 + uint32_t(0x9229 << 16) // SQ #define kPerFrameFunctionalGroupsSequence 0x5200 + uint32_t(0x9230 << 16) // SQ #define kWaveformSq 0x5400 + (0x0100 << 16) @@ -4411,6 +4412,7 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); //double contentTime = 0.0; int echoTrainLengthPhil = 0; int philMRImageDiffBValueNumber = 0; + int philMRImageDiffVolumeNumber = -1; int sqDepth = 0; int acquisitionTimesGE_UIH = 0; int sqDepth00189114 = -1; @@ -4644,8 +4646,14 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); // This will ensure correct ordering of slices in 4D datasets //Canon and Bruker reverse dimensionIndexItem order relative to Philips: new versions introduce compareTDCMdimRev //printf("%d: %d %d %d %d\n", ndim, d.dimensionIndexValues[0], d.dimensionIndexValues[1], d.dimensionIndexValues[2], d.dimensionIndexValues[3]); - for (int i = 0; i < ndim; i++) - dcmDim[numDimensionIndexValues].dimIdx[i] = d.dimensionIndexValues[dimensionIndexOrder[i]]; + if ((philMRImageDiffVolumeNumber > 0) && (sliceNumberMrPhilips > 0)) { //issue546: 2005,1596 provides temporal order + dcmDim[numDimensionIndexValues].dimIdx[0] = 1; + dcmDim[numDimensionIndexValues].dimIdx[1] = sliceNumberMrPhilips; + dcmDim[numDimensionIndexValues].dimIdx[2] = philMRImageDiffVolumeNumber; + } else { + for (int i = 0; i < ndim; i++) + dcmDim[numDimensionIndexValues].dimIdx[i] = d.dimensionIndexValues[dimensionIndexOrder[i]]; + } dcmDim[numDimensionIndexValues].TE = TE; dcmDim[numDimensionIndexValues].intenScale = d.intenScale; dcmDim[numDimensionIndexValues].intenIntercept = d.intenIntercept; @@ -6477,6 +6485,11 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); break; philMRImageDiffBValueNumber = dcmStrInt(lLength, &buffer[lPos]); break; + case kMRImageDiffVolumeNumber: + if (d.manufacturer != kMANUFACTURER_PHILIPS) + break; + philMRImageDiffVolumeNumber = dcmStrInt(lLength, &buffer[lPos]); + break; case kWaveformSq: d.imageStart = 1; //abort!!! printMessage("Skipping DICOM (audio not image) '%s'\n", fname); @@ -7468,12 +7481,16 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); gradientOrientationNumberPhilips = kMaxDTI4D + 1; //Philips includes derived Trace/ADC images into raw DTI, these should be removed... //if (sliceNumberMrPhilips == 1) printf("instance\t %d\ttime\t%g\tvolume\t%d\tgradient\t%d\tphase\t%d\tisLabel\t%d\n", d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, gradientOrientationNumberPhilips, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); //if (sliceNumberMrPhilips == 1) printf("ACQtime\t%g\tinstance\t %d\ttime\t%g\tvolume\t%d\tphase\t%d\tisLabel\t%d\n", d.acquisitionTime, d.imageNum, MRImageDynamicScanBeginTime, volumeNumber, d.phaseNumber, d.aslFlags == kASL_FLAG_PHILIPS_LABEL); - //if (sliceNumberMrPhilips == 1) printf("%d\t%d\t%g\n", philMRImageDiffBValueNumber, gradientOrientationNumberPhilips, d.CSA.dtiV[0]); //issue546 + //if (sliceNumberMrPhilips == 1) printf("%d\t%d\t%g\t%d\n", philMRImageDiffBValueNumber, gradientOrientationNumberPhilips, d.CSA.dtiV[0], philMRImageDiffVolumeNumber); //issue546 d.phaseNumber = (d.phaseNumber > philMRImageDiffBValueNumber) ? d.phaseNumber : philMRImageDiffBValueNumber; //we need both BValueNumber(2005,1412) and GradientOrientationNumber(2005,1413) to resolve volumes: issue546 d.rawDataRunNumber = (d.rawDataRunNumber > volumeNumber) ? d.rawDataRunNumber : volumeNumber; d.rawDataRunNumber = (d.rawDataRunNumber > gradientOrientationNumberPhilips) ? d.rawDataRunNumber : gradientOrientationNumberPhilips; if ((d.rawDataRunNumber < 0) && (d.manufacturer == kMANUFACTURER_PHILIPS) && (nDimIndxVal > 1) && (d.dimensionIndexValues[nDimIndxVal - 1] > 0)) d.rawDataRunNumber = d.dimensionIndexValues[nDimIndxVal - 1]; //Philips enhanced scans converted to classic with dcuncat + if (philMRImageDiffVolumeNumber > 0) { //use 2005,1596 for Philips DWI >= R5.6 + d.rawDataRunNumber = philMRImageDiffVolumeNumber; + d.phaseNumber = 0; + } // d.rawDataRunNumber = (d.rawDataRunNumber > d.phaseNumber) ? d.rawDataRunNumber : d.phaseNumber; //will not work: conflict for MultiPhase ASL with multiple averages //end: issue529 if (hasDwiDirectionality) diff --git a/console/nii_dicom.h b/console/nii_dicom.h index 6be07446..800d29e2 100644 --- a/console/nii_dicom.h +++ b/console/nii_dicom.h @@ -50,7 +50,7 @@ extern "C" { #define kCPUsuf " " //unknown CPU #endif -#define kDCMdate "v1.0.20210930" +#define kDCMdate "v1.0.20211006" #define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic From b974ddc55fa068c54200c5831978deb4601d96e7 Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 7 Oct 2021 07:57:23 -0400 Subject: [PATCH 19/20] Unify PASL JSON tags. --- Siemens/README.md | 52 ++--------------------------------------------- 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/Siemens/README.md b/Siemens/README.md index ec98c853..8609f80a 100644 --- a/Siemens/README.md +++ b/Siemens/README.md @@ -56,56 +56,8 @@ For Siemens V-series systems from the B-generation onward (around 2005), the mos ## Arterial Spin Labeling -Tools like [ExploreASL](https://sites.google.com/view/exploreasl) and [FSL BASIL](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/BASIL) can help process arterial spin labeling data. These tools require sequence details. These details differ between different sequences. If you create a BIDS JSON file with dcm2niix, the following tags will be created, using the same names used in the Siemens sequence PDFs. Note different sequences provide different values. The [dcm_qa_asl](https://github.com/neurolabusc/dcm_qa_asl) repository provides example DICOM ASL datasets. - -ep2d_pcasl, ep2d_pcasl_UI_PHC //pCASL 2D [Danny J.J. Wang](http://www.loft-lab.org) - - LabelOffset - - PostLabelDelay - - NumRFBlocks - - RFGap - - MeanGzx10 - - PhiAdjust - -tgse_pcasl //pCASL 3D [Danny J.J. Wang](http://www.loft-lab.org) - - RFGap - - MeanGzx10 - - T1 - -ep2d_pasl //PASL 2D Siemens Product - - InversionTime - - SaturationStopTime - -tgse_pasl //PASL 3D [Siemens Product](http://adni.loni.usc.edu/wp-content/uploads/2010/05/ADNI3_Basic_Siemens_Skyra_E11.pdf) - - BolusDuration - - InversionTime - -ep2d_fairest //PASL 2D http://www.pubmed.com/11746944 http://www.pubmed.com/21606572 - - PostInversionDelay - - PostLabelDelay - -to_ep2d_VEPCASL //pCASL 2D specific tags - Oxford (Thomas OKell) - - InversionTime - - BolusDuration - - TagRFFlipAngle - - TagRFDuration - - TagRFSeparation - - MeanTagGradient - - TagGradientAmplitude - - TagDuration - - MaximumT1Opt - - InitialPostLabelDelay [Array] - -jw_tgse_VEPCASL //pCASL 3D Oxford - - TagRFFlipAngle - - TagRFDuration - - TagRFSeparation - - MaximumT1Opt - - Tag0 - - Tag1 - - Tag2 - - Tag3 - - InitialPostLabelDelay [Array] - +Tools like [ExploreASL](https://sites.google.com/view/exploreasl) and [FSL BASIL](https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/BASIL) can help process arterial spin labeling data. These tools require sequence details. These details differ between different sequences. If you create a BIDS JSON file with dcm2niix, the following tags will be created, using the same names used in the Siemens sequence PDFs. Note different sequences provide different values. The [dcm_qa_asl](https://github.com/neurolabusc/dcm_qa_asl) repository provides example DICOM ASL datasets. See the [BIDS page for details](../BIDS/README.md). + ## Sample Datasets - [Slice timing dataset](httphttps://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#Slice_timing_corrections://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage). From 48f369d87e080c54c13f6f0e732c89ea9712c5cc Mon Sep 17 00:00:00 2001 From: neurolabusc Date: Thu, 7 Oct 2021 11:36:13 -0400 Subject: [PATCH 20/20] Update dcm_qa submodule. --- console/nii_dicom.cpp | 2 -- console/nii_dicom_batch.cpp | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/console/nii_dicom.cpp b/console/nii_dicom.cpp index 34847853..8cba19d2 100644 --- a/console/nii_dicom.cpp +++ b/console/nii_dicom.cpp @@ -7262,8 +7262,6 @@ const uint32_t kEffectiveTE = 0x0018 + (0x9082 << 16); bool isScaleVaries = false; //setting j = 1 in next few lines is a hack, just in case TE/scale/intercept listed AFTER dimensionIndexValues int j = 0; - //if (d.xyzDim[3] > 1) //not sure why this was included, disrupts Philips multiPLD ASL where each slice has different triggertime - // j = 1; for (int i = 0; i < d.xyzDim[4]; i++) { int slice = j + (i * d.xyzDim[3]); //dti4D->gradDynVol[i] = 0; //only PAR/REC diff --git a/console/nii_dicom_batch.cpp b/console/nii_dicom_batch.cpp index 518d0dc1..0ec44bce 100644 --- a/console/nii_dicom_batch.cpp +++ b/console/nii_dicom_batch.cpp @@ -1743,10 +1743,10 @@ tse3d: T2*/ if (d.durationLabelPulseGE > 0) { json_Float(fp, "\t\"LabelingDuration\": %g,\n", d.durationLabelPulseGE / 1000.0); json_Float(fp, "\t\"PostLabelingDelay\": %g,\n", d.TI / 1000.0); //For GE ASL: InversionTime -> Post-label delay + json_Float(fp, "\t\"NumberOfPointsPerArm\": %g,\n", d.numberOfPointsPerArm); + json_Float(fp, "\t\"NumberOfArms\": %g,\n", d.numberOfArms); } - json_Float(fp, "\t\"NumberOfPointsPerArm\": %g,\n", d.numberOfPointsPerArm); - json_Float(fp, "\t\"NumberOfArms\": %g,\n", d.numberOfArms); - json_Float(fp, "\t\"NumberOfExcitations\": %g,\n", d.numberOfExcitations); + if (d.numberOfExcitations > 1) json_Float(fp, "\t\"NumberOfExcitations\": %g,\n", d.numberOfExcitations); if ((d.CSA.multiBandFactor > 1) && (d.modality == kMODALITY_MR)) //AccelFactorSlice fprintf(fp, "\t\"MultibandAccelerationFactor\": %d,\n", d.CSA.multiBandFactor); json_Float(fp, "\t\"PercentPhaseFOV\": %g,\n", d.phaseFieldofView);